feat: first working version
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
upload
|
||||||
|
data.db
|
||||||
|
target/*
|
||||||
|
logs/*
|
||||||
2214
Cargo.lock
generated
Normal file
28
Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "pack"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2024"
|
||||||
|
authors = ["Kenneth Jao"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
axum = { version = "0.8.1", features = ["multipart"] }
|
||||||
|
bytes = "1.10.1"
|
||||||
|
env_filter = "0.1.3"
|
||||||
|
log = "0.4.27"
|
||||||
|
mime = "0.3.17"
|
||||||
|
rand = { version = "0.9.1", features = ["alloc"] }
|
||||||
|
reqwest = { version = "0.12.15", features = ["json"] }
|
||||||
|
serde = { version= "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
|
sqlite = "0.37.0"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tower = "0.5.2"
|
||||||
|
tower-http = { version = "0.6.2", features = ["fs", "trace"] }
|
||||||
|
trace = "0.1.7"
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-appender = "0.2.3"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
|
||||||
|
url = "2.5.4"
|
||||||
@ -1,3 +1,6 @@
|
|||||||
# pack
|
# pack
|
||||||
|
|
||||||
A web server for hosting built packages, designed to be connected with Gitea actions and repositories.
|
A web server written in [rust](https://www.rust-lang.org/) with [axum](https://docs.rs/axum/latest/axum/) for hosting built packages as a
|
||||||
|
destination endpoint for packages in a continuous integration workflow. It is designed around Gitea, using it for its authentication
|
||||||
|
as well as integrating with Gitea actions.
|
||||||
|
|
||||||
|
|||||||
75
graphics/code128.svg
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<svg data-cached="true" width="1596" height="432" viewBox="0 0 1596 432" version="1.1">
|
||||||
|
<path d="M 0,432 H 12 V 0 H 0 Z" />
|
||||||
|
<path d="m 18,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 36,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 66,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="M 84,432 H 96 V 0 H 84 Z" />
|
||||||
|
<path d="m 120,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 132,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 150,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 180,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 198,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 216,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 246,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 264,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 276,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 294,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 330,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 342,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 378,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 396,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 426,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 444,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 462,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 474,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 504,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 528,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 540,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 570,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 594,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 606,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 624,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 660,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 678,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 690,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 726,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 756,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 768,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 792,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 828,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 846,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 858,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 876,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 900,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 924,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 960,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 978,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 990,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1020,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1044,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1056,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1074,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1086,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1122,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1146,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 1176,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1188,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1206,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1230,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 1254,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 1284,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 1308,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1320,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1332,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1356,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1386,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1398,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 1428,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1452,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1470,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1488,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 1518,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1548,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 1572,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1584,432 h 12 V 0 h -12 z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
110
graphics/favicon.svg
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 12.7 12.7"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="favicon.svg"
|
||||||
|
inkscape:export-filename="pack.svg"
|
||||||
|
inkscape:export-xdpi="24.58"
|
||||||
|
inkscape:export-ydpi="24.58"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="11.313709"
|
||||||
|
inkscape:cx="23.864853"
|
||||||
|
inkscape:cy="16.307649"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1410"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
style="display:inline">
|
||||||
|
<rect
|
||||||
|
style="fill:#5b4824;fill-opacity:0.988235;stroke-width:10.0668;stroke-linecap:round;stroke-linejoin:bevel"
|
||||||
|
id="rect1527-3"
|
||||||
|
width="6.8788414"
|
||||||
|
height="6.8788414"
|
||||||
|
x="4.8726211"
|
||||||
|
y="-2.0062201"
|
||||||
|
transform="matrix(0.92312053,0.3845107,-0.92312053,0.3845107,0,0)"
|
||||||
|
inkscape:label="Inside" />
|
||||||
|
<path
|
||||||
|
id="rect1529-5-6"
|
||||||
|
style="fill:#b6904e;fill-opacity:0.988235;stroke-width:3.48451;stroke-linecap:round;stroke-linejoin:bevel"
|
||||||
|
inkscape:label="BottomTab"
|
||||||
|
d="M 12.580184,3.7010601 6.35,6.2969694 6.3499999,6.3921383 12.7,3.7463047 Z"
|
||||||
|
sodipodi:nodetypes="ccccc" />
|
||||||
|
<path
|
||||||
|
id="rect1527-5-9-2"
|
||||||
|
style="fill:#997b40;fill-opacity:0.988235;stroke-width:8.482;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none"
|
||||||
|
d="M 12.7,3.7463047 V 3.8754156 L 9.5249998,4.4276929 V 4.298582 Z"
|
||||||
|
sodipodi:nodetypes="ccccc"
|
||||||
|
inkscape:label="RightThick" />
|
||||||
|
<path
|
||||||
|
id="rect1527"
|
||||||
|
style="fill:#e7bb69;fill-opacity:0.988235;stroke-width:8.48184;stroke-linecap:round;stroke-linejoin:bevel"
|
||||||
|
d="M 6.3499998,1.1021618 12.699492,3.7469386 9.5249998,4.2985819 3.1755071,1.653805 Z"
|
||||||
|
sodipodi:nodetypes="ccccc"
|
||||||
|
inkscape:label="Right" />
|
||||||
|
<path
|
||||||
|
id="rect1527-5-9"
|
||||||
|
style="fill:#997b40;fill-opacity:0.988235;stroke-width:8.482;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none"
|
||||||
|
d="M 9.5249998,3.3899535 9.587387,3.5101459 6.35,6.5687057 l 2e-7,-0.17699 z"
|
||||||
|
sodipodi:nodetypes="ccccc"
|
||||||
|
inkscape:label="LeftThick" />
|
||||||
|
<path
|
||||||
|
id="rect1527-5-9-9"
|
||||||
|
style="fill:#785f30;fill-opacity:0.988235;stroke-width:8.482;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none"
|
||||||
|
d="M 9.5249998,4.298582 V 4.4276929 L 8.8941256,4.1651128 8.9686178,4.0668299 Z"
|
||||||
|
sodipodi:nodetypes="ccccc"
|
||||||
|
inkscape:label="RightThick2" />
|
||||||
|
<rect
|
||||||
|
style="fill:#e7bb69;fill-opacity:0.988235;stroke-width:3.62679;stroke-linecap:round;stroke-linejoin:bevel"
|
||||||
|
id="rect1529-5"
|
||||||
|
width="6.8791666"
|
||||||
|
height="5.8977885"
|
||||||
|
x="-13.758333"
|
||||||
|
y="9.0379715"
|
||||||
|
transform="matrix(-0.92307692,0.3846154,0,1,0,0)"
|
||||||
|
inkscape:label="Right Bottom" />
|
||||||
|
<rect
|
||||||
|
style="fill:#c29b56;fill-opacity:0.988235;stroke-width:3.62679;stroke-linecap:round;stroke-linejoin:bevel"
|
||||||
|
id="rect1529"
|
||||||
|
width="6.8791666"
|
||||||
|
height="5.8977885"
|
||||||
|
x="8.1217173e-08"
|
||||||
|
y="3.7463048"
|
||||||
|
transform="matrix(0.92307692,0.38461539,0,1,0,0)"
|
||||||
|
inkscape:label="Left Bottom" />
|
||||||
|
<path
|
||||||
|
id="rect1527-5"
|
||||||
|
style="fill:#eec06a;fill-opacity:0.988235;stroke-width:8.48184;stroke-linecap:round;stroke-linejoin:bevel"
|
||||||
|
d="M 3.1749996,0.74496517 9.5249998,3.3899535 6.3500002,6.3917157 -1.524423e-7,3.7467274 Z"
|
||||||
|
sodipodi:nodetypes="ccccc"
|
||||||
|
inkscape:label="Left" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
42
graphics/lines.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
def main():
|
||||||
|
left = 20
|
||||||
|
width = 1
|
||||||
|
tape_height = 10
|
||||||
|
triangles = 8
|
||||||
|
|
||||||
|
points = []
|
||||||
|
for i in range(triangles*2 + 1):
|
||||||
|
if i % 2:
|
||||||
|
points.append(f"{left},{i*tape_height/(2*triangles)}")
|
||||||
|
else:
|
||||||
|
points.append(f"{left+width},{i*tape_height/(2*triangles)}")
|
||||||
|
|
||||||
|
points.append("100,10")
|
||||||
|
points.append("100,0")
|
||||||
|
|
||||||
|
print(" ".join(points))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
text = '<textPath href="#packTapeNUM">PACK.PACK.PACK.PACK.</textPath>'
|
||||||
|
path = '<path id="packTapeNUM" fill="none" stroke="none" d="MOFFX OFFY l6 20">'
|
||||||
|
|
||||||
|
text_list = []
|
||||||
|
path_list = []
|
||||||
|
|
||||||
|
#path = (6, 10)
|
||||||
|
#perp = (-3, 6*10/3)
|
||||||
|
|
||||||
|
for i in range(35):
|
||||||
|
text_list.append(text.replace("NUM", str(i)))
|
||||||
|
rand = -0.2*(i%3)
|
||||||
|
path_list.append(path.replace("NUM", str(i)).replace("OFFX", f"{-3 + 3*i + 3*rand:.2f}").replace("OFFY", f"{10*rand:.2f}") )
|
||||||
|
|
||||||
|
print("".join(text_list))
|
||||||
|
print("".join(path_list))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
10
graphics/pack.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 12.7 12.7" version="1.1">
|
||||||
|
<path d="m 12.323869,4.1992062 -6.2301843,2.5959093 -1e-7,0.095169 6.3500004,-2.6458336 z" />
|
||||||
|
<path d="M 12.443685,4.2444508 V 4.3735617 L 9.2686845,4.925839 V 4.7967281 Z" />
|
||||||
|
<path d="M 6.0936845,1.6003079 12.443177,4.2450847 9.2686845,4.796728 2.9191918,2.1519511 Z" />
|
||||||
|
<path d="m 9.2686845,3.8880996 0.062387,0.1201924 -3.237387,3.0585598 2e-7,-0.17699 z" />
|
||||||
|
<path d="M 9.2686845,4.7967281 V 4.925839 L 8.6378103,4.6632589 8.7123025,4.564976 Z" />
|
||||||
|
<rect width="6.8791666" height="5.8977885" x="-13.480659" y="9.4293194" transform="matrix(-0.92307692,0.3846154,0,1,0,0)" />
|
||||||
|
<rect width="6.8791666" height="5.8977885" x="-0.27767482" y="4.3512492" transform="matrix(0.92307692,0.3846154,0,1,0,0)" />
|
||||||
|
<path d="M 2.9186843,1.2431113 9.2686845,3.8880996 6.0936849,6.8898618 -0.2563155,4.2448735 Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 889 B |
3
graphics/pdf417_codesvg.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="618" height="124" viewBox="0 0 618 124" version="1.1">
|
||||||
|
<path d="M 2.1999999e-6,0 H 41.200002 V 123.6 H 2.1999999e-6 Z M 46.350002,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 20.6,0 H 113.3 v 15.45 h 5.15 V 30.9 h -5.15 v 15.45 h -5.15 V 30.9 H 103 V 46.35 H 97.850002 V 30.9 h -5.15 v 15.45 h 5.15 V 61.8 H 103 v 15.450002 h 5.15 V 61.8 h 5.15 v 61.8 H 87.550002 Z M 118.45,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 25.75 v 15.45 h 5.15 V 30.9 H 154.5 V 15.45 h -15.45 z m 36.05,0 h 15.45 v 30.9 h -5.15 v 30.9 h 5.15 v 15.450002 h -5.15 v 15.45 h -5.15 V 108.15 h 10.3 V 123.6 H 175.1 Z m 20.6,0 h 5.15 V 15.45 H 195.7 Z M 206,0 h 10.3 v 15.45 h -5.15 V 30.9 h 5.15 V 15.45 h 10.3 V 30.9 h -5.15 v 15.45 h 5.15 V 61.8 h 5.15 v 15.450002 h -5.15 v 15.45 h 5.15 V 108.15 H 226.6 V 123.6 H 216.3 V 92.700002 H 206 V 123.6 h -5.15 V 108.15 H 195.7 V 92.700002 h 5.15 v -15.45 H 206 V 61.8 h 5.15 v 15.450002 h 10.3 V 61.8 H 216.3 V 46.35 H 206 V 61.8 H 190.55 V 46.35 h 5.15 V 30.9 H 206 Z m 20.6,0 h 20.6 v 46.35 h -5.15 V 15.45 H 226.6 Z m 36.05,0 h 10.3 v 30.9 h -5.15 v 15.45 h 5.15 v 30.900002 h -5.15 v 15.45 h 10.3 V 108.15 h 5.15 v 15.45 h -10.3 v -15.45 h -5.15 v 15.45 h -5.15 z m 30.9,0 h 10.3 V 30.9 H 309 V 0 h 5.15 v 30.9 h 5.15 v 15.45 h -5.15 V 61.8 h 5.15 v 15.450002 h 5.15 v 15.45 h -10.3 v -15.45 h -10.3 v 15.45 H 298.7 V 108.15 H 288.4 V 77.250002 h 10.3 V 61.8 h 5.15 V 46.35 H 298.7 V 30.9 h -10.3 v 30.9 h -5.15 V 15.45 h 10.3 z m 25.75,0 h 5.15 v 15.45 h 20.6 V 30.9 H 319.3 Z m 30.9,0 h 5.15 v 15.45 h 5.15 V 0 h 15.45 v 15.45 h -10.3 V 30.9 h -10.3 v 15.45 h 5.15 v 46.350002 h 5.15 V 108.15 h 5.15 v 15.45 h -20.6 z m 46.35,0 H 412 v 15.45 h 5.15 v 30.9 H 412 V 61.8 h 5.15 v 15.450002 h 5.15 V 61.8 h 10.3 v 15.450002 h -5.15 v 15.45 h -20.6 v -15.45 h -10.3 V 61.8 h 10.3 V 46.35 h -10.3 V 30.9 h 5.15 V 15.45 h -5.15 z m 20.6,0 h 5.15 v 15.45 h -5.15 z m 20.6,0 h 25.75 v 15.45 h -5.15 V 30.9 h -5.15 v 15.45 h -5.15 V 61.8 h 5.15 V 46.35 h 5.15 V 77.250002 H 453.2 V 123.6 h -5.15 v -15.45 h -5.15 v 15.45 h -5.15 z m 30.9,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 H 515 V 15.45 H 494.4 V 30.9 h 25.75 V 61.8 H 515 v 15.450002 h -10.3 v 15.45 h 15.45 V 108.15 h -10.3 V 123.6 H 494.4 V 92.700002 h 5.15 V 61.8 H 494.4 V 46.35 h -5.15 z m 36.05,0 h 36.05 V 123.6 H 525.3 Z m 41.2,0 h 5.15 v 123.6 h -5.15 z m 20.6,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 15.45,0 H 618 v 123.6 h -5.15 z M 123.6,15.45 h 5.15 V 30.9 h 5.15 V 15.45 h 5.15 V 30.9 h 5.15 v 15.45 h -10.3 v 30.900002 h 15.45 v 15.45 h 5.15 V 61.8 H 144.2 V 46.35 h 25.75 V 61.8 h -5.15 v 30.900002 h 5.15 V 108.15 H 154.5 V 123.6 H 144.2 V 92.700002 h -5.15 V 123.6 H 133.9 V 108.15 H 123.6 V 92.700002 h 5.15 v -15.45 h -5.15 v 15.45 h -5.15 V 61.8 H 113.3 V 46.35 h 10.3 z m 257.5,0 h 5.15 V 30.9 h -5.15 z m 41.2,0 h 10.3 V 30.9 h -10.3 z m 41.2,0 h 5.15 V 30.9 h -5.15 z m 10.3,0 h 5.15 V 30.9 H 473.8 Z M 231.75,30.9 h 5.15 v 15.45 h -5.15 z m 41.2,0 h 5.15 v 15.45 h -5.15 z m 97.85,0 h 5.15 v 15.45 h 5.15 v 30.900002 h -5.15 V 61.8 h -5.15 z m 15.45,0 h 5.15 v 15.45 h -5.15 z m 72.1,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h 10.3 V 61.8 h 5.15 v 15.450002 h 5.15 v 15.45 h -5.15 V 123.6 h -30.9 V 77.250002 h 5.15 V 108.15 h 10.3 V 92.700002 h -5.15 v -15.45 h 5.15 V 61.8 h -5.15 V 77.250002 H 463.5 V 46.35 h 5.15 z M 103,46.35 h 5.15 V 61.8 H 103 Z m 149.35,0 h 5.15 v 77.25 h -5.15 V 108.15 H 247.2 V 92.700002 h 5.15 z m 77.25,0 h 15.45 V 61.8 h -10.3 v 15.450002 h 10.3 v 15.45 H 339.9 V 108.15 H 329.6 V 123.6 H 303.85 V 92.700002 h 10.3 V 108.15 h 10.3 V 92.700002 h 5.15 v -15.45 h -5.15 V 61.8 h 5.15 z m 61.8,0 h 5.15 V 61.8 H 391.4 Z M 278.1,61.8 h 5.15 v 15.450002 h -5.15 z m -41.2,15.450002 h 5.15 v 15.45 h -5.15 z m 128.75,0 h 10.3 v 15.45 h 5.15 v -15.45 h 5.15 v 15.45 h 5.15 V 108.15 h -5.15 v 15.45 h -10.3 V 108.15 H 370.8 V 92.700002 h -5.15 z M 103,92.700002 V 108.15 h 5.15 V 92.700002 Z m 293.55,0 h 10.3 V 108.15 h -10.3 z m 30.9,0 h 5.15 V 123.6 h -5.15 z M 118.45,108.15 h 5.15 v 15.45 h -5.15 z m 221.45,0 h 5.15 v 15.45 h -5.15 z m 66.95,0 h 10.3 v 15.45 h -10.3 z" fill-rule="evenodd" style="stroke-width:0.858333" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
504
src/api.rs
Normal file
@ -0,0 +1,504 @@
|
|||||||
|
use std::{fs, path, slice::Iter};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use std::process::Command;
|
||||||
|
use rand::distr::{Alphanumeric, SampleString};
|
||||||
|
use thiserror::Error;
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::{Path, Json, State, Multipart},
|
||||||
|
http::{StatusCode, header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use reqwest::{
|
||||||
|
Client, Method, Request, RequestBuilder, Url
|
||||||
|
};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::{query, ServerState};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
#[repr(u8)]
|
||||||
|
enum RepoFeature {
|
||||||
|
Docs = 0,
|
||||||
|
Builds = 1,
|
||||||
|
Nightly = 2,
|
||||||
|
PreProd = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatureList = Vec<String>;
|
||||||
|
|
||||||
|
impl RepoFeature {
|
||||||
|
pub fn iter() -> Iter<'static, RepoFeature> {
|
||||||
|
static REPOFEATURE: [RepoFeature; 4] = [RepoFeature::Docs, RepoFeature::Builds, RepoFeature::Nightly, RepoFeature::PreProd];
|
||||||
|
REPOFEATURE.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum APIError {
|
||||||
|
#[error("Request error: {0}")]
|
||||||
|
RequestError(#[from] reqwest::Error),
|
||||||
|
#[error("SQL error: {0}")]
|
||||||
|
SQLError(#[from] sqlite::Error),
|
||||||
|
#[error("Filesystem error: {0}")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
#[error("Multipart extract error: {0}")]
|
||||||
|
MultipartError(#[from] axum::extract::multipart::MultipartError),
|
||||||
|
#[error("Unexpected input in JSON: {msg}")]
|
||||||
|
InvalidJson { msg: String },
|
||||||
|
#[error("No such feature '{feature}' exists")]
|
||||||
|
InvalidFeature { feature: String },
|
||||||
|
#[error("The feature '{feature}' is not enabled for this repository")]
|
||||||
|
DisabledFeature { feature: String },
|
||||||
|
#[error("The current user is not an owner of '{repo}'")]
|
||||||
|
Unauthorized { repo: String },
|
||||||
|
#[error("A token was not provided or is malformed")]
|
||||||
|
Tokenless,
|
||||||
|
#[error("{msg}")]
|
||||||
|
Other { msg: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
InvalidToken = 0, // Invalid OAuth token.
|
||||||
|
External = 1, // Error originates from an external location/server.
|
||||||
|
Filesystem = 2, // Error originates from filesystem operation.
|
||||||
|
Sql = 3, // Error originates from SQL.
|
||||||
|
BadRequest = 4, // Error originates from client request.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
status: u16,
|
||||||
|
code: u16,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result<T, E = APIError> = core::result::Result<T, E>;
|
||||||
|
impl IntoResponse for APIError {
|
||||||
|
fn into_response(self) -> Response<Body> {
|
||||||
|
let (status, code) = match self {
|
||||||
|
APIError::RequestError(ref err) => {
|
||||||
|
let status = err.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
let action = match status {
|
||||||
|
StatusCode::FORBIDDEN => ErrorCode::InvalidToken,
|
||||||
|
_ => ErrorCode::External
|
||||||
|
};
|
||||||
|
(status, action)
|
||||||
|
},
|
||||||
|
APIError::SQLError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Sql),
|
||||||
|
APIError::IOError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Filesystem),
|
||||||
|
APIError::MultipartError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::BadRequest),
|
||||||
|
APIError::InvalidJson{..} => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::External),
|
||||||
|
APIError::InvalidFeature{..} => (StatusCode::BAD_REQUEST, ErrorCode::BadRequest),
|
||||||
|
APIError::DisabledFeature{..} => (StatusCode::NOT_FOUND, ErrorCode::BadRequest),
|
||||||
|
APIError::Unauthorized{..} => (StatusCode::FORBIDDEN, ErrorCode::BadRequest),
|
||||||
|
APIError::Tokenless => (StatusCode::FORBIDDEN, ErrorCode::InvalidToken),
|
||||||
|
APIError::Other{..} => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::External),
|
||||||
|
};
|
||||||
|
(status, Json(ErrorResponse{
|
||||||
|
status: u16::from(status),
|
||||||
|
code: code as u16,
|
||||||
|
message: self.to_string(),
|
||||||
|
})).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RepoFeature> for &str {
|
||||||
|
fn from(value: RepoFeature) -> Self {
|
||||||
|
match value {
|
||||||
|
RepoFeature::Docs => "docs",
|
||||||
|
RepoFeature::Builds => "builds",
|
||||||
|
RepoFeature::Nightly => "nightly",
|
||||||
|
RepoFeature::PreProd => "preprod",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for RepoFeature {
|
||||||
|
type Error = APIError;
|
||||||
|
|
||||||
|
fn try_from(value: &str) -> Result<Self> {
|
||||||
|
match value.to_lowercase().as_str() {
|
||||||
|
"docs" => Ok(RepoFeature::Docs),
|
||||||
|
"builds" => Ok(RepoFeature::Builds),
|
||||||
|
"nightly" => Ok(RepoFeature::Nightly),
|
||||||
|
"preprod" => Ok(RepoFeature::PreProd),
|
||||||
|
other => Err(APIError::InvalidFeature { feature: other.to_owned() }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_feature_list(value: u64) -> FeatureList {
|
||||||
|
let mut v: Vec<String> = Vec::new();
|
||||||
|
for feature in RepoFeature::iter() {
|
||||||
|
let feat_num = 1 << (*feature as u64);
|
||||||
|
if (value & feat_num) == feat_num {
|
||||||
|
v.push(Into::<&str>::into(*feature).to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn gitea_api<T, U>(url: &str, method: Method, payload: &T, token: &str) -> Result<U> where
|
||||||
|
T: Serialize + ?Sized,
|
||||||
|
U: serde::de::DeserializeOwned + Default
|
||||||
|
{
|
||||||
|
// Make request to Gitea.
|
||||||
|
let res = RequestBuilder::from_parts(Client::new(), Request::new(method, Url::parse(url).unwrap()))
|
||||||
|
.header("User-Agent", "TestBot")
|
||||||
|
.header("Authorization", format!("token {token}"))
|
||||||
|
.json(payload)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
res.error_for_status_ref()?; // Return with error if Gitea request has error.
|
||||||
|
|
||||||
|
let mut content_type = "".to_owned();
|
||||||
|
if res.headers().contains_key(CONTENT_TYPE) {
|
||||||
|
content_type = res.headers().get(CONTENT_TYPE)
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| APIError::Other { msg: "Content-Type in Gitea response invalid".to_owned() })?
|
||||||
|
.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
if content_type.contains("application/json") {
|
||||||
|
Ok(res.json::<U>().await?)
|
||||||
|
} else {
|
||||||
|
Ok(U::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authorize(host: &str, token: &str, repo: &str) -> Result<Json<Value>> {
|
||||||
|
// Use repos API call to check admin permission. Return the repo JSON as
|
||||||
|
// well in case it needs to be used later.
|
||||||
|
let url = format!("{host}/api/v1/repos/{repo}");
|
||||||
|
let data = Empty{};
|
||||||
|
|
||||||
|
let json: Value = gitea_api(&url, Method::GET, &data, token).await?;
|
||||||
|
|
||||||
|
// If permission is not admin level, return error.
|
||||||
|
json.get("permissions")
|
||||||
|
.ok_or(APIError::InvalidJson{ msg: "Couldn't find 'permissions' key.".to_owned() })?
|
||||||
|
.get("admin")
|
||||||
|
.ok_or(APIError::InvalidJson{ msg: "Couldn't find 'admin' key.".to_owned() })?
|
||||||
|
.as_bool()
|
||||||
|
.ok_or(APIError::InvalidJson{ msg: "Value in 'admin' is not bool.".to_owned() })?
|
||||||
|
.then_some(0)
|
||||||
|
.ok_or(APIError::Unauthorized{ repo: repo.to_owned() })?;
|
||||||
|
|
||||||
|
Ok(Json(json))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub struct Empty();
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct TokenAuth {
|
||||||
|
code: String /* Could be refresh token. */
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct TokenRequest {
|
||||||
|
client_id: String,
|
||||||
|
client_secret: String,
|
||||||
|
code: String,
|
||||||
|
grant_type: String,
|
||||||
|
redirect_uri: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Default)]
|
||||||
|
pub struct TokenResponse {
|
||||||
|
access_token: String,
|
||||||
|
token_type: String,
|
||||||
|
expires_in: i32,
|
||||||
|
refresh_token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn token(State(state): State<ServerState<'_>>,
|
||||||
|
Json(payload): Json<TokenAuth>) -> Result<Json<TokenResponse>> {
|
||||||
|
|
||||||
|
let token_endpoint = format!("{}/login/oauth/access_token", state.config.gitea_host);
|
||||||
|
let redirect_uri = "http://127.0.0.1:3000";
|
||||||
|
|
||||||
|
let data = TokenRequest {
|
||||||
|
client_id: state.config.client_id,
|
||||||
|
client_secret: state.config.client_secret,
|
||||||
|
code: payload.code,
|
||||||
|
grant_type: "authorization_code".to_owned(),
|
||||||
|
redirect_uri: redirect_uri.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(gitea_api(&token_endpoint, Method::POST, &data, "").await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RefreshTokenRequest {
|
||||||
|
client_id: String,
|
||||||
|
client_secret: String,
|
||||||
|
refresh_token: String,
|
||||||
|
grant_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_token(State(state): State<ServerState<'_>>,
|
||||||
|
Json(payload): Json<TokenAuth>) -> Result<Json<TokenResponse>>{
|
||||||
|
|
||||||
|
let token_endpoint = format!("{}/login/oauth/access_token", state.config.gitea_host);
|
||||||
|
|
||||||
|
let data = RefreshTokenRequest {
|
||||||
|
client_id: state.config.client_id,
|
||||||
|
client_secret: state.config.client_secret,
|
||||||
|
refresh_token: payload.code,
|
||||||
|
grant_type: "refresh_token".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(gitea_api(&token_endpoint, Method::POST, &data, "").await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RepoResponse {
|
||||||
|
description: String,
|
||||||
|
exists: bool,
|
||||||
|
features: FeatureList,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_token(headers: HeaderMap) -> Result<String> {
|
||||||
|
Ok(headers.get(AUTHORIZATION)
|
||||||
|
.ok_or(APIError::Tokenless)?
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| APIError::Tokenless)?
|
||||||
|
.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_repo(State(state): State<ServerState<'_>>,
|
||||||
|
Path((owner, repo)): Path<(String, String)>,
|
||||||
|
headers: HeaderMap) -> Result<Json<RepoResponse>> {
|
||||||
|
let repo = format!("{owner}/{repo}");
|
||||||
|
let token = extract_token(headers)?;
|
||||||
|
// Pull repository information from Gitea.
|
||||||
|
let json = authorize(&state.config.gitea_host, &token, &repo).await?;
|
||||||
|
|
||||||
|
let description = json.get("description")
|
||||||
|
.ok_or(APIError::InvalidJson{ msg: "Couldn't find 'description' key.".to_owned() })?
|
||||||
|
.as_str()
|
||||||
|
.ok_or(APIError::InvalidJson{ msg: "Value in 'description' is not String.".to_owned() })?
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
// Check if entry exists and return features.
|
||||||
|
let mut features = state.sql.prepare(query::GET_REPO)?
|
||||||
|
.iter()
|
||||||
|
.bind((1, repo.as_str()))?
|
||||||
|
.map(|row| -> Result<FeatureList> {
|
||||||
|
Ok(as_feature_list(row?.read::<i64, _>("features") as u64))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<FeatureList>>>()?;
|
||||||
|
|
||||||
|
if features.len() == 1 {
|
||||||
|
Ok(Json(RepoResponse { description, exists: true, features: features.remove(0) }))
|
||||||
|
} else {
|
||||||
|
Ok(Json(RepoResponse { description, exists: false, features: vec!() }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct GiteaSetSecret {
|
||||||
|
data: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_repo(State(state): State<ServerState<'_>>,
|
||||||
|
Path((owner, repo)): Path<(String, String)>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Result<()> {
|
||||||
|
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||||
|
let token = extract_token(headers)?;
|
||||||
|
let _ = authorize(&state.config.gitea_host, &token, &repo).await?;
|
||||||
|
|
||||||
|
// Create secret and insert new repository into database.
|
||||||
|
let secret = Alphanumeric.sample_string(&mut rand::rng(), 48);
|
||||||
|
let _: i32 = state.sql.prepare(crate::query::CREATE_REPO)?
|
||||||
|
.iter()
|
||||||
|
.bind_iter::<_, (_, sqlite::Value)>([
|
||||||
|
(1, repo.clone().into()),
|
||||||
|
(2, 0.into()),
|
||||||
|
(3, secret.clone().into()),
|
||||||
|
])?
|
||||||
|
.map(|_| 0)
|
||||||
|
.sum(); // Evalute statement.
|
||||||
|
|
||||||
|
// Make associated folder.
|
||||||
|
fs::create_dir_all(path::Path::new(&state.config.upload_path).join(&repo))?;
|
||||||
|
|
||||||
|
// Add secret to Gitea secrets.
|
||||||
|
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, &repo);
|
||||||
|
let data = GiteaSetSecret { data: secret };
|
||||||
|
let _: Empty = gitea_api(secret_url.as_str(), Method::PUT, &data, &token).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
|
||||||
|
}
|
||||||
|
pub async fn delete_repo(State(state): State<ServerState<'_>>,
|
||||||
|
Path((owner, repo)): Path<(String, String)>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Result<()> {
|
||||||
|
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||||
|
let token = extract_token(headers)?;
|
||||||
|
let _ = authorize(&state.config.gitea_host, &token, &repo).await?;
|
||||||
|
|
||||||
|
let _: i32 = state.sql.prepare(crate::query::DELETE_REPO)?
|
||||||
|
.iter()
|
||||||
|
.bind((1, repo.as_str()))?
|
||||||
|
.map(|_| 0)
|
||||||
|
.sum(); // Evalute statement.
|
||||||
|
|
||||||
|
// Remove entire directory and its parent if its empty.
|
||||||
|
let dir = path::Path::new(&state.config.upload_path).join(&repo);
|
||||||
|
let parent = dir.as_path()
|
||||||
|
.parent()
|
||||||
|
.ok_or(APIError::Other { msg: "Could not find parent in repo directory. Should be impossible...".to_owned() })?;
|
||||||
|
|
||||||
|
fs::remove_dir_all(&dir)?;
|
||||||
|
if fs::read_dir(parent)?.next().is_none() {
|
||||||
|
fs::remove_dir(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove secret from Gitea.
|
||||||
|
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, &repo);
|
||||||
|
let data = Empty{};
|
||||||
|
let _: Empty = gitea_api(secret_url.as_str(), Method::DELETE, &data, &token).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PatchRepoRequest {
|
||||||
|
secret: bool,
|
||||||
|
feature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_secret(state: &ServerState<'_>, repo: &str, token: &str) -> Result<()> {
|
||||||
|
let secret = Alphanumeric.sample_string(&mut rand::rng(), 48);
|
||||||
|
let _: i32 = state.sql.prepare(crate::query::UPDATE_REPO_SECRET)?
|
||||||
|
.iter()
|
||||||
|
.bind_iter::<_, (_, sqlite::Value)>([
|
||||||
|
(1, secret.clone().into()),
|
||||||
|
(2, repo.into()),
|
||||||
|
])?
|
||||||
|
.map(|_| 0)
|
||||||
|
.sum(); // Evalute statement.
|
||||||
|
|
||||||
|
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, repo);
|
||||||
|
let data = GiteaSetSecret { data: secret };
|
||||||
|
let _: Empty = gitea_api(secret_url.as_str(), Method::PUT, &data, token).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_feature(state: &ServerState<'_>, repo: &str, feature: &str) -> Result<()> {
|
||||||
|
let feat_num: u64 = 1 << (RepoFeature::try_from(feature)? as u64);
|
||||||
|
|
||||||
|
// Update feature in database. (feature = feature ^ feat_num) and get result.
|
||||||
|
let features = state.sql.prepare(crate::query::UPDATE_REPO_FEATURES)?
|
||||||
|
.iter()
|
||||||
|
.bind_iter::<_, (_, sqlite::Value)>([
|
||||||
|
(1, (feat_num as i64).into()),
|
||||||
|
(2, repo.into()),
|
||||||
|
])?
|
||||||
|
.map(|row| -> Result<u64> {
|
||||||
|
Ok(row?.read::<i64, _>("features") as u64)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<u64>>>()?[0]; // Evalute statement.
|
||||||
|
|
||||||
|
// Check added or removed and update folders accordingly.
|
||||||
|
let added = (features & feat_num) == feat_num;
|
||||||
|
let dir = path::Path::new(&state.config.upload_path).join(repo).join(feature);
|
||||||
|
if added {
|
||||||
|
fs::create_dir(dir)?;
|
||||||
|
} else {
|
||||||
|
fs::remove_dir_all(dir)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn patch_repo(State(state): State<ServerState<'_>>,
|
||||||
|
Path((owner, repo)): Path<(String, String)>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(payload): Json<PatchRepoRequest>) -> Result<()> {
|
||||||
|
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||||
|
let token = extract_token(headers)?;
|
||||||
|
let _ = authorize(&state.config.gitea_host, &token, &repo).await?;
|
||||||
|
|
||||||
|
if payload.secret {
|
||||||
|
return update_secret(&state, &repo, &token).await;
|
||||||
|
}
|
||||||
|
update_feature(&state, &repo, &payload.feature).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct UploadFile {
|
||||||
|
name: String,
|
||||||
|
data: Bytes,
|
||||||
|
folder: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload(State(state): State<ServerState<'_>>,
|
||||||
|
Path((owner, repo, feature)): Path<(String, String, String)>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
mut multipart: Multipart) -> Result<()> {
|
||||||
|
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
|
||||||
|
let feature = feature.to_lowercase();
|
||||||
|
let user_secret = extract_token(headers)?; // Authorization should be the secret this time.
|
||||||
|
|
||||||
|
let (secret, features) = &state.sql.prepare(crate::query::UPLOAD_QUERY)?
|
||||||
|
.iter()
|
||||||
|
.bind((1, repo.as_str()))?
|
||||||
|
.map(|row| -> Result<(String, FeatureList)> {
|
||||||
|
let row = row?;
|
||||||
|
Ok(
|
||||||
|
(row.read::<&str, _>("secret").to_owned(),
|
||||||
|
as_feature_list(row.read::<i64, _>("features") as u64))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<(String, FeatureList)>>>()?[0];
|
||||||
|
|
||||||
|
(user_secret == *secret).then_some(0)
|
||||||
|
.ok_or(APIError::Unauthorized{ repo: repo.to_owned() })?;
|
||||||
|
|
||||||
|
features.contains(&feature).then_some(0)
|
||||||
|
.ok_or(APIError::DisabledFeature{ feature: feature.clone() })?;
|
||||||
|
|
||||||
|
// Process multipart.
|
||||||
|
let mut file = UploadFile{ folder: "".to_owned(), ..Default::default() };
|
||||||
|
while let Some(field) = multipart.next_field().await? {
|
||||||
|
let name = match field.name() {
|
||||||
|
Some(x) => x,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
match name {
|
||||||
|
"name" => file.name = field.text().await?,
|
||||||
|
"file" => file.data = field.bytes().await?,
|
||||||
|
"folder" => file.folder = field.text().await?,
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir = path::Path::new(&state.config.upload_path).join(&repo).join(feature);
|
||||||
|
fs::write(dir.join(&file.name), file.data)?;
|
||||||
|
let tardir = dir.join(&file.folder);
|
||||||
|
if file.folder != *"" {
|
||||||
|
if fs::exists(&tardir)? {
|
||||||
|
fs::remove_dir_all(&tardir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_dir(&tardir)?;
|
||||||
|
let output = Command::new("tar").args(["-xf", dir.join(&file.name).to_str().unwrap(), "-C", tardir.to_str().unwrap()])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
// If there was an error, remove everything because the operation was unsucessful.
|
||||||
|
if !output.status.success() {
|
||||||
|
fs::remove_dir_all(&tardir)?;
|
||||||
|
fs::remove_file(dir.join(file.name))?;
|
||||||
|
return Err(APIError::Other{ msg: "Failed to complete untar".to_owned() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
145
src/main.rs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
net::SocketAddr,
|
||||||
|
};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::{ConnectInfo, Request, DefaultBodyLimit},
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use tower_http::{
|
||||||
|
services::{ServeDir, ServeFile},
|
||||||
|
trace::TraceLayer,
|
||||||
|
};
|
||||||
|
use tracing::Level;
|
||||||
|
use tracing_subscriber::{
|
||||||
|
fmt,
|
||||||
|
prelude::*,
|
||||||
|
EnvFilter,
|
||||||
|
filter::LevelFilter,
|
||||||
|
};
|
||||||
|
use tracing_appender::rolling;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
type SQLConn = sqlite::ConnectionThreadSafe;
|
||||||
|
|
||||||
|
// type RepoMap = util::TimedHashMap<String, Vec<String>>;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
struct ServerConfig {
|
||||||
|
host: String,
|
||||||
|
port: String,
|
||||||
|
db_path: String,
|
||||||
|
log_path: String,
|
||||||
|
upload_path: String,
|
||||||
|
gitea_host: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ServerState<'a> {
|
||||||
|
config: ServerConfig,
|
||||||
|
sql: &'a SQLConn,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod query {
|
||||||
|
pub static DB_INIT: &str = "
|
||||||
|
CREATE TABLE IF NOT EXISTS repositories (
|
||||||
|
full_name TEXT PRIMARY KEY,
|
||||||
|
features BIGINT UNSIGNED NOT NULL,
|
||||||
|
secret TEXT NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
";
|
||||||
|
pub static GET_REPO: &str = "SELECT features FROM repositories WHERE full_name = ?;";
|
||||||
|
pub static UPLOAD_QUERY: &str = "SELECT secret, features FROM repositories WHERE full_name = ?;";
|
||||||
|
pub static CREATE_REPO: &str = "INSERT INTO repositories (full_name, features, secret) VALUES(?, ?, ?);";
|
||||||
|
pub static DELETE_REPO: &str = "DELETE FROM repositories WHERE full_name = ?;";
|
||||||
|
pub static UPDATE_REPO_SECRET: &str = "UPDATE repositories SET secret = ? WHERE full_name = ?;";
|
||||||
|
pub static UPDATE_REPO_FEATURES: &str = "UPDATE repositories SET features = (features | ?1) - (features & ?1) WHERE full_name = ? RETURNING features;";
|
||||||
|
}
|
||||||
|
|
||||||
|
static CONFIG_PATH: &str = "pack.yml";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Server initialization: read config, make address, initialize and setup SQL.
|
||||||
|
let file = String::from_utf8(fs::read(CONFIG_PATH)?)
|
||||||
|
.context(format!("Unable to read YAML config '{}'", CONFIG_PATH))?;
|
||||||
|
let config: ServerConfig = serde_yaml::from_str(&file)
|
||||||
|
.context(format!("Failed to parse YAML config '{}'.", CONFIG_PATH))?;
|
||||||
|
let addr: SocketAddr = format!("{}:{}", &config.host, &config.port).parse().
|
||||||
|
context("Invalid host or port.")?;
|
||||||
|
let sql = sqlite::Connection::open_thread_safe(&config.db_path)
|
||||||
|
.context(format!("Cannot open SQLite database at '{}'.", &config.db_path))?;
|
||||||
|
sql.execute(query::DB_INIT)
|
||||||
|
.context(format!("Failed to setup SQLite database at '{}'.", &config.db_path))?;
|
||||||
|
|
||||||
|
// Make file upload folder.
|
||||||
|
fs::create_dir_all(&config.upload_path)?;
|
||||||
|
let log_file = rolling::daily(&config.log_path, "");
|
||||||
|
let upload_path = config.upload_path.clone();
|
||||||
|
|
||||||
|
let sql: &'static sqlite::ConnectionThreadSafe = Box::leak(Box::new(sql));
|
||||||
|
let server_state = ServerState { config, sql };
|
||||||
|
|
||||||
|
let env_filter = EnvFilter::try_from_default_env()
|
||||||
|
.or_else(|_| EnvFilter::try_new("info"))?;
|
||||||
|
|
||||||
|
let std_layer = fmt::layer()
|
||||||
|
.compact()
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_filter(env_filter);
|
||||||
|
|
||||||
|
let log_layer = fmt::layer()
|
||||||
|
.json()
|
||||||
|
.with_writer(log_file)
|
||||||
|
.with_filter(LevelFilter::DEBUG);
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(std_layer)
|
||||||
|
.with(log_layer)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let trace = TraceLayer::new_for_http()
|
||||||
|
.make_span_with(|request: &Request<Body>| {
|
||||||
|
let addr = match request.extensions().get::<ConnectInfo<SocketAddr>>() {
|
||||||
|
Some(x) => x.0.to_string(),
|
||||||
|
None => "unknown".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::span!(Level::DEBUG,
|
||||||
|
"request",
|
||||||
|
method = format!("{}", request.method()),
|
||||||
|
version = format!("{:?}", request.version()),
|
||||||
|
remote_ip = addr,
|
||||||
|
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Build routes
|
||||||
|
let api_routes = Router::new()
|
||||||
|
.route("/token", post(api::token).patch(api::refresh_token))
|
||||||
|
.route("/repo/{owner}/{repo}", get(api::get_repo).post(api::create_repo).patch(api::patch_repo).delete(api::delete_repo))
|
||||||
|
.route("/upload/{owner}/{repo}/{feature}", post(api::upload).layer(DefaultBodyLimit::max(usize::pow(1024, 3))))
|
||||||
|
.with_state(server_state);
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.nest("/api", api_routes)
|
||||||
|
.route_service("/", ServeFile::new("static/index.html"))
|
||||||
|
.nest_service("/assets", ServeDir::new("static"))
|
||||||
|
.fallback_service(ServeDir::new(upload_path))
|
||||||
|
.layer(trace);
|
||||||
|
|
||||||
|
// Run server.
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
|
tracing::info!("Listening on {}", listener.local_addr().unwrap());
|
||||||
|
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
65
src/util.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// use std::{
|
||||||
|
// collections::HashMap,
|
||||||
|
// hash::Hash,
|
||||||
|
// marker::Send,
|
||||||
|
// sync::mpsc::{channel, Sender, TryRecvError},
|
||||||
|
// sync::{Arc, Mutex},
|
||||||
|
// thread,
|
||||||
|
// time::{Duration, SystemTime},
|
||||||
|
// };
|
||||||
|
|
||||||
|
// pub struct TimedHashMap<K, V> {
|
||||||
|
// map: Arc<Mutex<HashMap<K, (SystemTime, V)>>>,
|
||||||
|
// duration: Arc<Mutex<Duration>>,
|
||||||
|
// tx: Sender<i32>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Insert and cleanup send message to a main thread
|
||||||
|
// // the main looping thread which reads messages to know its next action
|
||||||
|
|
||||||
|
// impl<K, V> TimedHashMap<K, V>
|
||||||
|
// where
|
||||||
|
// K: std::cmp::Eq + Hash + Send + 'static,
|
||||||
|
// V: Send + 'static,
|
||||||
|
// {
|
||||||
|
// pub fn new() -> Self {
|
||||||
|
// let (tx, rx) = channel();
|
||||||
|
// let thp = Self {
|
||||||
|
// map: Arc::new(Mutex::new(HashMap::<K, (SystemTime, V)>::new())),
|
||||||
|
// duration: Arc::new(Mutex::new(Duration::new(60, 0))),
|
||||||
|
// tx,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let map2 = Arc::clone(&thp.map);
|
||||||
|
// let dur2 = Arc::clone(&thp.duration);
|
||||||
|
// // Cleanup thread.
|
||||||
|
// thread::spawn(move || loop {
|
||||||
|
// thread::sleep(*dur2.lock().unwrap());
|
||||||
|
// map2.lock().unwrap().retain(|_, v| v.0 < SystemTime::now());
|
||||||
|
// match rx.try_recv() {
|
||||||
|
// // Thread should get killed when tx drops.
|
||||||
|
// Ok(_) | Err(TryRecvError::Disconnected) => break,
|
||||||
|
// Err(TryRecvError::Empty) => {}
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// thp
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn insert(&mut self, key: K, value: V, time: Duration) -> Option<V> {
|
||||||
|
// let map2 = Arc::clone(&self.map);
|
||||||
|
// match map2
|
||||||
|
// .lock()
|
||||||
|
// .unwrap()
|
||||||
|
// .insert(key, (SystemTime::now() + time, value))
|
||||||
|
// {
|
||||||
|
// Some(x) => Some(x.1),
|
||||||
|
// None => None,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn set_interval(&mut self, duration: Duration) {
|
||||||
|
// let dur2 = Arc::clone(&self.duration);
|
||||||
|
// *dur2.lock().unwrap() = duration
|
||||||
|
// }
|
||||||
|
// }
|
||||||
BIN
static/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
26
static/fragile.svg
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="svg1852"
|
||||||
|
width="500"
|
||||||
|
height="500"
|
||||||
|
viewBox="0 0 500 500"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1856" />
|
||||||
|
<path
|
||||||
|
d="m 237.22651,265.4401 v 148.14986 h -72.40403 v 25.09037 H 335.82315 V 413.58996 H 263.41311 V 265.30632 C 322.38109,254.83555 368.1994,176.30943 368.1994,80.939099 c 0,-6.629314 -0.23449,-13.174288 -0.66756,-19.619425 h -58.77239 l 16.45555,55.312416 -18.107,58.89411 6.79535,58.8815 -37.3216,-55.31726 20.35516,-60.66703 -26.71846,-57.103736 H 132.46816 c -0.43607,6.445137 -0.66756,12.99011 -0.66756,19.619424 0,95.723142 46.1536,174.499352 105.42591,184.500992"
|
||||||
|
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0983483"
|
||||||
|
id="path10892" />
|
||||||
|
<rect
|
||||||
|
style="display:inline;fill:none;fill-opacity:0.988235;stroke:#000000;stroke-width:25.4064;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
||||||
|
id="rect7030"
|
||||||
|
width="474.59354"
|
||||||
|
height="474.59354"
|
||||||
|
x="12.459805"
|
||||||
|
y="12.703235"
|
||||||
|
ry="0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
703
static/index.css
Normal file
@ -0,0 +1,703 @@
|
|||||||
|
:root {
|
||||||
|
--font-family: 'Quicksand', sans-serif;
|
||||||
|
|
||||||
|
/* Light Colors */
|
||||||
|
--cl-bg: #e0ffdd;
|
||||||
|
--cl-text: black;
|
||||||
|
--cl-dec-text: black;
|
||||||
|
--cl-box: #e7bb69;
|
||||||
|
--cl-box-edge: #c19d59;
|
||||||
|
--cl-box-ridge: #e1b768;
|
||||||
|
--cl-label: white;
|
||||||
|
--cl-lines: black;
|
||||||
|
--cl-login-hover: rgba(255, 255, 255, 0.8);
|
||||||
|
--cl-null-text: #9b9b9b;
|
||||||
|
|
||||||
|
/* Dark Colors */
|
||||||
|
--cd-bg: #111;
|
||||||
|
--cd-text: #fff;
|
||||||
|
--cd-dec-text: #9b9b9b;
|
||||||
|
--cd-box: #262933;
|
||||||
|
--cd-box-edge: #1a1a1a;
|
||||||
|
--cd-box-ridge: #282b35;
|
||||||
|
--cd-label: #222;
|
||||||
|
--cd-lines: #4d4d4d;
|
||||||
|
--cd-login-hover: rgba(0, 0, 0, 0.5);
|
||||||
|
--cd-null-text: #9b9b9b;
|
||||||
|
|
||||||
|
--c-loading: #d6d6d6;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
--c-bg: var(--cl-bg);
|
||||||
|
--c-text: var(--cl-text);
|
||||||
|
--c-dec-text: var(--cl-dec-text);
|
||||||
|
--c-box: var(--cl-box);
|
||||||
|
--c-box-edge: var(--cl-box-edge);
|
||||||
|
--c-box-ridge: var(--cl-box-ridge);
|
||||||
|
--c-label: var(--cl-label);
|
||||||
|
--c-lines: var(--cl-lines);
|
||||||
|
--c-login-hover: var(--cl-login-hover);
|
||||||
|
--c-null-text: var(--cl-null-text);
|
||||||
|
|
||||||
|
--p-theme-switch-0: 0;
|
||||||
|
--p-theme-switch-1: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
--c-bg: var(--cd-bg);
|
||||||
|
--c-text: var(--cd-text);
|
||||||
|
--c-dec-text: var(--cd-dec-text);
|
||||||
|
--c-box: var(--cd-box);
|
||||||
|
--c-box-edge: var(--cd-box-edge);
|
||||||
|
--c-box-ridge: var(--cd-box-ridge);
|
||||||
|
--c-label: var(--cd-label);
|
||||||
|
--c-lines: var(--cd-lines);
|
||||||
|
--c-login-hover: var(--cd-login-hover);
|
||||||
|
--c-null-text: var(--cd-null-text);
|
||||||
|
|
||||||
|
--p-theme-switch-0: 1;
|
||||||
|
--p-theme-switch-1: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:has(#themeSwitch:checked) {
|
||||||
|
/* Swapped light and dark themes. */
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
--c-bg: var(--cl-bg);
|
||||||
|
--c-text: var(--cl-text);
|
||||||
|
--c-dec-text: var(--cl-dec-text);
|
||||||
|
--c-box: var(--cl-box);
|
||||||
|
--c-box-edge: var(--cl-box-edge);
|
||||||
|
--c-box-ridge: var(--cl-box-ridge);
|
||||||
|
--c-label: var(--cl-label);
|
||||||
|
--c-lines: var(--cl-lines);
|
||||||
|
--c-login-hover: var(--cl-login-hover);
|
||||||
|
--c-null-text: var(--cl-null-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
--c-bg: var(--cd-bg);
|
||||||
|
--c-text: var(--cd-text);
|
||||||
|
--c-dec-text: var(--cd-dec-text);
|
||||||
|
--c-box: var(--cd-box);
|
||||||
|
--c-box-edge: var(--cd-box-edge);
|
||||||
|
--c-box-ridge: var(--cd-box-ridge);
|
||||||
|
--c-label: var(--cd-label);
|
||||||
|
--c-lines: var(--cd-lines);
|
||||||
|
--c-login-hover: var(--cd-login-hover);
|
||||||
|
--c-null-text: var(--cd-null-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
color: var(--c-text);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@media (min-aspect-ratio: 100/135) { /* Wider */
|
||||||
|
font-size: min(calc(100/135 * 1vh), 62.5%);
|
||||||
|
/* Centered around the main box: packageLabel with net height/width 100/135 */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-aspect-ratio: 100/135) { /* longer */
|
||||||
|
font-size: min(calc(100/100 * 1vw), 62.5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100vw; height: 100%;
|
||||||
|
margin: 0 auto 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background-color: var(--c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#themeSwitchContainer {
|
||||||
|
width: 10rem; height: 10rem;
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0;
|
||||||
|
z-index: 5;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
--sun-color: #fffb15;
|
||||||
|
--moon-color: #ffe598;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
--day-color: #a3e0ee;
|
||||||
|
--night-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
--night-color: #a3e0ee;
|
||||||
|
--day-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
--w: 6deg;
|
||||||
|
--spokes: 10;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(2) {
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
top: 0; left: 0;
|
||||||
|
background-color: var(--day-color);
|
||||||
|
filter: brightness(100%);
|
||||||
|
transition: background-color 0.15s ease-in-out,
|
||||||
|
filter 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(3) {
|
||||||
|
width: 4rem; height: 4rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 3rem; left: 3rem;
|
||||||
|
background-color: var(--sun-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: var(--p-theme-switch-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(3)::before {
|
||||||
|
content: "";
|
||||||
|
width: 7rem; height: 7rem;
|
||||||
|
position: absolute;
|
||||||
|
top: -1.5rem; left: -1.5rem;
|
||||||
|
border-radius: inherit;
|
||||||
|
background-image: repeating-conic-gradient(
|
||||||
|
from calc(-1*var(--w)/2), var(--sun-color) 0 calc(var(--w) - 2deg),
|
||||||
|
transparent calc(var(--w)) calc(360deg/var(--spokes) - 2deg),
|
||||||
|
var(--sun-color) calc(360deg/var(--spokes))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
span:nth-child(4) {
|
||||||
|
width: 6rem; height: 6rem;
|
||||||
|
top: 1.75rem; left: 1.75rem;
|
||||||
|
background-color: var(--moon-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: var(--p-theme-switch-0);
|
||||||
|
mask-image: radial-gradient(circle, rgba(0,0,0,0) 33%, black 34%);
|
||||||
|
mask-position: -3rem -3rem;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 166.66% 166.66%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:hover ~ span:nth-child(2) {
|
||||||
|
filter: brightness(110%);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked ~ span:nth-child(2) {
|
||||||
|
background-color: var(--night-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked ~ span:nth-child(3) {
|
||||||
|
opacity: var(--p-theme-switch-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked ~ span:nth-child(4) {
|
||||||
|
opacity: var(--p-theme-switch-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#package {
|
||||||
|
margin: auto;
|
||||||
|
width: 298rem;
|
||||||
|
height: 200rem;
|
||||||
|
--spacing: 0.8rem;
|
||||||
|
background: repeating-linear-gradient(90deg,
|
||||||
|
var(--c-box) 0rem,
|
||||||
|
var(--c-box) var(--spacing),
|
||||||
|
var(--c-box-ridge) var(--spacing),
|
||||||
|
var(--c-box-ridge) calc(2 *var(--spacing))
|
||||||
|
);
|
||||||
|
|
||||||
|
border: 1rem solid var(--c-box-edge);
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
:nth-child(1) {
|
||||||
|
width: 30rem;
|
||||||
|
top: 6rem; left: 263rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:nth-child(2) {
|
||||||
|
width: 30rem;
|
||||||
|
top: 6rem; left: 228rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:nth-child(3) {
|
||||||
|
width: 35rem;
|
||||||
|
top: 162rem; left: 257rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:nth-child(4) {
|
||||||
|
width: 25rem;
|
||||||
|
top: 165rem; left: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#packageTape {
|
||||||
|
margin: auto;
|
||||||
|
height: 30rem;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font: 1.5px var(--font-family);
|
||||||
|
fill: rgba(255, 255, 255, 0.4);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#packageLabel {
|
||||||
|
width: 80rem;
|
||||||
|
height: 115rem;
|
||||||
|
|
||||||
|
outline-width: 5rem;
|
||||||
|
outline-style: solid;
|
||||||
|
outline-color: var(--c-label);
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--c-label);
|
||||||
|
border: 0.5rem solid var(--c-lines);
|
||||||
|
|
||||||
|
transition: background-color 0.15s ease-in-out,
|
||||||
|
border 0.15s ease-in-out,
|
||||||
|
outline-color 0.15s ease-in-out;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 2fr 1fr 7fr 2fr;
|
||||||
|
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--c-dec-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#packageLabelTop {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 3fr;
|
||||||
|
border-bottom: 0.5rem solid var(--c-lines);
|
||||||
|
|
||||||
|
> div:nth-child(1) {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-right: 0.5rem solid var(--c-lines);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 80%; height: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:nth-child(2) {
|
||||||
|
padding: 3%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
|
||||||
|
> div:nth-child(1) {
|
||||||
|
display: block;
|
||||||
|
color: var(--c-dec-text);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:nth-child(1) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:nth-child(2) {
|
||||||
|
display: inline-block;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:nth-child(2) {
|
||||||
|
width: 80%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0;
|
||||||
|
width: 35rem; height: 7rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
width: 24.8rem; height: 7rem;
|
||||||
|
line-height: 7rem;
|
||||||
|
position: relative;
|
||||||
|
left: 5rem;
|
||||||
|
|
||||||
|
font-size: 6rem;
|
||||||
|
font-family: 'Courier';
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
transition: background 0.15s ease-in-out,
|
||||||
|
color 0.15s ease-in-out;
|
||||||
|
|
||||||
|
z-index: 4;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#packageLabelTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 0.5rem solid var(--c-lines);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 1%;
|
||||||
|
font-size: 430%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cursorWait {
|
||||||
|
0% { content: "█"; }
|
||||||
|
50% { content: ""; }
|
||||||
|
100% { content: "█"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
to { --angle: 360deg; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --angle {
|
||||||
|
syntax: "<angle>";
|
||||||
|
initial-value: 0deg;
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#packageLabelContent {
|
||||||
|
border-bottom: 0.5rem solid var(--c-lines);
|
||||||
|
font-size: 150%;
|
||||||
|
font-family: 'Courier';
|
||||||
|
|
||||||
|
> h1 {
|
||||||
|
padding: 7%;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 100%;
|
||||||
|
color: var(--c-lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
> h1::after {
|
||||||
|
animation: 1.5s infinite normal cursorWait;
|
||||||
|
content: "hello";
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:nth-child(2) {
|
||||||
|
padding: 7% 7% 0 7%;
|
||||||
|
color: var(--c-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background: none;
|
||||||
|
color: var(--c-text);
|
||||||
|
border: 1px solid var(--c-lines);
|
||||||
|
padding: 2%;
|
||||||
|
margin: 2% 0 2% 0;
|
||||||
|
font-size: 150%;
|
||||||
|
font-family: 'Courier';
|
||||||
|
}
|
||||||
|
|
||||||
|
input.error {
|
||||||
|
outline: 2px solid red !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit] {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button enabled/disabled. */
|
||||||
|
input[type=submit] {
|
||||||
|
color: var(--c-null-text);
|
||||||
|
border-style: dashed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit].enabled {
|
||||||
|
color: var(--c-text);
|
||||||
|
border-style: solid;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit]:hover {
|
||||||
|
background-color: var(--c-login-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit].loading {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: linear-gradient(var(--c-label), var(--c-label)) padding-box, conic-gradient(
|
||||||
|
from var(--angle),
|
||||||
|
var(--c-lines) 0% 30%,
|
||||||
|
var(--c-loading) 30% 50%,
|
||||||
|
var(--c-lines) 50% 80%,
|
||||||
|
var(--c-loading) 80% 100%
|
||||||
|
) border-box;
|
||||||
|
animation: 1.5s rotate linear infinite;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repoSelect {
|
||||||
|
border-bottom: 2px solid var(--c-lines);
|
||||||
|
|
||||||
|
#repoSelectInput {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repoSelectButton {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repoSelectError {
|
||||||
|
line-height: 100%;
|
||||||
|
height: 3rem;
|
||||||
|
margin: 2% 0 2% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#repoInfo {
|
||||||
|
margin-top: 2%;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
|
||||||
|
> h1:first-child {
|
||||||
|
margin-top: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repoActions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repoLinkInput::selection {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repoLinkButton {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repoFeatures {
|
||||||
|
margin-top: 2%;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
|
||||||
|
#featureBox {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
margin-right: 2%;
|
||||||
|
font-weight: 800;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
/* .feature { */
|
||||||
|
/* border: 1px solid var(--c-lines); */
|
||||||
|
/* padding: 2%; */
|
||||||
|
/* margin-right: 2%; */
|
||||||
|
/* font-size: 150%; */
|
||||||
|
/* cursor: pointer; */
|
||||||
|
/* user-select: none; */
|
||||||
|
/* transition: background-color 0.15s ease-in-out; */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
/* .feature:hover { */
|
||||||
|
/* background-color: var(--c-login-hover); */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
/* Feature states. */
|
||||||
|
.feature {
|
||||||
|
color: var(--c-null-text);
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature.fEnabled {
|
||||||
|
color: var(--c-text);
|
||||||
|
border-style: solid;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmOverlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin: 30% auto 0 auto;
|
||||||
|
|
||||||
|
width: 80rem;
|
||||||
|
height: 30rem;
|
||||||
|
|
||||||
|
outline-width: 5rem;
|
||||||
|
outline-style: solid;
|
||||||
|
outline-color: var(--c-label);
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--c-label);
|
||||||
|
border: 0.5rem solid var(--c-lines);
|
||||||
|
|
||||||
|
transition: background-color 0.15s ease-in-out,
|
||||||
|
border 0.15s ease-in-out,
|
||||||
|
outline-color 0.15s ease-in-out;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding: 7%;
|
||||||
|
|
||||||
|
input[type=submit] {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmOverlay {
|
||||||
|
pointer-events: none;
|
||||||
|
top: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmOverlay.enabled {
|
||||||
|
pointer-events: auto;
|
||||||
|
top: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Repo selected and created states. */
|
||||||
|
#repoInfo, #repoFeatures {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repoInfo.repoSelected, #repoFeatures.repoCreated {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#packageLabelBot {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 6fr 1fr;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 3% auto 0 auto;
|
||||||
|
width: 80%; height: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin: auto;
|
||||||
|
margin-bottom: 1%;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--c-dec-text);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: filter 0.15s ease-in-out;
|
||||||
|
filter: brightness(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
filter: brightness(120%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body { /* Default no authentication */
|
||||||
|
#packageLabelTitle h1::before {
|
||||||
|
content: "PRIORITY PACKAGING"
|
||||||
|
}
|
||||||
|
|
||||||
|
#packageLabelContent > div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
background: var(--c-label);
|
||||||
|
color: var(--c-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#login::before {
|
||||||
|
content: "LOGIN";
|
||||||
|
}
|
||||||
|
|
||||||
|
#login:hover {
|
||||||
|
background-color: var(--c-login-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.auth {
|
||||||
|
#packageLabelTitle h1::before {
|
||||||
|
content: "PACKAGE INFO"
|
||||||
|
}
|
||||||
|
|
||||||
|
#packageLabelContent > div {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#packageLabelContent > h1 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
color: transparent;
|
||||||
|
background:transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login::before {
|
||||||
|
content: "LOGOUT";
|
||||||
|
}
|
||||||
|
|
||||||
|
#login:hover {
|
||||||
|
background: var(--c-label);
|
||||||
|
color: var(--c-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
191
static/index.html
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="chrome=1">
|
||||||
|
<title>Pack</title>
|
||||||
|
<link rel="icon" href="assets/favicon.ico?v=2">
|
||||||
|
<link rel="stylesheet" href="assets/index.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Quicksand:300,400" rel="stylesheet">
|
||||||
|
<!-- <link href="https://fonts.googleapis.com/css?family=Roboto+Slab:100,300" rel="stylesheet"> -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
|
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> -->
|
||||||
|
<!-- <script src="https://use.fontawesome.com/c8d5486cd8.js"></script> -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="themeSwitchContainer">
|
||||||
|
<input type="checkbox" id="themeSwitch">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div id="package">
|
||||||
|
<img src="assets/fragile.svg">
|
||||||
|
<img src="assets/keep_dry.svg">
|
||||||
|
<img src="assets/recycle.svg">
|
||||||
|
<img src="assets/qr_code.svg">
|
||||||
|
</div>
|
||||||
|
<svg id="packageTape" viewbox="0 0 100 10">
|
||||||
|
<rect width="100" height="0.3" x="0" y="4.85" style="fill:#483320;fill-opacity:0.8" />
|
||||||
|
<rect width="100" height="10" x="0" y="0" style="fill:#111;fill-opacity:0.8" />
|
||||||
|
|
||||||
|
<polygon points="21,0.0 20,0.625 21,1.25 20,1.875 21,2.5 20,3.125 21,3.75 20,4.375 21,5.0 20,5.625 21,6.25 20,6.875 21,7.5 20,8.125 21,8.75 20,9.375 21,10.0 100,10 100,0" style="fill:#111;fill-opacity:0.5"/>
|
||||||
|
<text><textPath href="#packTape0">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape1">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape2">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape3">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape4">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape5">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape6">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape7">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape8">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape9">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape10">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape11">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape12">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape13">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape14">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape15">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape16">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape17">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape18">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape19">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape20">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape21">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape22">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape23">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape24">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape25">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape26">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape27">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape28">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape29">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape30">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape31">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape32">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape33">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape34">PACK.PACK.PACK.PACK.</textPath></text>
|
||||||
|
<path id="packTape0" fill="none" stroke="none" d="M-3.00 -0.00 l6 20"><path id="packTape1" fill="none" stroke="none" d="M-0.60 -2.00 l6 20"><path id="packTape2" fill="none" stroke="none" d="M1.80 -4.00 l6 20"><path id="packTape3" fill="none" stroke="none" d="M6.00 -0.00 l6 20"><path id="packTape4" fill="none" stroke="none" d="M8.40 -2.00 l6 20"><path id="packTape5" fill="none" stroke="none" d="M10.80 -4.00 l6 20"><path id="packTape6" fill="none" stroke="none" d="M15.00 -0.00 l6 20"><path id="packTape7" fill="none" stroke="none" d="M17.40 -2.00 l6 20"><path id="packTape8" fill="none" stroke="none" d="M19.80 -4.00 l6 20"><path id="packTape9" fill="none" stroke="none" d="M24.00 -0.00 l6 20"><path id="packTape10" fill="none" stroke="none" d="M26.40 -2.00 l6 20"><path id="packTape11" fill="none" stroke="none" d="M28.80 -4.00 l6 20"><path id="packTape12" fill="none" stroke="none" d="M33.00 -0.00 l6 20"><path id="packTape13" fill="none" stroke="none" d="M35.40 -2.00 l6 20"><path id="packTape14" fill="none" stroke="none" d="M37.80 -4.00 l6 20"><path id="packTape15" fill="none" stroke="none" d="M42.00 -0.00 l6 20"><path id="packTape16" fill="none" stroke="none" d="M44.40 -2.00 l6 20"><path id="packTape17" fill="none" stroke="none" d="M46.80 -4.00 l6 20"><path id="packTape18" fill="none" stroke="none" d="M51.00 -0.00 l6 20"><path id="packTape19" fill="none" stroke="none" d="M53.40 -2.00 l6 20"><path id="packTape20" fill="none" stroke="none" d="M55.80 -4.00 l6 20"><path id="packTape21" fill="none" stroke="none" d="M60.00 -0.00 l6 20"><path id="packTape22" fill="none" stroke="none" d="M62.40 -2.00 l6 20"><path id="packTape23" fill="none" stroke="none" d="M64.80 -4.00 l6 20"><path id="packTape24" fill="none" stroke="none" d="M69.00 -0.00 l6 20"><path id="packTape25" fill="none" stroke="none" d="M71.40 -2.00 l6 20"><path id="packTape26" fill="none" stroke="none" d="M73.80 -4.00 l6 20"><path id="packTape27" fill="none" stroke="none" d="M78.00 -0.00 l6 20"><path id="packTape28" fill="none" stroke="none" d="M80.40 -2.00 l6 20"><path id="packTape29" fill="none" stroke="none" d="M82.80 -4.00 l6 20"><path id="packTape30" fill="none" stroke="none" d="M87.00 -0.00 l6 20"><path id="packTape31" fill="none" stroke="none" d="M89.40 -2.00 l6 20"><path id="packTape32" fill="none" stroke="none" d="M91.80 -4.00 l6 20"><path id="packTape33" fill="none" stroke="none" d="M96.00 -0.00 l6 20"><path id="packTape34" fill="none" stroke="none" d="M98.40 -2.00 l6 20">
|
||||||
|
</svg>
|
||||||
|
<div id="packageLabel">
|
||||||
|
<div id="packageLabelTop">
|
||||||
|
<div>
|
||||||
|
<svg width="48" height="48" viewBox="0 0 12.7 12.7" version="1.1">
|
||||||
|
<defs id="defs2" />
|
||||||
|
<path d="m 12.323869,4.1992062 -6.2301843,2.5959093 -1e-7,0.095169 6.3500004,-2.6458336 z" />
|
||||||
|
<path d="M 12.443685,4.2444508 V 4.3735617 L 9.2686845,4.925839 V 4.7967281 Z" />
|
||||||
|
<path d="M 6.0936845,1.6003079 12.443177,4.2450847 9.2686845,4.796728 2.9191918,2.1519511 Z" />
|
||||||
|
<path d="m 9.2686845,3.8880996 0.062387,0.1201924 -3.237387,3.0585598 2e-7,-0.17699 z" />
|
||||||
|
<path d="M 9.2686845,4.7967281 V 4.925839 L 8.6378103,4.6632589 8.7123025,4.564976 Z" />
|
||||||
|
<rect width="6.8791666" height="5.8977885" x="-13.480659" y="9.4293194" transform="matrix(-0.92307692,0.3846154,0,1,0,0)" />
|
||||||
|
<rect width="6.8791666" height="5.8977885" x="-0.27767482" y="4.3512492" transform="matrix(0.92307692,0.3846154,0,1,0,0)" />
|
||||||
|
<path d="M 2.9186843,1.2431113 9.2686845,3.8880996 6.0936849,6.8898618 -0.2563155,4.2448735 Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<p><b>BYTE TRANSIT FEES PAID</b></p>
|
||||||
|
<p>64 OF 64</p>
|
||||||
|
<p>6.28 TB FIBRE OPTIC EXPRESS RATE</p>
|
||||||
|
<p>ACCEPTS TAR.GZ, ZIP</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<svg width="618" height="124" viewBox="0 0 618 124" version="1.1">
|
||||||
|
<path d="M 2.1999999e-6,0 H 41.200002 V 123.6 H 2.1999999e-6 Z M 46.350002,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 20.6,0 H 113.3 v 15.45 h 5.15 V 30.9 h -5.15 v 15.45 h -5.15 V 30.9 H 103 V 46.35 H 97.850002 V 30.9 h -5.15 v 15.45 h 5.15 V 61.8 H 103 v 15.450002 h 5.15 V 61.8 h 5.15 v 61.8 H 87.550002 Z M 118.45,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 25.75 v 15.45 h 5.15 V 30.9 H 154.5 V 15.45 h -15.45 z m 36.05,0 h 15.45 v 30.9 h -5.15 v 30.9 h 5.15 v 15.450002 h -5.15 v 15.45 h -5.15 V 108.15 h 10.3 V 123.6 H 175.1 Z m 20.6,0 h 5.15 V 15.45 H 195.7 Z M 206,0 h 10.3 v 15.45 h -5.15 V 30.9 h 5.15 V 15.45 h 10.3 V 30.9 h -5.15 v 15.45 h 5.15 V 61.8 h 5.15 v 15.450002 h -5.15 v 15.45 h 5.15 V 108.15 H 226.6 V 123.6 H 216.3 V 92.700002 H 206 V 123.6 h -5.15 V 108.15 H 195.7 V 92.700002 h 5.15 v -15.45 H 206 V 61.8 h 5.15 v 15.450002 h 10.3 V 61.8 H 216.3 V 46.35 H 206 V 61.8 H 190.55 V 46.35 h 5.15 V 30.9 H 206 Z m 20.6,0 h 20.6 v 46.35 h -5.15 V 15.45 H 226.6 Z m 36.05,0 h 10.3 v 30.9 h -5.15 v 15.45 h 5.15 v 30.900002 h -5.15 v 15.45 h 10.3 V 108.15 h 5.15 v 15.45 h -10.3 v -15.45 h -5.15 v 15.45 h -5.15 z m 30.9,0 h 10.3 V 30.9 H 309 V 0 h 5.15 v 30.9 h 5.15 v 15.45 h -5.15 V 61.8 h 5.15 v 15.450002 h 5.15 v 15.45 h -10.3 v -15.45 h -10.3 v 15.45 H 298.7 V 108.15 H 288.4 V 77.250002 h 10.3 V 61.8 h 5.15 V 46.35 H 298.7 V 30.9 h -10.3 v 30.9 h -5.15 V 15.45 h 10.3 z m 25.75,0 h 5.15 v 15.45 h 20.6 V 30.9 H 319.3 Z m 30.9,0 h 5.15 v 15.45 h 5.15 V 0 h 15.45 v 15.45 h -10.3 V 30.9 h -10.3 v 15.45 h 5.15 v 46.350002 h 5.15 V 108.15 h 5.15 v 15.45 h -20.6 z m 46.35,0 H 412 v 15.45 h 5.15 v 30.9 H 412 V 61.8 h 5.15 v 15.450002 h 5.15 V 61.8 h 10.3 v 15.450002 h -5.15 v 15.45 h -20.6 v -15.45 h -10.3 V 61.8 h 10.3 V 46.35 h -10.3 V 30.9 h 5.15 V 15.45 h -5.15 z m 20.6,0 h 5.15 v 15.45 h -5.15 z m 20.6,0 h 25.75 v 15.45 h -5.15 V 30.9 h -5.15 v 15.45 h -5.15 V 61.8 h 5.15 V 46.35 h 5.15 V 77.250002 H 453.2 V 123.6 h -5.15 v -15.45 h -5.15 v 15.45 h -5.15 z m 30.9,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 H 515 V 15.45 H 494.4 V 30.9 h 25.75 V 61.8 H 515 v 15.450002 h -10.3 v 15.45 h 15.45 V 108.15 h -10.3 V 123.6 H 494.4 V 92.700002 h 5.15 V 61.8 H 494.4 V 46.35 h -5.15 z m 36.05,0 h 36.05 V 123.6 H 525.3 Z m 41.2,0 h 5.15 v 123.6 h -5.15 z m 20.6,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 15.45,0 H 618 v 123.6 h -5.15 z M 123.6,15.45 h 5.15 V 30.9 h 5.15 V 15.45 h 5.15 V 30.9 h 5.15 v 15.45 h -10.3 v 30.900002 h 15.45 v 15.45 h 5.15 V 61.8 H 144.2 V 46.35 h 25.75 V 61.8 h -5.15 v 30.900002 h 5.15 V 108.15 H 154.5 V 123.6 H 144.2 V 92.700002 h -5.15 V 123.6 H 133.9 V 108.15 H 123.6 V 92.700002 h 5.15 v -15.45 h -5.15 v 15.45 h -5.15 V 61.8 H 113.3 V 46.35 h 10.3 z m 257.5,0 h 5.15 V 30.9 h -5.15 z m 41.2,0 h 10.3 V 30.9 h -10.3 z m 41.2,0 h 5.15 V 30.9 h -5.15 z m 10.3,0 h 5.15 V 30.9 H 473.8 Z M 231.75,30.9 h 5.15 v 15.45 h -5.15 z m 41.2,0 h 5.15 v 15.45 h -5.15 z m 97.85,0 h 5.15 v 15.45 h 5.15 v 30.900002 h -5.15 V 61.8 h -5.15 z m 15.45,0 h 5.15 v 15.45 h -5.15 z m 72.1,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h 10.3 V 61.8 h 5.15 v 15.450002 h 5.15 v 15.45 h -5.15 V 123.6 h -30.9 V 77.250002 h 5.15 V 108.15 h 10.3 V 92.700002 h -5.15 v -15.45 h 5.15 V 61.8 h -5.15 V 77.250002 H 463.5 V 46.35 h 5.15 z M 103,46.35 h 5.15 V 61.8 H 103 Z m 149.35,0 h 5.15 v 77.25 h -5.15 V 108.15 H 247.2 V 92.700002 h 5.15 z m 77.25,0 h 15.45 V 61.8 h -10.3 v 15.450002 h 10.3 v 15.45 H 339.9 V 108.15 H 329.6 V 123.6 H 303.85 V 92.700002 h 10.3 V 108.15 h 10.3 V 92.700002 h 5.15 v -15.45 h -5.15 V 61.8 h 5.15 z m 61.8,0 h 5.15 V 61.8 H 391.4 Z M 278.1,61.8 h 5.15 v 15.450002 h -5.15 z m -41.2,15.450002 h 5.15 v 15.45 h -5.15 z m 128.75,0 h 10.3 v 15.45 h 5.15 v -15.45 h 5.15 v 15.45 h 5.15 V 108.15 h -5.15 v 15.45 h -10.3 V 108.15 H 370.8 V 92.700002 h -5.15 z M 103,92.700002 V 108.15 h 5.15 V 92.700002 Z m 293.55,0 h 10.3 V 108.15 h -10.3 z m 30.9,0 h 5.15 V 123.6 h -5.15 z M 118.45,108.15 h 5.15 v 15.45 h -5.15 z m 221.45,0 h 5.15 v 15.45 h -5.15 z m 66.95,0 h 10.3 v 15.45 h -10.3 z" fill-rule="evenodd" style="stroke-width:0.858333" />
|
||||||
|
</svg>
|
||||||
|
<h1 id="login"></h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="packageLabelTitle">
|
||||||
|
<h1><b></b></h1>
|
||||||
|
</div>
|
||||||
|
<div id="packageLabelContent">
|
||||||
|
<h1>AWAITING AUTHORIZATION...</h1>
|
||||||
|
<div>
|
||||||
|
<div id="repoSelect">
|
||||||
|
<h1>REPOSITORY:</h1>
|
||||||
|
<input id="repoSelectInput" type="text" size="32" placeholder="<user>/<repository>">
|
||||||
|
<input class="enabled" id="repoSelectButton" type="submit" value="Select">
|
||||||
|
<p id="repoSelectError"></p>
|
||||||
|
</div>
|
||||||
|
<div id="repoInfo">
|
||||||
|
<h1>ACTIONS:</h1>
|
||||||
|
<div id="repoActions">
|
||||||
|
<input class="enabled" id="repoCreate" type="submit" value="Create">
|
||||||
|
<input id="repoDelete" type="submit" value="Delete">
|
||||||
|
<input id="repoSecret" type="submit" value="Regenerate Secret">
|
||||||
|
</div>
|
||||||
|
<div id="repoFeatures">
|
||||||
|
<h1>LINK:</h1>
|
||||||
|
<input id="repoLinkInput" type="text" size="32" readonly>
|
||||||
|
<input class="enabled" id="repoLinkButton" type="submit" value="Copy">
|
||||||
|
<h1>FEATURES:</h1>
|
||||||
|
<div id="featureBox">
|
||||||
|
<input class="feature" type="submit" data-tag="docs" value="Docs">
|
||||||
|
<input class="feature" type="submit" data-tag="builds" value="Builds">
|
||||||
|
<input class="feature" type="submit" data-tag="nightly" value="Nightly">
|
||||||
|
<input class="feature" type="submit" data-tag="preprod" value="PreProd">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="confirmOverlay">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1>CONFIRMATION:</h1>
|
||||||
|
<input id="confirmInput" type="text" size="32" placeholder="<user>/<repository>">
|
||||||
|
<input class="enabled" id="confirmButtonYes" type="submit" value="Confirm">
|
||||||
|
<input class="enabled" id="confirmButtonNo" type="submit" value="Cancel">
|
||||||
|
<p>This will (irreversibly) delete all associated files! Please type in the full repository name to confirm.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="packageLabelBot">
|
||||||
|
<svg width="1596" height="432" viewBox="0 0 1596 432" version="1.1">
|
||||||
|
<path d="M 0,432 H 12 V 0 H 0 Z" />
|
||||||
|
<path d="m 18,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 36,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 66,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="M 84,432 H 96 V 0 H 84 Z" />
|
||||||
|
<path d="m 120,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 132,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 150,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 180,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 198,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 216,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 246,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 264,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 276,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 294,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 330,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 342,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 378,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 396,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 426,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 444,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 462,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 474,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 504,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 528,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 540,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 570,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 594,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 606,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 624,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 660,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 678,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 690,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 726,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 756,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 768,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 792,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 828,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 846,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 858,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 876,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 900,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 924,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 960,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 978,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 990,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1020,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1044,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1056,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1074,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1086,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1122,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1146,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 1176,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1188,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1206,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1230,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 1254,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 1284,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 1308,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1320,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1332,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1356,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1386,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1398,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 1428,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1452,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1470,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1488,432 h 24 V 0 h -24 z" />
|
||||||
|
<path d="m 1518,432 h 12 V 0 h -12 z" />
|
||||||
|
<path d="m 1548,432 h 18 V 0 h -18 z" />
|
||||||
|
<path d="m 1572,432 h 6 V 0 h -6 z" />
|
||||||
|
<path d="m 1584,432 h 12 V 0 h -12 z" />
|
||||||
|
</svg>
|
||||||
|
<a href="/">https://pack.kjao.me/</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
<script src="assets/index.js"></script>
|
||||||
|
</html>
|
||||||
368
static/index.js
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
const CONFIG = {
|
||||||
|
client_id: "3d463405-d098-4038-8b25-a44ca5b9ab9c",
|
||||||
|
redirect_uri: "http://127.0.0.1:3000/",
|
||||||
|
authorization_endpoint: "https://git.kjao.me/login/oauth/authorize",
|
||||||
|
token_endpoint: "https://git.kjao.me/login/oauth/access_token",
|
||||||
|
requested_scopes: "read:repository,write:repository"
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATES = { // Either the non-d
|
||||||
|
auth: "auth",
|
||||||
|
error: "error",
|
||||||
|
loading: "loading",
|
||||||
|
repoSelected: "repoSelected",
|
||||||
|
repoCreated: "repoCreated",
|
||||||
|
buttonOn: "enabled",
|
||||||
|
featureOn: "fEnabled",
|
||||||
|
confirmOn: "enabled",
|
||||||
|
}
|
||||||
|
|
||||||
|
var G_REPO_VALUE = "",
|
||||||
|
G_CONFIRM_VALUE = "",
|
||||||
|
G_CONFIRM = -1;
|
||||||
|
|
||||||
|
// UTILITY //
|
||||||
|
|
||||||
|
document.on = document.addEventListener;
|
||||||
|
function sugar(obj) {
|
||||||
|
obj.on = obj.addEventListener;
|
||||||
|
obj.addClass = (x) => obj.classList.add(x);
|
||||||
|
obj.delClass = (x) => obj.classList.remove(x);
|
||||||
|
obj.hasClass = (x) => obj.classList.contains(x);
|
||||||
|
obj.setClass = (x, y) => obj.classList.toggle(x, y);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(s) {
|
||||||
|
let x = [...document.querySelectorAll(s)].map(sugar);
|
||||||
|
return (x.length === 1) ? x[0] : x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if object is empty.
|
||||||
|
function isEmpty(obj) {
|
||||||
|
for (const prop in obj) {
|
||||||
|
if (Object.hasOwn(obj, prop)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns error and resolves promise if necessary.
|
||||||
|
function getErr(f) {
|
||||||
|
return (err) => {
|
||||||
|
if (err instanceof Promise) {
|
||||||
|
return err.then(f);
|
||||||
|
} else {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a query string into an object
|
||||||
|
function parseQueryString(string) {
|
||||||
|
if(string == "") { return {}; }
|
||||||
|
var segments = string.split("&").map(s => s.split("=") );
|
||||||
|
var queryString = {};
|
||||||
|
segments.forEach(s => G_QUERYString[s[0]] = s[1]);
|
||||||
|
return queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch function for applet use case.
|
||||||
|
async function jfetch(url, method, obj={}) {
|
||||||
|
let request = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": window.localStorage.getItem("token"),
|
||||||
|
},
|
||||||
|
method: method,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isEmpty(obj)) request["body"] = JSON.stringify(obj);
|
||||||
|
|
||||||
|
return fetch(url, request)
|
||||||
|
.then((response) => {
|
||||||
|
let contentType = response.headers.get("Content-Type");
|
||||||
|
let json;
|
||||||
|
if (contentType && contentType.indexOf("application/json" !== -1)) {
|
||||||
|
json = response.json();
|
||||||
|
} else { // Reprocess non-json into json.
|
||||||
|
json = response.text().then((text) => {
|
||||||
|
return { status: response.status, "message": text };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!response.ok) { throw json; }
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAUTH //
|
||||||
|
|
||||||
|
async function getToken() {
|
||||||
|
let response;
|
||||||
|
if (G_QUERY.code) {
|
||||||
|
response = await jfetch("/api/token", "POST", { code: G_QUERY.code });
|
||||||
|
} else {
|
||||||
|
response = await jfetch("/api/token", "PATCH", { code: window.localStorage.getItem("refresh_token") });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem("token", response.access_token);
|
||||||
|
window.localStorage.setItem("token_expiry", (Date.now() + response.expires_in*1000).toString());
|
||||||
|
window.localStorage.setItem("refresh_token", response.refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryRefresh() {
|
||||||
|
if (Date.now() < parseInt(window.localStorage.getItem("token_expiry"))) return; // Not expired.
|
||||||
|
await getToken().catch((err) => {
|
||||||
|
let json = err.json();
|
||||||
|
if (json.status == 401 && json.code === 0) { // Could not refresh, so refresh token expired.
|
||||||
|
window.localStorage.clear();
|
||||||
|
alert(`Could not get new session, did you remove access from Gitea?`);
|
||||||
|
window.location = "/";
|
||||||
|
} else {
|
||||||
|
throw json;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUTTON ONCLICK MIDDLEWARE //
|
||||||
|
|
||||||
|
// Refresh token if necessary.
|
||||||
|
function tryAuth(f) {
|
||||||
|
return async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!G_AUTH) return;
|
||||||
|
await tryRefresh();
|
||||||
|
await f(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Adds loading classes for UI while awaiting.
|
||||||
|
function doLoading(f) {
|
||||||
|
return async (e) => {
|
||||||
|
if (e.target.hasClass("loading")) return;
|
||||||
|
e.target.addClass(STATES["loading"]);
|
||||||
|
await f(e);
|
||||||
|
e.target.delClass(STATES["loading"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUTTON ONCLICK HELPERS //
|
||||||
|
|
||||||
|
function setRepoError(s, err) {
|
||||||
|
const input = get("#repoSelectInput"),
|
||||||
|
error = get("#repoSelectError"),
|
||||||
|
info = get("#repoInfo");
|
||||||
|
error.innerText = s;
|
||||||
|
info.setClass(STATES["repoSelected"], !err);
|
||||||
|
input.setClass(STATES["error"], err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRepoActions(s) {
|
||||||
|
get("#repoCreate").setClass(STATES["buttonOn"], !s);
|
||||||
|
get("#repoDelete").setClass(STATES["buttonOn"], s);
|
||||||
|
get("#repoSecret").setClass(STATES["buttonOn"], s);
|
||||||
|
get("#repoFeatures").setClass(STATES["repoCreated"], s);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getConfirm() {
|
||||||
|
get("#confirmOverlay").addClass(STATES["confirmOn"]);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const check = function() {
|
||||||
|
if (G_CONFIRM == -1) {
|
||||||
|
setTimeout(check, 100);
|
||||||
|
} else {
|
||||||
|
get("#confirmOverlay").delClass(STATES["confirmOn"]);
|
||||||
|
resolve(G_CONFIRM);
|
||||||
|
G_CONFIRM = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.on("keydown", (e) => {
|
||||||
|
if (e.keyCode == 27) get("#confirmOverlay").delClass(STATES["confirmOn"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// BUTTON ONCLICK FUNCTIONS //
|
||||||
|
|
||||||
|
get("#login").on("click", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (G_AUTH) {
|
||||||
|
window.localStorage.clear();
|
||||||
|
window.location = "/";
|
||||||
|
} else {
|
||||||
|
// Build the authorization URL
|
||||||
|
let url = CONFIG.authorization_endpoint
|
||||||
|
+ "?response_type=code"
|
||||||
|
+ "&client_id="+encodeURIComponent(CONFIG.client_id)
|
||||||
|
+ "&scope="+encodeURIComponent(CONFIG.requested_scopes)
|
||||||
|
+ "&redirect_uri="+encodeURIComponent(CONFIG.redirect_uri);
|
||||||
|
|
||||||
|
// Redirect to the authorization server
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
get("#repoSelectInput").on("keydown", (e) => {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
get("#repoSelectButton").click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = e.target.value;
|
||||||
|
if (value !== G_REPO_VALUE) {
|
||||||
|
get("#repoSelectError").innerText = ""; // Delete error text.
|
||||||
|
get("#repoSelectInput").delClass(STATES["error"]); // Remove error class.
|
||||||
|
get("#repoInfo").delClass(STATES["repoSelected"]); // Remove repoSelect class.
|
||||||
|
setRepoActions(false); // Set actions to default.
|
||||||
|
get(".feature").forEach((feature) => feature.delClass(STATES["featureOn"]));
|
||||||
|
G_REPO_VALUE = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
get("#repoSelectButton").on("click", tryAuth(doLoading(selectRepo)));
|
||||||
|
async function selectRepo(e) {
|
||||||
|
const input = get("#repoSelectInput");
|
||||||
|
let repo = input.value.trim();
|
||||||
|
input.value = repo;
|
||||||
|
|
||||||
|
if (repo === "") {
|
||||||
|
setRepoError("Field cannot be empty.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await jfetch("/api/repo/" + repo, "GET")
|
||||||
|
.then((json) => {
|
||||||
|
setRepoError("OK", false);
|
||||||
|
setRepoActions(json["exists"]);
|
||||||
|
|
||||||
|
if (json["exists"]) {
|
||||||
|
json["features"].forEach((feature) => {
|
||||||
|
get(`.feature[data-tag=${feature}]`).addClass(STATES["featureOn"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get("#repoLinkInput").value = CONFIG["redirect_uri"] + repo;
|
||||||
|
get("#repoInfo").addClass(STATES["repoSelected"]);
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(getErr((err) => {
|
||||||
|
console.log(err);
|
||||||
|
setRepoError("Unable to find repository or you have insufficient permissions.", true);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
get("#repoCreate").on("click", tryAuth(doLoading(createRepo)));
|
||||||
|
async function createRepo(e) {
|
||||||
|
const input = get("#repoSelectInput");
|
||||||
|
await jfetch("/api/repo/" + input.value, "POST")
|
||||||
|
.then((json) => {
|
||||||
|
setRepoActions(true);
|
||||||
|
})
|
||||||
|
.catch(getErr((err) => {
|
||||||
|
console.log(err);
|
||||||
|
alert("An unexpected error occurred! Check console for more details.");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
get("#repoDelete").on("click", tryAuth(doLoading(deleteRepo)));
|
||||||
|
async function deleteRepo(e) {
|
||||||
|
const input = get("#repoSelectInput");
|
||||||
|
if (!(await getConfirm())) return;
|
||||||
|
await jfetch("/api/repo/" + input.value, "DELETE")
|
||||||
|
.then((json) => {
|
||||||
|
setRepoActions(false);
|
||||||
|
})
|
||||||
|
.catch(getErr((err) => {
|
||||||
|
console.log(err);
|
||||||
|
alert("An unexpected error occurred! Check console for more details.");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
get("#repoSecret").on("click", tryAuth(doLoading(regenSecret)));
|
||||||
|
async function regenSecret(e) {
|
||||||
|
const input = get("#repoSelectInput");
|
||||||
|
await jfetch("/api/repo/" + input.value, "PATCH", { secret: true, feature: "" })
|
||||||
|
.then((json) => {
|
||||||
|
setRepoActions(true);
|
||||||
|
})
|
||||||
|
.catch(getErr((err) => {
|
||||||
|
console.log(err);
|
||||||
|
alert("An unexpected error occurred! Check console for more details.");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
get("#repoLinkButton").on("click", async (e) => {
|
||||||
|
const input = get("#repoLinkInput");
|
||||||
|
input.select();
|
||||||
|
input.setSelectionRange(0, 99999);
|
||||||
|
navigator.clipboard.writeText(input.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
get(".feature").forEach((el) => el.on("click", tryAuth(doLoading(toggleFeature))));
|
||||||
|
async function toggleFeature(e) {
|
||||||
|
const input = get("#repoSelectInput");
|
||||||
|
let attr = e.target.getAttribute("data-tag");
|
||||||
|
if (e.target.hasClass(STATES["featureOn"]) && !(await getConfirm())) return;
|
||||||
|
await jfetch("/api/repo/" + input.value, "PATCH", { secret: false, feature: attr })
|
||||||
|
.then((json) => {
|
||||||
|
e.target.setClass(STATES["featureOn"]);
|
||||||
|
})
|
||||||
|
.catch(getErr((err) => {
|
||||||
|
console.log(err);
|
||||||
|
alert("An unexpected error occurred! Check console for more details.");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
get("#confirmInput").on("keydown", function(e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
get("#confirmButtonYes").click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 27) {
|
||||||
|
get("#confirmButtonNo").click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = e.target.value;
|
||||||
|
if (value !== G_CONFIRM_VALUE) {
|
||||||
|
get("#confirmInput").delClass(STATES["error"]);
|
||||||
|
G_REPO_VALUE = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
get("#confirmButtonNo").on("click", (e) => {
|
||||||
|
G_CONFIRM = false;
|
||||||
|
get("#confirmInput").value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
get("#confirmButtonYes").on("click", (e) => {
|
||||||
|
let confirmValue = get("#confirmInput").value.trim();
|
||||||
|
let repoValue = get("#repoSelectInput").value;
|
||||||
|
|
||||||
|
if (confirmValue === repoValue) {
|
||||||
|
G_CONFIRM = true;
|
||||||
|
get("#confirmInput").value = "";
|
||||||
|
} else {
|
||||||
|
get("#confirmInput").addClass(STATES["error"]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
// Get query and delete URL parameters.
|
||||||
|
if(G_QUERY.error) alert("Error returned from authorization server: "+q.error); // Check for auth errors.
|
||||||
|
if(G_QUERY.code) {
|
||||||
|
await getToken();
|
||||||
|
G_AUTH = true;
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
}
|
||||||
|
get("body").setClass(STATES["auth"], G_AUTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
var G_QUERY = parseQueryString(window.location.search.substring(1));
|
||||||
|
var G_AUTH = window.localStorage.getItem("token") !== null;
|
||||||
|
init();
|
||||||
64
static/keep_dry.svg
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="svg1852"
|
||||||
|
width="500"
|
||||||
|
height="500"
|
||||||
|
viewBox="0 0 500 500"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1856" />
|
||||||
|
<g
|
||||||
|
id="g1862"
|
||||||
|
transform="matrix(0.09388818,0,0,-0.09388818,21.082497,529.0621)"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235">
|
||||||
|
<path
|
||||||
|
d="m 1999.88,1286.63 c -0.91,-5.74 -1.47,-13.6 -1.28,-23.03 0.41,-36.42 9,-88.28 33.43,-133.6 20.15,-37.26 52.81,-69.26 103.81,-77.77 11.59,-1.91 23.7,-2.9 36.18,-2.73 85.06,1.11 135.06,32.01 163.01,77.39 33.38,54.2 43.55,131.92 43.67,210.19 l 2.6,1507.16 c -126.9,-21.04 -233.24,-103.14 -287.97,-215.21 -63.82,130.62 -197.67,220.48 -352.46,220.48 -149.3,0 -279.07,-83.62 -345.41,-206.77 -66.27,123.15 -196.08,206.77 -345.37,206.77 -157.59,0 -293.481,-93.15 -355.844,-227.67 C 861.051,3314.35 1549.72,3838.21 2383,3863.05 l 0.27,148.88 h 187.77 l -0.27,-151.71 c 783.7,-47.65 1428.25,-537.3 1611.37,-1186.35 -70.31,105.89 -190.37,175.64 -326.75,175.64 -155.04,0 -289.08,-90.24 -352.74,-221.17 -63.72,130.93 -197.78,221.17 -352.83,221.17 -154.44,0 -288.02,-89.58 -352.07,-219.69 -46.12,93.84 -128.46,166.58 -228.77,199.85 l -2.55,-1492.59 c -0.12,-108.37 -16.81,-220.06 -71.65,-309.22 -60.27,-97.981 -159.91,-164.618 -320.61,-166.712 -25.01,-0.316 -48.08,1.403 -69.23,4.934 -119.15,19.93 -193.7,91.43 -238.54,174.538 -40.59,75.2 -54.83,160.79 -55.62,220.78 -0.16,12.23 0.39,25.75 1,33.95 l 188.1,-8.72"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path2290" />
|
||||||
|
<path
|
||||||
|
d="m 3074.81,4774.53 c -4.53,-29.67 -16.02,-59.39 -36.62,-79.82 -20.86,-20.57 -49.46,-33.3 -80.93,-33.3 -31.86,0 -60.74,12.97 -81.6,33.87 h -0.03 c -21.81,23.81 -33.79,49.02 -33.79,81.85 0,18.13 4.21,35.41 11.7,50.77 7.58,15.5 22.35,32.15 30.37,40.51 l 208.22,211.25 c 4.81,4.99 12.79,5.05 17.77,0.21 3.2,-3.14 4.38,-7.55 3.53,-11.64 l -38.62,-293.7"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path2292" />
|
||||||
|
<path
|
||||||
|
d="m 2778.9,4254.29 c -4.55,-29.69 -16.02,-59.41 -36.61,-79.82 -20.83,-20.58 -49.45,-33.33 -80.96,-33.33 -31.81,0 -60.72,12.95 -81.55,33.89 h -0.05 c -21.79,23.8 -33.82,48.98 -33.82,81.85 0,18.14 4.26,35.36 11.72,50.76 7.61,15.49 22.37,32.15 30.32,40.52 l 208.28,211.26 c 4.85,4.94 12.8,5.04 17.77,0.19 3.15,-3.13 4.36,-7.55 3.55,-11.64 l -38.65,-293.68"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path2294" />
|
||||||
|
<path
|
||||||
|
d="m 1987.59,4044.19 c -4.5,-29.7 -16.02,-59.44 -36.6,-79.83 -20.83,-20.58 -49.44,-33.32 -80.98,-33.32 -31.77,0 -60.62,13.01 -81.53,33.87 v 0 c -21.79,23.82 -33.81,49 -33.81,81.87 0,18.11 4.18,35.37 11.73,50.77 7.52,15.49 22.33,32.16 30.29,40.49 l 208.29,211.28 c 4.83,4.99 12.79,5.05 17.73,0.18 3.18,-3.12 4.34,-7.55 3.54,-11.63 l -38.66,-293.68"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path2296" />
|
||||||
|
<path
|
||||||
|
d="m 3267.59,3987.48 c -4.46,-29.67 -15.95,-59.41 -36.58,-79.82 -20.82,-20.58 -49.46,-33.28 -80.94,-33.28 -31.82,0 -60.68,12.95 -81.56,33.89 h -0.03 c -21.75,23.77 -33.83,48.96 -33.83,81.82 0,18.14 4.22,35.4 11.77,50.77 7.53,15.5 22.28,32.14 30.28,40.5 l 208.29,211.24 c 4.81,4.99 12.81,5.11 17.72,0.25 3.23,-3.16 4.38,-7.6 3.49,-11.66 l -38.61,-293.71"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path2298" />
|
||||||
|
<path
|
||||||
|
d="m 3596.74,4484.43 c -4.5,-29.7 -15.99,-59.43 -36.62,-79.82 -20.84,-20.63 -49.44,-33.33 -80.97,-33.33 -31.74,0 -60.61,12.98 -81.49,33.9 h -0.04 c -21.79,23.74 -33.82,48.99 -33.82,81.82 0,18.14 4.2,35.39 11.73,50.77 7.51,15.5 22.28,32.16 30.32,40.52 l 208.28,211.25 c 4.83,4.98 12.8,5.04 17.75,0.18 3.18,-3.13 4.34,-7.54 3.46,-11.62 l -38.6,-293.67"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path2300" />
|
||||||
|
<path
|
||||||
|
d="m 4009.05,3960.81 c -4.5,-29.68 -15.99,-59.39 -36.64,-79.8 -20.81,-20.58 -49.41,-33.33 -80.93,-33.33 -31.85,0 -60.73,12.97 -81.56,33.89 h -0.06 c -21.78,23.8 -33.82,49 -33.82,81.85 0,18.13 4.24,35.36 11.76,50.8 7.57,15.49 22.33,32.11 30.32,40.45 l 208.26,211.29 c 4.84,4.96 12.81,5.06 17.8,0.18 3.11,-3.1 4.31,-7.52 3.44,-11.66 l -38.57,-293.67"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path2302" />
|
||||||
|
<path
|
||||||
|
d="m 2396.48,4761.2 c -4.44,-29.65 -15.95,-59.39 -36.58,-79.81 -20.79,-20.59 -49.46,-33.32 -80.97,-33.32 -31.75,0 -60.59,12.94 -81.51,33.88 h -0.02 c -21.79,23.81 -33.83,49.01 -33.83,81.85 0,18.1 4.18,35.35 11.73,50.77 7.52,15.51 22.33,32.12 30.3,40.49 l 208.28,211.26 c 4.85,4.97 12.8,5.07 17.82,0.21 3.12,-3.12 4.33,-7.54 3.45,-11.66 l -38.67,-293.67"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path2304" />
|
||||||
|
<path
|
||||||
|
d="m 1598.56,4601.13 c -4.46,-29.68 -15.96,-59.42 -36.6,-79.81 -20.77,-20.59 -49.43,-33.35 -80.92,-33.35 -31.81,0 -60.62,13 -81.55,33.92 h -0.07 c -21.72,23.77 -33.73,48.99 -33.73,81.83 0,18.13 4.14,35.39 11.71,50.76 7.5,15.52 22.34,32.17 30.26,40.52 L 1616,4906.26 c 4.83,4.98 12.81,5.08 17.73,0.19 3.22,-3.14 4.32,-7.54 3.45,-11.63 l -38.62,-293.69"
|
||||||
|
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path2306" />
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
style="fill:none;fill-opacity:0.988235;stroke:#000000;stroke-width:25.4064;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
|
||||||
|
id="rect7030"
|
||||||
|
width="474.5936"
|
||||||
|
height="474.5936"
|
||||||
|
x="12.7032"
|
||||||
|
y="-487.29681"
|
||||||
|
transform="scale(1,-1)"
|
||||||
|
ry="0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.7 KiB |
341
static/qr_code.svg
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg width="264" height="264" viewBox="0 0 264 264"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events">
|
||||||
|
<rect x="0" y="0" width="264" height="264" fill="#ffffff"/>
|
||||||
|
<defs>
|
||||||
|
<rect id="p" width="8" height="8"/>
|
||||||
|
</defs>
|
||||||
|
<g fill="#000000">
|
||||||
|
<use xlink:href="#p" x="32" y="32"/>
|
||||||
|
<use xlink:href="#p" x="32" y="40"/>
|
||||||
|
<use xlink:href="#p" x="32" y="48"/>
|
||||||
|
<use xlink:href="#p" x="32" y="56"/>
|
||||||
|
<use xlink:href="#p" x="32" y="64"/>
|
||||||
|
<use xlink:href="#p" x="32" y="72"/>
|
||||||
|
<use xlink:href="#p" x="32" y="80"/>
|
||||||
|
<use xlink:href="#p" x="32" y="96"/>
|
||||||
|
<use xlink:href="#p" x="32" y="112"/>
|
||||||
|
<use xlink:href="#p" x="32" y="128"/>
|
||||||
|
<use xlink:href="#p" x="32" y="176"/>
|
||||||
|
<use xlink:href="#p" x="32" y="184"/>
|
||||||
|
<use xlink:href="#p" x="32" y="192"/>
|
||||||
|
<use xlink:href="#p" x="32" y="200"/>
|
||||||
|
<use xlink:href="#p" x="32" y="208"/>
|
||||||
|
<use xlink:href="#p" x="32" y="216"/>
|
||||||
|
<use xlink:href="#p" x="32" y="224"/>
|
||||||
|
<use xlink:href="#p" x="40" y="32"/>
|
||||||
|
<use xlink:href="#p" x="40" y="80"/>
|
||||||
|
<use xlink:href="#p" x="40" y="104"/>
|
||||||
|
<use xlink:href="#p" x="40" y="112"/>
|
||||||
|
<use xlink:href="#p" x="40" y="120"/>
|
||||||
|
<use xlink:href="#p" x="40" y="128"/>
|
||||||
|
<use xlink:href="#p" x="40" y="136"/>
|
||||||
|
<use xlink:href="#p" x="40" y="144"/>
|
||||||
|
<use xlink:href="#p" x="40" y="152"/>
|
||||||
|
<use xlink:href="#p" x="40" y="160"/>
|
||||||
|
<use xlink:href="#p" x="40" y="176"/>
|
||||||
|
<use xlink:href="#p" x="40" y="224"/>
|
||||||
|
<use xlink:href="#p" x="48" y="32"/>
|
||||||
|
<use xlink:href="#p" x="48" y="48"/>
|
||||||
|
<use xlink:href="#p" x="48" y="56"/>
|
||||||
|
<use xlink:href="#p" x="48" y="64"/>
|
||||||
|
<use xlink:href="#p" x="48" y="80"/>
|
||||||
|
<use xlink:href="#p" x="48" y="96"/>
|
||||||
|
<use xlink:href="#p" x="48" y="104"/>
|
||||||
|
<use xlink:href="#p" x="48" y="176"/>
|
||||||
|
<use xlink:href="#p" x="48" y="192"/>
|
||||||
|
<use xlink:href="#p" x="48" y="200"/>
|
||||||
|
<use xlink:href="#p" x="48" y="208"/>
|
||||||
|
<use xlink:href="#p" x="48" y="224"/>
|
||||||
|
<use xlink:href="#p" x="56" y="32"/>
|
||||||
|
<use xlink:href="#p" x="56" y="48"/>
|
||||||
|
<use xlink:href="#p" x="56" y="56"/>
|
||||||
|
<use xlink:href="#p" x="56" y="64"/>
|
||||||
|
<use xlink:href="#p" x="56" y="80"/>
|
||||||
|
<use xlink:href="#p" x="56" y="96"/>
|
||||||
|
<use xlink:href="#p" x="56" y="104"/>
|
||||||
|
<use xlink:href="#p" x="56" y="112"/>
|
||||||
|
<use xlink:href="#p" x="56" y="128"/>
|
||||||
|
<use xlink:href="#p" x="56" y="160"/>
|
||||||
|
<use xlink:href="#p" x="56" y="176"/>
|
||||||
|
<use xlink:href="#p" x="56" y="192"/>
|
||||||
|
<use xlink:href="#p" x="56" y="200"/>
|
||||||
|
<use xlink:href="#p" x="56" y="208"/>
|
||||||
|
<use xlink:href="#p" x="56" y="224"/>
|
||||||
|
<use xlink:href="#p" x="64" y="32"/>
|
||||||
|
<use xlink:href="#p" x="64" y="48"/>
|
||||||
|
<use xlink:href="#p" x="64" y="56"/>
|
||||||
|
<use xlink:href="#p" x="64" y="64"/>
|
||||||
|
<use xlink:href="#p" x="64" y="80"/>
|
||||||
|
<use xlink:href="#p" x="64" y="96"/>
|
||||||
|
<use xlink:href="#p" x="64" y="104"/>
|
||||||
|
<use xlink:href="#p" x="64" y="128"/>
|
||||||
|
<use xlink:href="#p" x="64" y="136"/>
|
||||||
|
<use xlink:href="#p" x="64" y="144"/>
|
||||||
|
<use xlink:href="#p" x="64" y="160"/>
|
||||||
|
<use xlink:href="#p" x="64" y="176"/>
|
||||||
|
<use xlink:href="#p" x="64" y="192"/>
|
||||||
|
<use xlink:href="#p" x="64" y="200"/>
|
||||||
|
<use xlink:href="#p" x="64" y="208"/>
|
||||||
|
<use xlink:href="#p" x="64" y="224"/>
|
||||||
|
<use xlink:href="#p" x="72" y="32"/>
|
||||||
|
<use xlink:href="#p" x="72" y="80"/>
|
||||||
|
<use xlink:href="#p" x="72" y="96"/>
|
||||||
|
<use xlink:href="#p" x="72" y="104"/>
|
||||||
|
<use xlink:href="#p" x="72" y="136"/>
|
||||||
|
<use xlink:href="#p" x="72" y="144"/>
|
||||||
|
<use xlink:href="#p" x="72" y="160"/>
|
||||||
|
<use xlink:href="#p" x="72" y="176"/>
|
||||||
|
<use xlink:href="#p" x="72" y="224"/>
|
||||||
|
<use xlink:href="#p" x="80" y="32"/>
|
||||||
|
<use xlink:href="#p" x="80" y="40"/>
|
||||||
|
<use xlink:href="#p" x="80" y="48"/>
|
||||||
|
<use xlink:href="#p" x="80" y="56"/>
|
||||||
|
<use xlink:href="#p" x="80" y="64"/>
|
||||||
|
<use xlink:href="#p" x="80" y="72"/>
|
||||||
|
<use xlink:href="#p" x="80" y="80"/>
|
||||||
|
<use xlink:href="#p" x="80" y="96"/>
|
||||||
|
<use xlink:href="#p" x="80" y="112"/>
|
||||||
|
<use xlink:href="#p" x="80" y="128"/>
|
||||||
|
<use xlink:href="#p" x="80" y="144"/>
|
||||||
|
<use xlink:href="#p" x="80" y="160"/>
|
||||||
|
<use xlink:href="#p" x="80" y="176"/>
|
||||||
|
<use xlink:href="#p" x="80" y="184"/>
|
||||||
|
<use xlink:href="#p" x="80" y="192"/>
|
||||||
|
<use xlink:href="#p" x="80" y="200"/>
|
||||||
|
<use xlink:href="#p" x="80" y="208"/>
|
||||||
|
<use xlink:href="#p" x="80" y="216"/>
|
||||||
|
<use xlink:href="#p" x="80" y="224"/>
|
||||||
|
<use xlink:href="#p" x="88" y="104"/>
|
||||||
|
<use xlink:href="#p" x="88" y="112"/>
|
||||||
|
<use xlink:href="#p" x="88" y="144"/>
|
||||||
|
<use xlink:href="#p" x="96" y="48"/>
|
||||||
|
<use xlink:href="#p" x="96" y="56"/>
|
||||||
|
<use xlink:href="#p" x="96" y="64"/>
|
||||||
|
<use xlink:href="#p" x="96" y="72"/>
|
||||||
|
<use xlink:href="#p" x="96" y="80"/>
|
||||||
|
<use xlink:href="#p" x="96" y="88"/>
|
||||||
|
<use xlink:href="#p" x="96" y="112"/>
|
||||||
|
<use xlink:href="#p" x="96" y="120"/>
|
||||||
|
<use xlink:href="#p" x="96" y="136"/>
|
||||||
|
<use xlink:href="#p" x="96" y="160"/>
|
||||||
|
<use xlink:href="#p" x="96" y="168"/>
|
||||||
|
<use xlink:href="#p" x="96" y="184"/>
|
||||||
|
<use xlink:href="#p" x="96" y="192"/>
|
||||||
|
<use xlink:href="#p" x="96" y="200"/>
|
||||||
|
<use xlink:href="#p" x="96" y="208"/>
|
||||||
|
<use xlink:href="#p" x="96" y="224"/>
|
||||||
|
<use xlink:href="#p" x="104" y="32"/>
|
||||||
|
<use xlink:href="#p" x="104" y="48"/>
|
||||||
|
<use xlink:href="#p" x="104" y="56"/>
|
||||||
|
<use xlink:href="#p" x="104" y="64"/>
|
||||||
|
<use xlink:href="#p" x="104" y="72"/>
|
||||||
|
<use xlink:href="#p" x="104" y="88"/>
|
||||||
|
<use xlink:href="#p" x="104" y="96"/>
|
||||||
|
<use xlink:href="#p" x="104" y="104"/>
|
||||||
|
<use xlink:href="#p" x="104" y="120"/>
|
||||||
|
<use xlink:href="#p" x="104" y="136"/>
|
||||||
|
<use xlink:href="#p" x="104" y="144"/>
|
||||||
|
<use xlink:href="#p" x="104" y="152"/>
|
||||||
|
<use xlink:href="#p" x="104" y="168"/>
|
||||||
|
<use xlink:href="#p" x="104" y="200"/>
|
||||||
|
<use xlink:href="#p" x="104" y="224"/>
|
||||||
|
<use xlink:href="#p" x="112" y="40"/>
|
||||||
|
<use xlink:href="#p" x="112" y="48"/>
|
||||||
|
<use xlink:href="#p" x="112" y="56"/>
|
||||||
|
<use xlink:href="#p" x="112" y="64"/>
|
||||||
|
<use xlink:href="#p" x="112" y="72"/>
|
||||||
|
<use xlink:href="#p" x="112" y="80"/>
|
||||||
|
<use xlink:href="#p" x="112" y="120"/>
|
||||||
|
<use xlink:href="#p" x="112" y="136"/>
|
||||||
|
<use xlink:href="#p" x="112" y="144"/>
|
||||||
|
<use xlink:href="#p" x="112" y="160"/>
|
||||||
|
<use xlink:href="#p" x="112" y="176"/>
|
||||||
|
<use xlink:href="#p" x="112" y="200"/>
|
||||||
|
<use xlink:href="#p" x="112" y="216"/>
|
||||||
|
<use xlink:href="#p" x="112" y="224"/>
|
||||||
|
<use xlink:href="#p" x="120" y="32"/>
|
||||||
|
<use xlink:href="#p" x="120" y="40"/>
|
||||||
|
<use xlink:href="#p" x="120" y="48"/>
|
||||||
|
<use xlink:href="#p" x="120" y="64"/>
|
||||||
|
<use xlink:href="#p" x="120" y="104"/>
|
||||||
|
<use xlink:href="#p" x="120" y="112"/>
|
||||||
|
<use xlink:href="#p" x="120" y="128"/>
|
||||||
|
<use xlink:href="#p" x="120" y="176"/>
|
||||||
|
<use xlink:href="#p" x="120" y="184"/>
|
||||||
|
<use xlink:href="#p" x="120" y="192"/>
|
||||||
|
<use xlink:href="#p" x="128" y="40"/>
|
||||||
|
<use xlink:href="#p" x="128" y="56"/>
|
||||||
|
<use xlink:href="#p" x="128" y="80"/>
|
||||||
|
<use xlink:href="#p" x="128" y="96"/>
|
||||||
|
<use xlink:href="#p" x="128" y="136"/>
|
||||||
|
<use xlink:href="#p" x="128" y="152"/>
|
||||||
|
<use xlink:href="#p" x="128" y="160"/>
|
||||||
|
<use xlink:href="#p" x="128" y="176"/>
|
||||||
|
<use xlink:href="#p" x="128" y="200"/>
|
||||||
|
<use xlink:href="#p" x="128" y="216"/>
|
||||||
|
<use xlink:href="#p" x="128" y="224"/>
|
||||||
|
<use xlink:href="#p" x="136" y="32"/>
|
||||||
|
<use xlink:href="#p" x="136" y="40"/>
|
||||||
|
<use xlink:href="#p" x="136" y="56"/>
|
||||||
|
<use xlink:href="#p" x="136" y="64"/>
|
||||||
|
<use xlink:href="#p" x="136" y="96"/>
|
||||||
|
<use xlink:href="#p" x="136" y="136"/>
|
||||||
|
<use xlink:href="#p" x="136" y="144"/>
|
||||||
|
<use xlink:href="#p" x="136" y="152"/>
|
||||||
|
<use xlink:href="#p" x="136" y="200"/>
|
||||||
|
<use xlink:href="#p" x="136" y="208"/>
|
||||||
|
<use xlink:href="#p" x="136" y="216"/>
|
||||||
|
<use xlink:href="#p" x="144" y="40"/>
|
||||||
|
<use xlink:href="#p" x="144" y="56"/>
|
||||||
|
<use xlink:href="#p" x="144" y="72"/>
|
||||||
|
<use xlink:href="#p" x="144" y="80"/>
|
||||||
|
<use xlink:href="#p" x="144" y="88"/>
|
||||||
|
<use xlink:href="#p" x="144" y="96"/>
|
||||||
|
<use xlink:href="#p" x="144" y="112"/>
|
||||||
|
<use xlink:href="#p" x="144" y="120"/>
|
||||||
|
<use xlink:href="#p" x="144" y="144"/>
|
||||||
|
<use xlink:href="#p" x="144" y="160"/>
|
||||||
|
<use xlink:href="#p" x="144" y="184"/>
|
||||||
|
<use xlink:href="#p" x="144" y="200"/>
|
||||||
|
<use xlink:href="#p" x="144" y="208"/>
|
||||||
|
<use xlink:href="#p" x="144" y="224"/>
|
||||||
|
<use xlink:href="#p" x="152" y="40"/>
|
||||||
|
<use xlink:href="#p" x="152" y="72"/>
|
||||||
|
<use xlink:href="#p" x="152" y="88"/>
|
||||||
|
<use xlink:href="#p" x="152" y="96"/>
|
||||||
|
<use xlink:href="#p" x="152" y="112"/>
|
||||||
|
<use xlink:href="#p" x="152" y="144"/>
|
||||||
|
<use xlink:href="#p" x="152" y="168"/>
|
||||||
|
<use xlink:href="#p" x="152" y="192"/>
|
||||||
|
<use xlink:href="#p" x="152" y="200"/>
|
||||||
|
<use xlink:href="#p" x="152" y="216"/>
|
||||||
|
<use xlink:href="#p" x="152" y="224"/>
|
||||||
|
<use xlink:href="#p" x="160" y="40"/>
|
||||||
|
<use xlink:href="#p" x="160" y="48"/>
|
||||||
|
<use xlink:href="#p" x="160" y="64"/>
|
||||||
|
<use xlink:href="#p" x="160" y="72"/>
|
||||||
|
<use xlink:href="#p" x="160" y="80"/>
|
||||||
|
<use xlink:href="#p" x="160" y="104"/>
|
||||||
|
<use xlink:href="#p" x="160" y="136"/>
|
||||||
|
<use xlink:href="#p" x="160" y="144"/>
|
||||||
|
<use xlink:href="#p" x="160" y="160"/>
|
||||||
|
<use xlink:href="#p" x="160" y="168"/>
|
||||||
|
<use xlink:href="#p" x="160" y="176"/>
|
||||||
|
<use xlink:href="#p" x="160" y="184"/>
|
||||||
|
<use xlink:href="#p" x="160" y="192"/>
|
||||||
|
<use xlink:href="#p" x="160" y="200"/>
|
||||||
|
<use xlink:href="#p" x="160" y="208"/>
|
||||||
|
<use xlink:href="#p" x="160" y="216"/>
|
||||||
|
<use xlink:href="#p" x="168" y="120"/>
|
||||||
|
<use xlink:href="#p" x="168" y="128"/>
|
||||||
|
<use xlink:href="#p" x="168" y="152"/>
|
||||||
|
<use xlink:href="#p" x="168" y="160"/>
|
||||||
|
<use xlink:href="#p" x="168" y="192"/>
|
||||||
|
<use xlink:href="#p" x="168" y="200"/>
|
||||||
|
<use xlink:href="#p" x="168" y="224"/>
|
||||||
|
<use xlink:href="#p" x="176" y="32"/>
|
||||||
|
<use xlink:href="#p" x="176" y="40"/>
|
||||||
|
<use xlink:href="#p" x="176" y="48"/>
|
||||||
|
<use xlink:href="#p" x="176" y="56"/>
|
||||||
|
<use xlink:href="#p" x="176" y="64"/>
|
||||||
|
<use xlink:href="#p" x="176" y="72"/>
|
||||||
|
<use xlink:href="#p" x="176" y="80"/>
|
||||||
|
<use xlink:href="#p" x="176" y="96"/>
|
||||||
|
<use xlink:href="#p" x="176" y="112"/>
|
||||||
|
<use xlink:href="#p" x="176" y="120"/>
|
||||||
|
<use xlink:href="#p" x="176" y="128"/>
|
||||||
|
<use xlink:href="#p" x="176" y="144"/>
|
||||||
|
<use xlink:href="#p" x="176" y="160"/>
|
||||||
|
<use xlink:href="#p" x="176" y="176"/>
|
||||||
|
<use xlink:href="#p" x="176" y="192"/>
|
||||||
|
<use xlink:href="#p" x="176" y="200"/>
|
||||||
|
<use xlink:href="#p" x="184" y="32"/>
|
||||||
|
<use xlink:href="#p" x="184" y="80"/>
|
||||||
|
<use xlink:href="#p" x="184" y="96"/>
|
||||||
|
<use xlink:href="#p" x="184" y="104"/>
|
||||||
|
<use xlink:href="#p" x="184" y="128"/>
|
||||||
|
<use xlink:href="#p" x="184" y="136"/>
|
||||||
|
<use xlink:href="#p" x="184" y="144"/>
|
||||||
|
<use xlink:href="#p" x="184" y="152"/>
|
||||||
|
<use xlink:href="#p" x="184" y="160"/>
|
||||||
|
<use xlink:href="#p" x="184" y="192"/>
|
||||||
|
<use xlink:href="#p" x="184" y="216"/>
|
||||||
|
<use xlink:href="#p" x="184" y="224"/>
|
||||||
|
<use xlink:href="#p" x="192" y="32"/>
|
||||||
|
<use xlink:href="#p" x="192" y="48"/>
|
||||||
|
<use xlink:href="#p" x="192" y="56"/>
|
||||||
|
<use xlink:href="#p" x="192" y="64"/>
|
||||||
|
<use xlink:href="#p" x="192" y="80"/>
|
||||||
|
<use xlink:href="#p" x="192" y="96"/>
|
||||||
|
<use xlink:href="#p" x="192" y="128"/>
|
||||||
|
<use xlink:href="#p" x="192" y="144"/>
|
||||||
|
<use xlink:href="#p" x="192" y="152"/>
|
||||||
|
<use xlink:href="#p" x="192" y="160"/>
|
||||||
|
<use xlink:href="#p" x="192" y="168"/>
|
||||||
|
<use xlink:href="#p" x="192" y="176"/>
|
||||||
|
<use xlink:href="#p" x="192" y="184"/>
|
||||||
|
<use xlink:href="#p" x="192" y="192"/>
|
||||||
|
<use xlink:href="#p" x="192" y="200"/>
|
||||||
|
<use xlink:href="#p" x="192" y="216"/>
|
||||||
|
<use xlink:href="#p" x="192" y="224"/>
|
||||||
|
<use xlink:href="#p" x="200" y="32"/>
|
||||||
|
<use xlink:href="#p" x="200" y="48"/>
|
||||||
|
<use xlink:href="#p" x="200" y="56"/>
|
||||||
|
<use xlink:href="#p" x="200" y="64"/>
|
||||||
|
<use xlink:href="#p" x="200" y="80"/>
|
||||||
|
<use xlink:href="#p" x="200" y="96"/>
|
||||||
|
<use xlink:href="#p" x="200" y="112"/>
|
||||||
|
<use xlink:href="#p" x="200" y="136"/>
|
||||||
|
<use xlink:href="#p" x="200" y="144"/>
|
||||||
|
<use xlink:href="#p" x="200" y="168"/>
|
||||||
|
<use xlink:href="#p" x="200" y="184"/>
|
||||||
|
<use xlink:href="#p" x="200" y="200"/>
|
||||||
|
<use xlink:href="#p" x="200" y="208"/>
|
||||||
|
<use xlink:href="#p" x="200" y="216"/>
|
||||||
|
<use xlink:href="#p" x="200" y="224"/>
|
||||||
|
<use xlink:href="#p" x="208" y="32"/>
|
||||||
|
<use xlink:href="#p" x="208" y="48"/>
|
||||||
|
<use xlink:href="#p" x="208" y="56"/>
|
||||||
|
<use xlink:href="#p" x="208" y="64"/>
|
||||||
|
<use xlink:href="#p" x="208" y="80"/>
|
||||||
|
<use xlink:href="#p" x="208" y="96"/>
|
||||||
|
<use xlink:href="#p" x="208" y="128"/>
|
||||||
|
<use xlink:href="#p" x="208" y="160"/>
|
||||||
|
<use xlink:href="#p" x="208" y="176"/>
|
||||||
|
<use xlink:href="#p" x="208" y="192"/>
|
||||||
|
<use xlink:href="#p" x="208" y="200"/>
|
||||||
|
<use xlink:href="#p" x="208" y="208"/>
|
||||||
|
<use xlink:href="#p" x="208" y="224"/>
|
||||||
|
<use xlink:href="#p" x="216" y="32"/>
|
||||||
|
<use xlink:href="#p" x="216" y="80"/>
|
||||||
|
<use xlink:href="#p" x="216" y="104"/>
|
||||||
|
<use xlink:href="#p" x="216" y="112"/>
|
||||||
|
<use xlink:href="#p" x="216" y="128"/>
|
||||||
|
<use xlink:href="#p" x="216" y="136"/>
|
||||||
|
<use xlink:href="#p" x="216" y="144"/>
|
||||||
|
<use xlink:href="#p" x="216" y="176"/>
|
||||||
|
<use xlink:href="#p" x="216" y="192"/>
|
||||||
|
<use xlink:href="#p" x="216" y="200"/>
|
||||||
|
<use xlink:href="#p" x="216" y="224"/>
|
||||||
|
<use xlink:href="#p" x="224" y="32"/>
|
||||||
|
<use xlink:href="#p" x="224" y="40"/>
|
||||||
|
<use xlink:href="#p" x="224" y="48"/>
|
||||||
|
<use xlink:href="#p" x="224" y="56"/>
|
||||||
|
<use xlink:href="#p" x="224" y="64"/>
|
||||||
|
<use xlink:href="#p" x="224" y="72"/>
|
||||||
|
<use xlink:href="#p" x="224" y="80"/>
|
||||||
|
<use xlink:href="#p" x="224" y="112"/>
|
||||||
|
<use xlink:href="#p" x="224" y="120"/>
|
||||||
|
<use xlink:href="#p" x="224" y="128"/>
|
||||||
|
<use xlink:href="#p" x="224" y="144"/>
|
||||||
|
<use xlink:href="#p" x="224" y="152"/>
|
||||||
|
<use xlink:href="#p" x="224" y="176"/>
|
||||||
|
<use xlink:href="#p" x="224" y="184"/>
|
||||||
|
<use xlink:href="#p" x="224" y="192"/>
|
||||||
|
<use xlink:href="#p" x="224" y="200"/>
|
||||||
|
<use xlink:href="#p" x="224" y="208"/>
|
||||||
|
<use xlink:href="#p" x="224" y="216"/>
|
||||||
|
<use xlink:href="#p" x="224" y="224"/>
|
||||||
|
</g>
|
||||||
|
<g></g></svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
30
static/recycle.svg
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="svg1852"
|
||||||
|
width="500"
|
||||||
|
height="500"
|
||||||
|
viewBox="0 0 500 500"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1856" />
|
||||||
|
<g
|
||||||
|
id="g9426"
|
||||||
|
transform="matrix(0.10916853,0,0,-0.10916853,-59.454168,597.04669)">
|
||||||
|
<path
|
||||||
|
d="m 4125.96,4640.21 c -60.45,105.33 -126.32,210.05 -194.01,325.81 -59.51,101.77 -115.02,247.69 -233.79,288.49 -88.97,30.57 -218.55,24.87 -335.74,24.87 -238.52,0 -471.48,4.96 -708.8,7.46 115.68,-111.3 194.18,-264.61 286.02,-400.41 31.49,-46.59 56.11,-98.61 84.55,-146.74 114.69,-194.18 232.64,-389.07 343.21,-581.96 -95.1,-52.48 -191.01,-104.12 -281.03,-161.66 296.9,17.35 577.91,27.92 877.91,44.77 137.57,245.43 280.22,499.06 410.38,743.62 -82.35,-48.66 -164.78,-97.22 -248.7,-144.25 z m -1248.51,166.64 c -53.07,84.43 -111.38,177.15 -169.12,268.59 -55.7,88.24 -126.21,174.96 -233.78,203.94 -20.3,5.47 -49.31,7.78 -77.09,12.43 -25.3,4.25 -53.65,10.67 -77.1,12.45 -163.65,12.42 -273.55,-67.71 -348.2,-149.23 -34.1,-37.26 -55.41,-86.3 -82.06,-134.3 -127.21,-229.18 -269.64,-477.23 -385.49,-693.89 285.43,-153.11 565.14,-311.94 850.57,-465.07 m 1803.1,902.79 c 154.09,87.98 305.54,178.61 460.1,266.11 -199.68,-359.9 -396.33,-722.85 -599.39,-1079.37 -433.28,-21.84 -870.82,-42.72 -1308.18,-64.66 -2.27,5.2 13.74,13.73 24.88,19.9 165.99,92.14 340.08,192.34 504.87,286.02 -101.8,173.43 -204.22,346.23 -305.9,519.77 -184.74,-321.8 -361.72,-651.32 -544.66,-974.91 -338.03,183.44 -673.86,369.03 -1009.74,554.62 114.18,211.08 229.87,419.32 353.15,639.16 59.08,105.34 109.28,220.43 189.02,305.9 77.4,82.98 199.37,151.4 345.7,159.17 52.84,2.81 104.95,-13.21 156.69,-17.41 94.81,-7.68 197.91,-2.48 295.94,-2.48 245.41,0 492.06,-7.47 738.66,-7.47 100.19,0 187.99,-2.45 261.14,-24.87 126.97,-38.91 189.11,-153.41 251.19,-258.64 64.97,-110.17 124.97,-214.57 186.53,-320.84"
|
||||||
|
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path9858" />
|
||||||
|
<path
|
||||||
|
d="m 4640.77,2469.03 c -184.34,-12.36 -393.76,2.49 -601.88,2.49 -207.08,0 -414.32,2.29 -601.85,7.47 1.22,98.66 9.77,219.19 4.96,323.31 -136.6,-262.16 -280.27,-517.25 -417.81,-778.46 143.98,-242.29 281.8,-490.82 425.27,-733.67 4.53,99.18 0.77,195.09 7.47,288.5 95.04,-1.2 198.28,-4.97 300.92,-4.97 105.41,0 212.31,-10.51 303.43,-2.5 146.62,12.92 202.16,117.08 263.63,213.9 148.23,233.45 303.64,481.76 450.14,708.8 -0.99,4.29 3.69,6.38 0,7.45 -40.63,-16.78 -87.23,-29.16 -134.28,-32.32 z m -932.64,1029.64 c 165.79,-293.25 333.99,-588.18 504.86,-880.41 11.88,-20.34 25.18,-52.99 37.31,-57.2 21.31,-7.42 70.62,0 99.47,0 98.19,0 222.77,-8.87 295.96,0 111.22,13.45 204.78,78.03 263.64,144.25 87.85,98.84 152.41,266.37 104.43,435.23 -13.84,48.76 -35.98,84.65 -59.69,126.83 -135.64,241.53 -272.96,480.57 -407.84,711.29 -275.88,-153.55 -557.25,-323.17 -838.14,-479.99 z m 870.46,606.84 c 2.01,-0.48 2.46,-2.5 4.96,-2.49 123.6,-214.99 242.66,-429.99 368.1,-646.64 78.16,-135.04 186.73,-261.92 171.61,-487.45 -11.1,-165.26 -114.82,-275.77 -186.53,-390.47 -174.63,-279.29 -344.05,-546.98 -519.82,-825.68 -77.96,-123.69 -147.75,-232.92 -310.85,-266.12 -54.08,-11.02 -113.29,-8.09 -181.57,-4.99 -123.95,5.65 -259.75,-1.14 -380.51,2.5 -0.74,-175.84 -6.02,-347.16 -7.46,-522.291 -5.89,-3.617 -13.59,14.101 -19.89,24.871 -199.27,339.78 -396.99,696.46 -596.9,1037.09 206.96,387.43 418.13,770.71 624.25,1158.98 -0.3,-190.38 -9.75,-412.48 -12.45,-616.8 202.92,1.47 398.55,-4.36 601.89,-2.49 -173.7,322.08 -370.65,642.4 -549.64,967.46 331.45,191.65 663.62,382.6 994.81,574.52"
|
||||||
|
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path9860" />
|
||||||
|
<path
|
||||||
|
d="m 1447.4,2270.08 c -46.21,-78.85 -95.63,-159.69 -94.49,-266.13 1.81,-172.7 95.5,-317.04 198.96,-387.96 82.01,-56.24 184.38,-79.6 318.33,-79.6 256.45,0.02 521.31,-2.09 793.36,2.49 -2.81,321.34 -2.82,645.47 -2.48,969.94 -357.31,0 -714.62,0 -1071.91,0 -47.61,-73.71 -91.59,-153.18 -141.77,-238.74 z m -325.8,1410.14 c -76.5,-133.74 -157.479,-262.83 -233.784,-392.96 -25.332,-43.2 -57.761,-87.1 -77.086,-131.8 -50.343,-116.47 -0.851,-226.72 42.266,-308.41 137.602,-260.55 278.474,-523.3 415.334,-773.46 13.71,124.29 86.65,215.53 141.77,310.88 167.68,290.08 327.8,575.06 499.9,863.01 93.53,-52.38 186.41,-118.66 278.54,-164.15 -135.06,216.08 -272.93,436.74 -407.88,656.57 -15.15,24.7 -42.29,85.59 -62.17,92.03 -21.62,7 -67.46,0 -104.45,0 -238.34,0 -531.63,-0.02 -738.649,0 77.593,-55.05 167.129,-98.16 246.209,-151.71 z m 1345.48,-872.95 c -173.7,95.71 -348.89,211.51 -524.76,313.37 -102.26,-172.15 -203.97,-344.85 -300.92,-522.28 368.91,0 751.06,0 1111.7,0 0,-384.68 0,-769.32 0,-1153.98 -322.47,0 -602.15,0 -907.77,0 -293.56,0 -455.02,139.48 -544.66,360.61 -40.87,100.82 -101.41,198.5 -154.19,298.46 -130.44,246.98 -258.828,486.89 -392.964,738.64 -37.481,70.34 -61.715,131.77 -54.711,236.27 8.261,123.16 82.304,205.41 136.781,295.95 58.137,96.62 107.676,189.42 161.66,271.09 -145.043,96.19 -312.867,182.88 -452.637,276.05 300.981,0 606.771,0 910.261,0 103.02,0 204.66,-5.91 303.42,0 7.03,0.44 5.98,1.86 9.95,2.5 8.82,1.41 34.47,-41.84 47.24,-62.18 218.06,-347.29 435.03,-714.78 651.6,-1054.5"
|
||||||
|
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||||
|
id="path9862" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |