Merge branch 'main' into gallery_scroll_improvement

This commit is contained in:
Ashil
2025-08-07 13:59:59 +05:30
committed by GitHub
65 changed files with 2423 additions and 1240 deletions

View File

@@ -15,6 +15,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
RUSTFLAGS: -D warnings
jobs:
lint:
runs-on: ubuntu-latest
@@ -33,9 +36,9 @@ jobs:
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- run: cargo fmt --check
- run: cargo clippy
- run: cargo clippy --all-targets --all-features
- run: cargo build

View File

@@ -48,7 +48,7 @@ See [docs/](docs/README.md) for how to edit these documents.
## Code contributions
If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](auth/docs/adding-icons.md), or fixing a specific bug.
If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](mobile/apps/auth/docs/adding-icons.md), or fixing a specific bug.
Code that changes the behaviour of the product might not get merged, at least not initially. The PR can serve as a discussion bed, but you might find it easier to just start a discussion instead, or post your perspective in the (likely) existing thread about the behaviour change or new feature you wish for.

View File

@@ -98,7 +98,7 @@ more, see [docs/adding-icons](docs/adding-icons.md).
The best way to support this project is by checking out [Ente
Photos](../mobile/README.md) or spreading the word.
For more ways to contribute, see [../CONTRIBUTING.md](../../../CONTRIBUTING.md).
For more ways to contribute, see [../../../CONTRIBUTING.md](../../../CONTRIBUTING.md).
## Certificate Fingerprints

View File

@@ -60,6 +60,15 @@
"slug": "amtrak",
"hex": "003A5D"
},
{
"title": "Animal Crossing",
"slug:": "animal_crossing",
"altNames": [
"AnimalCrossing",
"Bell Tree Forums",
"BellTree Forums"
]
},
{
"title": "Ankama",
"slug": "ankama"
@@ -81,6 +90,13 @@
"Docaposte AR24"
]
},
{
"title": "Art Fight",
"slug": "art_fight",
"altNames": [
"ArtFight"
]
},
{
"title": "Aruba",
"slug": "aruba",
@@ -341,6 +357,9 @@
"slug": "cih",
"hex": "D14633"
},
{
"title": "Chucklefish"
},
{
"title": "Clipper",
"slug": "clippercard",
@@ -1505,6 +1524,9 @@
{
"title": "Skinport"
},
{
"title": "Smogon"
},
{
"title": "SMSPool",
"slug": "sms_pool_net",
@@ -1664,6 +1686,12 @@
{
"title": "TorGuard"
},
{
"title": "Toyhouse",
"altNames": [
"Toyhou.se"
]
},
{
"title": "Trading 212"
},
@@ -1685,6 +1713,15 @@
"T Rowe Price Group, Inc"
]
},
{
"title": "TU Dresden",
"slug": "tu_dresden",
"altNames": [
"Technische Universität Dresden",
"Dresden University of Technology"
],
"hex": "00305d"
},
{
"title": "Tweakers"
},
@@ -1699,6 +1736,12 @@
"Twitch tv"
]
},
{
"title": "Twitter",
"altNames": [
"X"
]
},
{
"title": "Ubiquiti",
"slug": "ubiquiti",
@@ -1906,6 +1949,22 @@
{
"title": "Co-Wheels",
"slug": "cowheels"
},
{
"title": "Zivver",
"slug": "zivver"
},
{
"title": "Meesman Indexbeleggen",
"slug": "meesman"
},
{
"title": "Scouting Nederland",
"slug": "scoutingnederland"
},
{
"title": "ISC2",
"slug": "isc2"
}
]
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 326.24 311.07"><path fill="#1aae5e" d="M266.38 1.26c-.93 9.12-7.71 47.21-27.28 83.44a1 1 0 0 1-1.7-.06c-4-8.24-24.86-44.58-79.29-44.91C96.71 39.35 66.67 87 66.67 87s-23.45 32.93-33.49 94.89-22.61 79-22.61 79L.39 277a2.54 2.54 0 0 0 1.11 3.67c13 5.81 72.68 30.38 151.68 30.38 53.8 0 70-7.58 74.25-10.55a.61.61 0 0 0-.35-1.12c-10.55 1.16-51.2-5.26-51.2-44 0-34.79 25.3-51.62 51.81-52.83 12.56-.58 39.35 4.74 47.72 29.3 7 20.66-2.31 39.15-5.45 44.5a.62.62 0 0 0 .77.89c9.52-3.84 53.71-25.77 55.47-93.95 1.55-59.91-47.64-85.62-70.8-86.71a1.18 1.18 0 0 1-1-1.62c6.23-15.94 45-57.27 51.84-64.46a1.57 1.57 0 0 0 0-2.17A159.05 159.05 0 0 0 268.46.17a1.42 1.42 0 0 0-2.08 1.09Z"/></svg>

After

Width:  |  Height:  |  Size: 730 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="584.48486" >
<g transform="translate(-137.5,-22.390163)">
<path style="fill:#1c252c" d="m 137.5,29.631487 0,252.320873 132.1114,0 c 51.99819,0 100.05059,-18.25003 100.05059,-71.85789 0,-36.24066 -26.10767,-52.7841 -54.82326,-61.4437 17.4955,-7.01771 37.3423,-21.76056 37.3423,-52.666034 0,-39.55613 -39.24823,-66.353249 -92.09117,-66.353249 l -122.58986,0 z m 80.93311,58.76577 29.75482,0 c 12.11271,0 21.34908,9.487931 21.34908,19.117473 0,8.94677 -9.59745,18.52237 -21.34908,18.52237 l -29.75482,0 0,-37.639843 z m 0,90.008333 39.27636,0 c 13.71131,0 25.21721,10.41366 25.21721,22.31611 0,12.63921 -10.76115,23.13438 -27.59759,23.13438 l -36.89598,0 0,-45.45049 z" id="path2998" />
<path id="path3005" d="m 202.9606,309.47481 0,252.32088 132.11141,0 c 51.99819,0 100.05058,-18.25004 100.05058,-71.8579 0,-36.24065 -26.10766,-52.78409 -54.82326,-61.4437 17.4955,-7.01771 37.3423,-21.76056 37.3423,-52.66603 0,-39.55613 -39.24822,-66.35325 -92.09117,-66.35325 l -122.58986,0 z m 80.93312,58.76577 29.75482,0 c 12.11271,0 21.34908,9.48793 21.34908,19.11747 0,8.94678 -9.59745,18.52238 -21.34908,18.52238 l -29.75482,0 0,-37.63985 z m 0,90.00833 39.27636,0 c 13.71131,0 25.21721,10.41366 25.21721,22.31612 0,12.6392 -10.76115,23.13437 -27.5976,23.13437 l -36.89597,0 0,-45.45049 z" style="fill:#1c252c" />
<path style="fill:#1c252c" d="m 376.01464,281.92185 0,-252.32088 203.52297,0 0,60.937872 -122.8279,0 0,33.087358 99.73816,0 0,57.12926 -99.73816,0 0,40.46655 122.8279,0 0,60.69984 z" id="path3007"/>
<path style="fill:#1c252c" d="m 689.78812,289.1442 c 57.28767,0 103.11071,-32.67744 103.11071,-85.63169 0,-85.46143 -111.17753,-72.31037 -111.17753,-98.91916 0,-10.278288 10.80934,-15.732656 21.89462,-15.732656 19.10438,0 32.9075,12.586126 32.9075,12.586126 L 784.35446,55.903565 C 765.2233,37.697019 735.00886,22.390163 694.30743,22.390163 c -61.12759,0 -101.11859,36.280243 -101.11859,80.044867 0,86.54828 109.57491,73.98694 109.57491,101.14304 0,9.51935 -9.15835,19.09619 -25.76901,19.09619 -18.85922,0 -33.79879,-11.38479 -45.42578,-21.04418 l -48.11128,45.86837 c 19.37303,18.87022 50.47517,41.64575 106.33044,41.64575 z" id="path3009" />
<path style="fill:#1c252c" d="m 855.18627,281.92185 0,-191.621047 -66.6508,0 0,-60.699833 214.23473,0 0,60.699833 -66.65082,0 0,191.621047 z" id="path3011" />
<path style="fill:#1c252c" d="m 437.90467,309.29628 80.69507,0 0,151.15449 c 0,15.34925 15.27608,29.49386 31.20285,29.49386 15.02419,0 30.2111,-12.77313 30.2111,-30.30098 l 0,-150.34737 80.45703,0 0,149.31902 c 0,59.25051 -49.19276,106.51359 -112.80036,106.51359 -63.96969,0 -109.76569,-51.43651 -109.76569,-109.74208 z" id="path3013" />
<path style="fill:#1c252c;fill-opacity:1;stroke:none" d="m 759.97084,561.61716 0,-90.16156 -94.84461,-162.15932 81.6448,0 53.66637,86.88408 53.85177,-86.88408 81.83021,0 -95.21543,163.08638 0,89.2345 z" id="path3015" />
<path style="fill:#ffed31" d="m 936.11938,447.38916 -47.60772,47.60772 0,64.27041 47.60772,47.60771 201.38062,0 0,-159.48584 z" id="path3017" />
<path style="fill:#1c252c" id="path3024" d="m 469,573.36218 c 0,2.20914 -1.79086,4 -4,4 -2.20914,0 -4,-1.79086 -4,-4 0,-2.20914 1.79086,-4 4,-4 2.20914,0 4,1.79086 4,4 z" transform="matrix(2.6779338,0,0,2.6779338,-327.65077,-1008.3244)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,4 @@
<svg width="76" height="28" viewBox="0 0 76 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.6137 0.263554V24.2528C3.6137 24.3057 3.59223 24.3565 3.55413 24.3932L0.140163 27.6896C0.0876965 27.7404 0 27.703 0 27.6301V0.263554C0 0.217915 0.0370442 0.180891 0.0827069 0.180891H3.53099C3.57666 0.180891 3.6137 0.217915 3.6137 0.263554ZM12.9151 7.14572C12.9151 4.77676 14.8577 3.33976 17.4613 3.33976C19.7149 3.33976 21.3857 4.27186 23.173 5.94205C23.445 6.21361 23.5321 6.36292 23.8042 6.05236L25.4188 4.31055C25.6908 4 25.5433 3.92232 25.2713 3.65076C23.3283 1.39816 20.4529 0 17.4223 0C12.9538 0 9.41785 2.91284 9.41785 7.10703C9.41785 15.8838 22.9397 14.0583 22.9397 20.1554C22.9397 22.6408 20.764 24.5827 17.5388 24.5827C14.4361 24.5827 12.8017 23.1551 11.0921 21.524C10.8201 21.2522 10.7614 21.1884 10.5283 21.4216L8.68301 23.1634C8.411 23.3966 8.43746 23.4164 8.63175 23.7269C10.3993 26.1087 13.6923 27.9612 17.539 27.9612C22.3572 27.9612 26.5147 24.7768 26.5147 20.3107C26.5147 11.0679 12.9153 13.1263 12.9153 7.14557L12.9151 7.14572ZM44.5835 3.53395C47.3423 3.53395 49.596 4.58257 51.4999 6.17492C51.5444 6.213 51.6963 6.33632 51.743 6.37576C51.9289 6.53262 51.9644 6.53973 52.1883 6.30851C52.2428 6.25229 52.4023 6.08032 52.4023 6.08032L54.0336 4.29771C54.2087 4.09279 54.1792 4.07768 54.0528 3.94545C54.0011 3.89134 53.9102 3.80702 53.8311 3.72813C51.3835 1.55367 48.2361 0 44.5835 0C36.8514 0 30.8288 6.21361 30.8288 14.0196C30.8288 21.8255 36.8514 28 44.5835 28C48.2361 28 51.3906 26.3504 53.8384 24.1369C54.1104 23.9037 54.2128 23.8197 53.9409 23.5477L52.278 21.729C52.006 21.4571 51.9349 21.5213 51.6629 21.7545C49.759 23.3466 47.3422 24.4659 44.5834 24.4659C39.0271 24.4659 34.5199 19.9612 34.5199 14.0194C34.5199 8.03869 39.0272 3.53395 44.5835 3.53395ZM66.7904 0.0562167C63.2934 0.0562167 60.5212 1.59885 58.8115 3.73463C58.7651 3.78873 58.6609 3.93985 58.6299 3.98821C58.505 4.18286 58.541 4.19736 58.7175 4.39382C59.1585 4.88481 60.5292 6.43666 60.5292 6.43666C60.5868 6.49756 60.6405 6.49998 60.6904 6.45585C60.7448 6.4078 60.863 6.29536 60.9279 6.23174C62.0159 5.10544 63.8762 3.24107 66.7514 3.24107C69.9763 3.24107 71.9191 5.26049 71.9191 8.21187C71.9191 12.3999 69.5179 14.3919 64.1713 18.8858C62.6162 20.1928 59.7455 22.7114 59.029 23.3411C58.9234 23.4338 58.8637 23.5669 58.8637 23.7071V27.4699C58.8637 27.6378 59.0612 27.7277 59.1874 27.6169C60.6628 26.3236 66.7627 20.9799 68.7448 19.2968C73.8769 14.9387 75.2608 11.9788 75.2608 8.17288C75.2608 3.55163 71.9193 0.0563678 66.7902 0.0563678L66.7904 0.0562167Z" fill="black"/>
<path d="M75.4003 24.6003H66.7791C66.6688 24.6003 66.5794 24.6896 66.5794 24.7999V27.3522C66.5794 27.4624 66.6688 27.5518 66.7791 27.5518H75.4003C75.5106 27.5518 75.6 27.4624 75.6 27.3522V24.7999C75.6 24.6896 75.5106 24.6003 75.4003 24.6003Z" fill="#468145"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="351.54666"
height="329.84"
viewBox="0 0 351.54666 329.84"
sodipodi:docname="SN_logo_CMYK.eps"
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">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:page
x="0"
y="0"
inkscape:label="1"
id="page1"
width="351.54666"
height="329.84"
margin="0"
bleed="0" />
</sodipodi:namedview>
<g
id="g1"
inkscape:groupmode="layer"
inkscape:label="1">
<g
id="group-R5">
<path
id="path2"
d="m 1090.05,2469.06 c -24.19,-7.15 -103.737,-35.43 -111.945,-99.52 -0.417,-3.28 -0.664,-6.75 -0.664,-10.38 0,-1.78 0.235,-3.7 0.364,-5.56 -10.262,0.29 -19.739,0.6 -26.133,0.81 l -65.332,1.34 -23.559,-4.16 c -28.023,-4.55 -103.66,-16.9 -105.203,-94.63 -0.223,-5.16 -1.117,-25.66 -1.117,-25.66 0,-46.17 9.445,-96.59 26.602,-141.96 5.046,-13.56 12.753,-23.97 18.382,-31.57 0.071,-0.11 3.368,-4.86 4.36,-6.29 9.855,-38.77 26.3,-70.63 45.929,-98.74 -53.027,-6.36 -106.531,-15 -159.277,-30.85 l 10.656,1.99 c -28.793,-2.16 -56.379,-17.47 -84.343,-46.81 l -2.582,-2.71 -2.157,-3.07 c -9.359,-13.34 -16.133,-26.39 -20.597,-39.33 -108.993,-32.27 -198.711,-59.86 -271.504,-83.66 -64.18,-21.16 -191.942,-75.53 -197.344,-77.84 l -3.488,-1.48 c 0,0 -0.75,-0.5 -0.969,-0.64 -0.926,-0.38 -1.899,-0.79 -1.899,-0.79 C 20.5117,1627.77 0,1560.47 0,1511.02 c 0,-77.67 60.293,-99.23 89.2734,-109.59 6.5352,-2.34 13.3046,-4.76 16.7896,-6.72 39.054,-21.38 76.554,-14 103.933,-8.62 l 18.535,3.23 c 13.426,1.65 28.203,6.02 42.918,10.64 13.539,-28.39 38.336,-54.64 77.246,-62.14 33.27,-6.51 55.453,-0.36 73.301,6.07 0.996,-10.01 3.031,-19.97 7.145,-29.66 6.875,-16.22 18.355,-31.11 32.617,-43.77 -23.574,-18.45 -57.028,-51.65 -57.028,-91.97 0,-3.61 0.274,-7.28 0.844,-11 -0.008,0.04 0.399,-2.76 0.399,-2.76 3.066,-21.52 11.211,-78.7 89.461,-110.76 30.101,-12.33 64.925,-15.82 103.968,-10.82 3.43,-11.8 8.594,-22.92 15.438,-33.02 -21.535,-10.618 -26.383,-14.302 -29.688,-16.97 l -1.097,-0.91 c -14.344,-12.238 -35.231,-31.379 -50.989,-55.961 -6.539,-9.769 -26.257,-42.648 -26.257,-80.109 0,-6.758 0.64,-13.668 2.121,-20.629 4.996,-23.449 18.433,-42.66 38.859,-55.539 28.457,-17.571 59.524,-19.403 83.496,-17.114 3.903,-25.226 22.399,-54.019 79.567,-70.777 l 3.652,-1.082 c 18.387,-5.598 43.184,-4.641 95.445,-0.18 l 21.008,1.61 c 6.332,0.222 16.938,1.801 29.336,4.019 0.02,-25.679 7.574,-48.519 21.941,-65.519 l 1.036,-1.188 c 32.378,-35.941 81.164,-45.679 100.636,-48.211 21.704,-3.019 49.384,6.219 103.054,28.078 l 13.14,5.274 c 2.61,1.027 9.13,2.937 16.79,5.066 2.08,-2.797 3.56,-5.718 6.13,-8.359 11.57,-11.93 32.27,-25.891 65.17,-24.75 30.02,0.793 80.65,12.051 86.31,13.332 l 3.03,0.68 2.91,1.05 c 62.56,22.559 268.81,97.289 296.36,112.11 l 1.04,0.57 c 22.84,11.539 205.84,69.93 283.99,94.859 l 56.19,18 c 82.34,25.911 157.95,84.43 179.73,107.129 l 0.8,0.852 c 18.25,19.91 30.13,47.211 35.35,81.149 0.57,3.61 0.85,7.19 0.85,10.73 0,17.83 -6.96,34.73 -16.23,50.72 42.8,19.11 84.58,37.78 85.78,38.31 15.35,5.69 54.79,23.33 71.3,59.78 3.85,8.53 5.57,16.91 6.1,25.08 45.66,11.71 96.03,24.63 96.03,24.63 0.05,0.01 1.58,0.37 1.58,0.37 28.95,6.86 105.87,25.09 177.5,60.59 66.98,32.41 79.83,70.82 79.83,103.21 0,4.38 -0.23,8.66 -0.6,12.8 l -0.24,2.95 -0.24,-3.74 c 0.31,2.41 0.46,4.85 0.46,7.29 0,33.59 -27.8,69.46 -53.31,88.19 -0.41,1.45 -0.69,2.46 -0.69,2.46 -16.17,19.47 -40.32,20.74 -51.92,21.36 l 1.3,-0.09 -14.24,2.33 c -18.43,3.88 -38.47,6.86 -81.29,5.91 -1.01,12.89 -3.02,25.28 -7.03,36.8 -25.36,76.06 -92.5,130.37 -170.88,137.82 -49.09,6.17 -87.12,3.96 -128.02,0.48 35.15,41.09 71.23,87.59 80.71,115.32 8.24,23.4 23.39,130.23 24.13,140.16 l 0.1,1.48 0.21,13.73 c 0,29.46 -2.97,49.47 -5.44,65.61 -0.18,1.62 -3.11,28.05 -3.39,30.67 -2.34,70.84 -14.17,133.75 -16.55,145.76 l -23.69,117.63 -70.81,-96.45 c -9.23,-12.78 -20.6,-28.08 -25.51,-34.26 -14.11,-10.83 -79.29,-55.93 -128.62,-86.83 -11.04,44.47 -37.4,83.58 -79.17,113.49 l -1.87,1.35 -1.99,1.16 c -52.54,30.73 -110.3,59.29 -176.56,60.18 -66.4,2.28 -128.22,-11.25 -183.74,-40.23 -33.75,-18.23 -58.71,-44.83 -80.73,-68.3 -13.84,-14.71 -26.65,-28.35 -40.27,-39.12 l 2.63,1.94 c -12.03,-8.28 -23.19,-17.08 -33.97,-26 -8.34,43.79 -18.04,92.87 -39.65,140.64 -26.22,56.12 -61.23,104.55 -110.28,152.92 -23.83,24.47 -67.53,43.26 -91.52,52.28 l -2.9,1.03 -14.98,5.04 -16.03,-4.73"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path3"
d="m 473.715,1191.47 c -2.852,17.82 20.695,43.53 44.914,60.65 24.98,17.09 64.922,34.93 89.207,43.49 24.219,7.87 41.348,13.57 54.894,17.87 78.485,27.09 255.391,89.13 262.52,91.97 7.832,3.61 409.42,134.83 443.68,142.01 33.48,7.13 177.58,71.27 192.62,84.14 9.92,8.56 301.68,42.09 339.51,23.53 37.06,-18.53 -12.12,-145.54 55.66,-126.98 66.96,18.53 275.99,51.36 313.07,-8.52 37.09,-59.97 7.18,-89.93 -19.21,-101.31 -25.71,-10.75 -511.44,-131.98 -511.44,-131.98 0,0 -691.94,-186.88 -718.33,-191.14 -20.68,-3.61 -163.326,-47.83 -217.564,-65.61 -14.961,-5 2.149,0.69 -26.355,-7.88 -36.418,-10.65 -29.981,-5 -46.399,-2.12 -36.367,7.12 -64.199,32.12 -67.07,66.3 -2.84,33.53 53.527,52.79 49.269,52.79 -4.289,0 -106.289,-52.09 -179.757,-22.13 -54.942,22.13 -56.375,56.4 -59.219,74.92"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path4"
d="m 728.52,1007.06 c 0,0 369.52,114.79 409.46,129.84 39.93,14.95 473.58,134.1 503.58,140.49 29.22,6.43 334.55,100.61 369.43,109.88 34.96,9.26 265.42,70.62 265.42,70.62 0,0 73.48,17.78 147.66,21.34 74.18,2.88 76.27,-5.69 103.4,-7.83 27.13,-1.42 10.69,-7.13 19.31,-10 7.82,-2.13 41.31,-36.35 39.22,-52.78 -2.18,-16.4 16.34,-44.22 -51.4,-77 -67.74,-33.58 -141.23,-50.67 -168.36,-57.1 -27.05,-6.39 -158.37,-40.62 -158.37,-40.62 0,0 32.18,-15 22.18,-37.13 -10,-22.09 -44.96,-34.22 -44.96,-34.22 l -166.19,-74.22 c 0,0 41.39,-12.08 47.05,-20.66 5.74,-8.56 20.7,-29.96 18.52,-43.47 -2.09,-13.62 -7.13,-37.88 -22.78,-54.97 -15.74,-16.39 -84.18,-70.609 -159.06,-94.191 -74.88,-24.211 -323.16,-101.957 -349.51,-116.957 -26.44,-14.223 -289.63,-109.102 -289.63,-109.102 0,0 -50.62,-11.441 -77.03,-12.128 -41.35,-1.43 -38.51,42.789 -38.51,42.789 0,0 -53.48,-12.871 -67.07,-18.571 -13.53,-5 -74.88,-32.09 -90.552,-29.91 -16.437,2.129 -49.961,9.949 -69.902,32.09 -19.289,22.82 -6.418,68.441 0.691,73.48 6.418,5 21.379,28.481 21.379,28.481 0,0 -94.121,-22.09 -113.41,-22.781 -18.516,-0.7 -83.438,-8.621 -99.871,-3.61 -16.367,5.039 -54.895,13.571 -47.766,43.52 7.129,30.699 26.387,53.488 47.071,62.051 20.695,8.609 -74.903,-26.352 -74.903,-26.352 0,0 -45.633,-16.43 -79.16,4.262 -32.832,20.699 -12.129,67.789 0.703,87.008 12.824,20 31.387,36.402 41.367,44.921 10.676,8.61 111.993,52.83 111.993,52.83"
style="fill:#1c63b7;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path5"
d="m 144.184,1624.47 c 0,0 130.488,55.6 193.277,76.31 144.078,47.09 321.703,98.43 380.211,115.57 55.613,17.09 227.519,69.18 373.038,113.4 115.6,34.96 215.41,64.93 226.15,67.75 24.94,5.7 524.24,162.66 540.68,143.4 16.4,-18.52 67.78,-64.92 64.92,-132.01 -2.87,-67.74 -126.97,-232.5 -148.37,-243.89 -22.13,-10.73 -391.6,-83.49 -391.6,-83.49 0,0 191.15,75.62 199.68,-2.86 7.91,-78.45 -148.36,-106.28 -156.19,-110.54 -7.83,-5 -310.29,-99.87 -537.097,-172.63 -150.559,-48.47 -247.531,-85.56 -256.828,-87 -24.219,-2.14 -66.309,-34.96 -108.426,-18.56 -28.508,11.38 -64.875,43.47 -48.488,83.44 16.418,40.7 51.379,48.52 51.379,48.52 0,0 -22.844,-7.82 -71.329,-16.43 -48.527,-7.84 -57.089,-26.35 -97.031,-18.53 -40.637,7.83 -51.379,56.36 -49.246,69.92 2.848,13.53 -57.023,-14.27 -86.293,-17.87 -29.957,-3.56 -62.07,-17.09 -92.031,-0.69 -29.195,16.43 -80.59,16.43 -80.59,72.74 0,87.05 87.77,108.49 94.184,113.45"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path6"
d="m 1933.07,2015.15 c 0,0.04 -5.39,22.36 -5.39,22.36 -4.7,18.3 -10.48,41.04 -10.96,50.74 -4.61,40.61 -26.09,74.92 -63.92,102 -46.75,27.35 -97.36,52.66 -152.11,53.36 -57.99,2.13 -111.74,-9.48 -159.79,-34.57 -26,-14.04 -47.04,-36.48 -67.44,-58.22 -14.31,-15.22 -29.08,-30.96 -45.7,-44.09 -20.05,-13.84 -38.66,-30.05 -56.66,-45.7 l -31.8,-27.05 c -0.72,-0.18 -4.38,-1.78 -5.48,-1.26 l -0.72,1.26 c -6.5,24.43 -11.26,49.57 -15.89,73.88 -8.42,44.27 -17.09,90.05 -37,134.05 -23.51,50.31 -55.42,94.32 -100.45,138.58 -20.18,20.83 -71.6,39.74 -73.76,40.57 l -0.9,0.3 -0.89,-0.25 c -2.96,-0.88 -71.84,-21.48 -76.51,-57.93 -2.35,-18.43 12.11,-37.17 43.03,-55.7 -0.05,0 0.7,-0.6 1.37,-1.13 -8.55,-5.69 -79.213,-3.35 -122.104,-1.91 l -59.785,1.22 c -0.547,-0.09 -18.703,-3.3 -18.703,-3.3 -29.695,-4.84 -63.379,-10.22 -63.985,-46.58 -0.71,-7.95 -1.062,-16.09 -1.062,-24.48 0,-40.04 8.191,-84.14 23.367,-124.27 2.637,-7.08 7.305,-13.44 11.809,-19.52 4.433,-6.09 8.672,-11.83 11.023,-18.22 15.195,-66.48 54.719,-109.79 96.594,-155.71 0,0 0.476,-0.56 0.691,-0.78 -2.468,-0.23 -5.019,-0.48 -5.019,-0.48 -78.934,-7.39 -160.645,-15.05 -238.086,-38.31 -15.672,-1.17 -32.852,-11.47 -51.875,-31.44 -12.324,-17.56 -17.891,-32.35 -17.891,-46.96 -0.633,-4.26 -0.886,-8.56 -0.886,-12.92 0,-24.47 9.433,-49.61 29.218,-77.83 26.043,-41 54.942,-74.09 88.289,-101.18 42.285,-31.87 88.555,-54.04 150.059,-71.88 27.148,-8.39 49.328,7.18 70.832,22.27 10.242,7.18 20.918,13.95 31.964,18.65 -15.987,-28.7 -28.362,-64 -28.362,-98.22 0,-25.97 6.984,-51.06 24.812,-71.62 20.02,-21.57 48.7,-32.36 73.99,-40.78 15,-2.79 29.26,-2.48 44.35,-2.09 l 28.42,-0.18 c 31.61,2 64.33,18.92 83.75,43.13 56.2,83.31 84.5,172.06 84.5,264.42 0,10.65 -0.39,21.39 -1.15,32.14 0.4,0.87 3.15,7 3.15,7 0,0 1.06,2.3 2.11,4.52 3.59,-3.26 25.67,-23.13 25.67,-23.13 27.53,-21.57 28.79,-24.4 44.32,-58.66 l 31.3,-66.01 c 7.92,-15.48 11.61,-37.96 11.61,-65.26 0,-123.88 -76.31,-346.73 -183.8,-465.79 -23.87,-26.48 -35.65,-45.479 -35.65,-57.569 0,-3.082 0.76,-5.699 2.26,-7.91 12.61,-18.09 71.96,4.738 78.68,7.437 l 0.84,0.301 0.48,0.781 c 1.53,2.391 151.32,240.16 199.37,429.52 7.92,31.17 12.79,66.31 12.79,102.49 0,74.31 -20.44,153.15 -76.97,211.23 -0.61,0.48 -11.65,9.7 -11.65,9.7 -4.61,4.35 -9.91,9.13 -16.05,12.82 1.31,2.62 3.74,7.8 5.05,10.53 28,-7.34 53.83,-18.74 78.74,-29.88 30,-13.43 58.27,-26.08 90.4,-32.21 11.35,-2.79 19.36,-7.44 27.09,-11.96 11.74,-6.83 22.78,-13.22 43.49,-12.34 l 31.91,3.38 c 14.4,2.13 23.87,3.56 33.83,-0.99 3.56,-1.62 8.44,-0.79 14.79,2.69 27.56,15 77.74,76.52 81.92,105.79 0.17,1.65 0.17,3.61 0.17,5.7 0,24.92 -9.39,77.96 -37.57,99.66 -14.09,11.04 -43.22,25.78 -68.88,38.74 -20.43,10.31 -45.22,22.88 -49.74,27.92 75.44,40.96 143.45,9.44 144.14,9.12 70.75,-45.56 93.75,-60.35 101.88,-64.43 0.35,-0.26 0.14,-0.83 0.53,-1.04 0,0 0.34,0.47 0.43,0.56 3.13,-1.48 3.74,-1.09 4.3,-0.35 l 1.61,2.48 -3.26,2.13 c -2.57,1.65 2.66,12.61 7.22,22.27 10.74,22.6 26.91,56.73 19.7,96.39"
style="fill:#00a650;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path7"
d="m 1775.14,1881.4 v -0.04 0.04"
style="fill:#00a650;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path8"
d="m 2381.63,1549.8 c -19.35,58.06 -69.83,99.1 -128.66,104.58 -50.74,6.52 -87.62,3.26 -134.32,-0.87 l -31.66,-2.69 c -7.73,0 -42.09,-13.79 -89.83,-33.23 -11.31,-4.64 -22.13,-9.04 -31.31,-12.69 26.09,21.52 67.58,64.78 102.79,105.45 40.62,45.56 86.8,102.61 94.71,125.74 5.87,16.7 20.78,117.1 21.56,127.67 0.09,4.04 0.18,7.87 0.18,11.52 0,26.18 -2.65,43.61 -5.04,59.23 l -3.65,33.04 c -2.18,73.53 -15.66,139.58 -15.78,140.27 l -1.27,6.31 -3.83,-5.22 c -0.26,-0.35 -24.26,-33.52 -30.48,-40.44 -6.3,-6.31 -104.57,-74.44 -159.83,-107.13 -87.37,-52.18 -126.85,-119.76 -139.81,-141.98 l -3.09,-5.22 c -8.12,-13.3 -17.43,-33.96 -26.38,-53.91 -8.4,-18.66 -16.31,-36.31 -22.09,-45.18 -8.18,-12.05 -17.87,-38.87 -24.96,-58.53 -3.14,-8.65 -6.14,-16.88 -7.22,-18.56 -5.74,-6.14 -8.05,-18.71 -10.26,-30.83 -1.26,-6.96 -2.7,-13.4 -4.7,-18.18 -0.91,1.74 -1.91,3.92 -2.7,6.3 -0.26,0.92 -7.74,26.13 -55.13,113.89 -43.83,81.75 -50.53,81.18 -132.49,73.96 l -30.75,-2.57 c -90.52,-6.61 -157.61,-53.31 -193.62,-78.39 l -16.61,-11.19 c -18.61,-11.47 -55.14,-62.95 -97.33,-122.56 l -47.75,-66.71 6.37,0.65 c 11.18,1.31 21.61,-0.22 32.74,-1.83 11.58,-1.65 23.6,-3.08 36.38,-1.29 -0.72,-3.44 -1.52,-6.92 -1.52,-10.41 0,-17.38 9.08,-34.64 25.87,-47.91 20.93,-20.91 41.13,-33.3 67.29,-41.13 3.28,-0.13 5.2,-4.53 6.97,-8.83 2.01,-4.66 4.05,-9.52 7.94,-12.17 l 1.46,-1.05 1.91,0.92 c 3.26,1.83 4.67,9.43 6.87,82.87 0.39,14.61 0.85,29.75 1.24,31.61 15.78,49.92 54.74,81.71 106.66,86.96 19.22,2.09 74.44,-4.86 82.53,-45.47 0.22,-0.75 10.95,-24.74 17.31,-31.26 0.08,-0.32 18,-51.75 18,-51.75 11.04,-32.75 19.74,-58.61 39.09,-98.84 2.65,-4.78 2.91,-9.82 3.21,-15.09 l 0.83,-8.43 c 0.87,-4.26 1.3,-8.78 1.3,-13.43 0,-16.27 -5.08,-34.71 -14.56,-52.49 -7.78,-13.52 -24.35,-33.78 -36.57,-36.35 -31.65,-6 -39.7,-0.78 -59.13,11.91 l -18.01,11.26 c -13.12,7.7 -25.6,64.01 -29.34,85.06 4.38,42.96 10.56,117.18 3.3,123.05 -4.39,3.91 -12.74,-2.61 -63.44,-49.7 -30.74,-28.56 -68.98,-64.09 -100.56,-87.96 -45.07,-33.66 -67.46,-75.7 -83.84,-106.44 -7.11,-13.35 -13.27,-24.88 -19.27,-31.58 -8.53,-9.09 -5.27,-21.34 -2.15,-33.13 l 2.84,-12.04 c 5.53,-28.18 22.73,-49.4 39.34,-69.92 l 3.37,-4.17 c 11.22,-15.49 44.61,-56.01 44.93,-56.41 0.81,-0.73 56.85,-53.65 102.47,-14.61 64.27,20.79 94.79,40.35 96,41.18 22.88,11.31 47.88,22.87 60.19,27.48 -20.7,-20.05 -62.1,-74.3 -76.53,-118.36 -14.4,-45.26 -44.27,-117.611 -51.92,-133.002 l -7.83,-15.269 c -15.09,-29.688 -50.35,-99.227 -59.46,-108.868 -11.5,-11.492 -15.96,-19.402 -8.11,-30.711 1.65,-2.429 3.53,-3.691 5.7,-3.91 3.86,-0.308 7.12,2.918 11.22,6.961 3.86,3.828 8.7,8.598 15.17,11.688 9.26,4.832 59.84,51.711 113.31,101.32 42.71,39.57 86.84,80.492 101.19,91.57 29.53,23.012 51.87,45.401 80.14,73.701 l 7.91,7.87 c 0,0 73.97,75.23 85.4,86.84 9.13,-5.74 56.18,-34.52 92.88,-46.18 26.53,-5.22 57.83,-9.22 90.78,0 58.71,17.35 147.76,82.4 212.11,133.88 l 3.74,3 -4.34,1.87 c -11.92,5.13 -25.57,0.65 -42.87,-5 -21.49,-6.96 -48.28,-15.7 -83.32,-9.78 -50.75,7.43 -69.05,14.7 -82.4,20 l -9.22,3.44 c -0.04,0 -5.47,1.74 -5.47,1.74 -34.22,10.6 -69.62,21.7 -90.05,51.34 -0.18,0.53 -0.27,1.05 -0.27,1.65 0,2.36 1,5.75 2.09,9.49 l 2.17,8.91 c 4.66,17.61 18.09,29.57 31.05,41.14 l 11.66,10.83 c 23.99,29.13 46.61,47.92 71.22,59.34 20.48,10.49 52.53,23.61 89.66,26.88 l 22.35,0.21 c 24.34,0.35 47.3,0.7 71.13,-3.08 17.83,-2.35 22.57,-21.23 27.14,-39.49 l 1.21,-4.77 c 0.62,-12.05 -1.95,-27.23 -7.08,-41.1 -4.44,-3.82 -57.1,-9.25 -88.49,-12.52 -43.75,-4.48 -43.78,-4.65 -44.44,-7.65 l -0.26,-1.13 0.83,-1.22 c 12.3,-18.87 53.01,-27.22 76.22,-30.48 65.05,-9.13 153.67,-3.05 183.41,22.78 38.96,35.74 67.83,74.32 83.53,111.58 18.05,47.96 22.04,84.96 12.13,113.35"
style="fill:#fddd04;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path9"
d="m 712.691,1138.68 c -4.218,-0.13 -106.554,-52.31 -179.718,-22.31 -54.871,22.53 -56.582,56.49 -59.414,74.88 -1.864,12.31 8.027,28.09 22.16,42.35 -22.903,-17.22 -43.379,-41.22 -40.723,-58.53 2.801,-18.35 4.52,-52.35 59.395,-74.83 56.238,-23.05 129.707,2.39 162.578,15.53 15.547,14.96 38.465,23 35.722,22.91"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path10"
d="m 789.223,1026.02 c -18.789,-4.34 -46.356,-8.56 -58.817,-6.22 -36.328,6.83 -64.043,31.83 -66.965,66.22 -0.781,9.18 2.872,17.22 8.418,24.13 -13.757,-8.95 -28.476,-22.56 -26.98,-40.3 2.891,-34.36 30.633,-59.36 66.961,-66.18 2.305,-0.43 4.133,-0.91 5.703,-1.39 6.758,2.95 10.977,4.78 10.977,4.78 0,0 24.152,7.56 60.703,18.96"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path11"
d="m 51.7539,239.469 112.6801,7.051 c 2.437,-18.29 7.402,-32.239 14.898,-41.829 12.195,-15.5 29.629,-23.261 52.289,-23.261 16.899,0 29.934,3.961 39.082,11.902 9.152,7.93 13.727,17.117 13.727,27.566 0,9.942 -4.364,18.821 -13.075,26.672 -8.714,7.852 -28.933,15.25 -60.652,22.219 -51.937,11.68 -88.973,27.191 -111.1092,46.531 -22.3086,19.352 -33.461,44.012 -33.461,73.989 0,19.703 5.707,38.3 17.125,55.82 11.4102,17.512 28.5822,31.281 51.5042,41.301 22.914,10.031 54.332,15.039 94.242,15.039 48.976,0 86.312,-9.11 112.019,-27.328 25.711,-18.211 41.004,-47.18 45.887,-86.922 l -111.633,-6.528 c -2.964,17.25 -9.191,29.797 -18.691,37.641 -9.5,7.84 -22.617,11.758 -39.344,11.758 -13.769,0 -24.14,-2.918 -31.113,-8.75 -6.973,-5.852 -10.453,-12.949 -10.453,-21.309 0,-6.101 2.875,-11.59 8.625,-16.472 5.578,-5.059 18.824,-9.758 39.738,-14.11 51.762,-11.168 88.844,-22.449 111.238,-33.859 22.395,-11.418 38.692,-25.578 48.887,-42.481 10.195,-16.918 15.293,-35.82 15.293,-56.73 0,-24.578 -6.797,-47.238 -20.391,-67.981 -13.593,-20.738 -32.589,-36.457 -56.992,-47.187 -24.402,-10.711 -55.16,-16.07 -92.285,-16.07 -65.184,0 -110.32,12.55 -135.4179,37.648 -25.0977,25.09 -39.3086,56.992 -42.6172,95.68"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path12"
d="m 659.059,221.43 101.171,-11.5 c -5.578,-21.098 -14.726,-39.348 -27.453,-54.782 -12.722,-15.418 -28.972,-27.41 -48.75,-35.937 -19.785,-8.539 -44.929,-12.813 -75.422,-12.813 -29.46,0 -53.992,2.75 -73.593,8.223 -19.614,5.481 -36.473,14.367 -50.586,26.649 -14.121,12.269 -25.192,26.679 -33.203,43.222 -8.02,16.539 -12.028,38.477 -12.028,65.828 0,28.551 4.875,52.321 14.637,71.309 7.145,13.922 16.906,26.41 29.281,37.48 12.375,11.051 25.102,19.27 38.172,24.68 20.735,8.531 47.32,12.801 79.735,12.801 45.312,0 79.863,-8.11 103.652,-24.32 23.801,-16.211 40.48,-39.911 50.07,-71.098 l -100.129,-13.34 c -3.133,11.848 -8.847,20.777 -17.129,26.789 -8.281,6.02 -19.382,9.027 -33.328,9.027 -17.601,0 -31.851,-6.308 -42.746,-18.929 -10.89,-12.598 -16.336,-31.699 -16.336,-57.27 0,-22.801 5.399,-40.109 16.211,-51.937 10.801,-11.821 24.57,-17.742 41.305,-17.742 13.937,0 25.656,3.562 35.16,10.71 9.5,7.149 16.602,18.129 21.309,32.95"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path13"
d="m 902.711,250.969 c 0,-24.75 5,-43.047 15.027,-54.899 10.02,-11.859 22.617,-17.781 37.774,-17.781 15.332,0 27.929,5.84 37.785,17.52 9.843,11.671 14.763,30.41 14.763,56.211 0,24.05 -4.97,41.949 -14.9,53.722 -9.933,11.77 -22.219,17.637 -36.855,17.637 -15.52,0 -28.332,-5.969 -38.438,-17.91 -10.109,-11.93 -15.156,-30.098 -15.156,-54.5 z m -106.672,-0.258 c 0,42.348 14.289,77.25 42.883,104.699 28.574,27.449 67.187,41.18 115.808,41.18 55.6,0 97.6,-16.129 126.01,-48.371 22.83,-25.969 34.25,-57.949 34.25,-95.949 0,-42.688 -14.16,-77.68 -42.49,-104.961 -28.32,-27.27 -67.49,-40.911 -117.504,-40.911 -44.621,0 -80.703,11.321 -108.234,33.993 -33.817,28.05 -50.723,64.828 -50.723,110.32"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path14"
d="m 1458.77,112.672 h -99.36 v 44.969 c -14.8,-18.469 -29.76,-31.641 -44.83,-39.481 -15.07,-7.84 -33.59,-11.762 -55.55,-11.762 -29.28,0 -52.25,8.75 -68.89,26.274 -16.64,17.519 -24.96,44.476 -24.96,80.91 v 176.727 h 106.92 v -152.68 c 0,-17.43 3.23,-29.797 9.67,-37.117 6.45,-7.321 15.52,-10.973 27.19,-10.973 12.72,0 23.14,4.871 31.25,14.641 8.09,9.75 12.15,27.269 12.15,52.539 v 133.59 h 106.41 V 112.672"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path15"
d="M 1651.7,495.93 V 390.309 h 58.55 V 312.41 h -58.55 v -98.371 c 0,-11.828 1.13,-19.66 3.4,-23.48 3.47,-5.918 9.57,-8.868 18.3,-8.868 7.83,0 18.81,2.258 32.93,6.778 l 7.85,-73.438 c -26.33,-5.761 -50.9,-8.633 -73.73,-8.633 -26.49,0 -46.02,3.391 -58.56,10.184 -12.55,6.789 -21.83,17.098 -27.85,30.938 -5.99,13.839 -9,36.25 -9,67.23 v 97.66 h -39.22 v 77.899 h 39.22 v 50.98 l 106.66,54.641"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path16"
d="m 1764.88,390.309 h 106.41 V 112.672 h -106.41 z m 0,105.621 h 106.41 v -72.418 h -106.41 v 72.418"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path17"
d="m 1939.26,390.309 h 99.08 v -45.227 c 14.8,18.469 29.8,31.668 44.96,39.609 15.18,7.93 33.63,11.899 55.43,11.899 29.45,0 52.5,-8.758 69.14,-26.281 16.66,-17.508 24.98,-44.567 24.98,-81.168 V 112.672 h -106.93 v 152.68 c 0,17.418 -3.22,29.75 -9.67,36.988 -6.45,7.23 -15.51,10.851 -27.19,10.851 -12.91,0 -23.36,-4.882 -31.36,-14.64 -8.03,-9.762 -12.04,-27.281 -12.04,-52.551 V 112.672 h -106.4 v 277.637"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
<path
id="path18"
d="m 2389.43,255.672 c 0,-20.043 4.28,-34.891 12.82,-44.57 8.53,-9.672 19.78,-14.512 33.73,-14.512 13.24,0 24.35,5.012 33.32,15.039 8.98,10.019 13.47,25.133 13.47,45.351 0,20.211 -4.7,35.68 -14.12,46.399 -9.41,10.73 -20.92,16.082 -34.51,16.082 -13.59,0 -24.43,-4.922 -32.54,-14.77 -8.1,-9.851 -12.17,-26.191 -12.17,-49.019 z m 100.14,134.637 h 99.61 V 128.09 l 0.25,-12.281 c 0,-17.4301 -3.71,-34.0277 -11.11,-49.809 -7.4,-15.7695 -17.25,-28.5312 -29.53,-38.2891 C 2536.48,17.9492 2520.9,10.8906 2501.99,6.53125 2483.07,2.17188 2461.43,0 2437.01,0 c -55.76,0 -94.06,8.37109 -114.88,25.0898 -20.84,16.7305 -31.25,39.129 -31.25,67.1915 0,3.4882 0.17,8.1877 0.53,14.1167 l 103.26,-11.7691 c 2.62,-9.5781 6.62,-16.207 12.03,-19.8672 7.83,-5.4023 17.68,-8.1015 29.53,-8.1015 15.33,0 26.8,4.1015 34.38,12.289 7.59,8.1914 11.38,22.4808 11.38,42.8708 v 42.09 c -10.47,-12.371 -20.92,-21.351 -31.38,-26.918 -16.37,-8.722 -34.07,-13.082 -53.07,-13.082 -37.11,0 -67.09,16.211 -89.92,48.629 -16.21,23 -24.32,53.41 -24.32,91.242 0,43.219 10.45,76.16 31.37,98.821 20.92,22.648 48.28,33.988 82.09,33.988 21.6,0 39.43,-3.66 53.45,-10.988 14.05,-7.313 27.15,-19.43 39.36,-36.34 v 41.047"
style="fill:#ed1c24;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,329.84)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="451.00259" height="507.332" viewBox="0 0 451.00259 507.332" id="svg2">
<defs id="defs4"/>
<metadata id="metadata7">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-197.35585,-58.696183)" id="layer1">
<g transform="translate(28.601602,6.2831826)" id="g2986">
<g id="g2988">
<g id="g2990">
<path d="m 284.953,425.625 c -41.441,-0.008 -74.219,-15.391 -94.77,-44.48 -23.637,-33.461 -28.066,-83.176 -11.559,-129.75 21.262,-60.016 68.266,-89.355 68.742,-89.641 l 2.801,-1.703 4.555,-14.98 21.422,4.02 14.844,29.734 -0.773,5.273 c -0.738,4.98 -1.48,9.875 -2.215,14.695 l -0.191,1.238 c -5.277,34.695 -9.832,64.66 -6.102,82.488 2.242,10.711 7.199,17.898 15.895,22.906 2.984,-3.594 5.02,-5.305 7.535,-5.305 h 0.812 l 0.98,0.34 c 3.582,1.434 4.047,5.508 3.922,10.246 0.348,0.098 0.703,0.199 1.07,0.289 6.363,1.621 11.062,5.375 13.602,10.852 4.191,9.082 0.234,17.125 -3.254,24.215 -2.578,5.238 -6.379,12.953 -8.754,23.418 3.586,6.008 4.141,11.75 3.934,15.641 -0.297,5.516 -2.406,11.066 -5.996,15.848 1.102,4.875 0.484,9.473 -1.844,13.688 -5.285,9.566 -15.68,10.465 -19.094,10.754 -1.574,0.137 -3.34,0.215 -5.262,0.215 h -0.3 z" id="path2992" style="fill:#260859"/>
</g>
<g id="g2994">
<path d="m 506.766,425.695 c -6.77,0 -12.57,-2.73 -16.773,-7.902 -8.246,-10.133 -6.812,-26.621 -3.918,-41.938 -3.145,-2.297 -7.34,-5.594 -9.633,-10.934 l -5.355,-12.438 11.875,-6.527 c 24.914,-13.723 34.227,-39.723 27.668,-77.289 -2.957,-4.148 -7.086,-11.844 -10.824,-25.562 -2.727,-10.008 -13.16,-72.691 -7.508,-83.227 1.012,-1.898 5.035,-8.109 14.039,-8.109 5.695,0 19.23,3.664 28.004,12.398 0.246,-0.012 0.488,-0.016 0.734,-0.016 5.488,0 9.648,2.668 13.672,5.242 l 0.477,0.301 c 1.07,0.68 2.219,1.41 3.492,2.164 2.246,1.336 6.668,3.961 10.598,9.719 6.301,1.035 11.359,5.949 20.273,15.086 0.207,0.219 0.996,1.027 1.203,1.227 2.219,2.195 3.977,5.051 5.602,9.125 11.438,4.383 18.34,21.609 19.629,27.852 l 0.508,1.938 c 15.383,58.23 11.477,106.727 -11.297,140.25 -18.512,27.266 -48.637,43.574 -89.535,48.465 -1.045,0.117 -1.994,0.175 -2.931,0.175 l 0,0 z" id="path2996" style="fill:#260859"/>
</g>
</g>
<path d="M 382.699,555.371 C 362.824,548.375 188.613,484.344 188.613,390.375 l 0.012,-281.641 c 0,-25.367 21.531,-46.004 47.996,-46.004 16.855,0 32.289,8.32 40.957,21.902 31.453,-3.801 73.332,-11.934 99.66,-23.809 l 2.559,-1.129 15.438,-7.281 16.172,7.629 c 26.121,12.301 69,20.699 101.461,24.602 8.672,-13.59 24.102,-21.914 40.949,-21.914 26.473,0 48.008,20.551 48.008,45.812 l -0.027,281.832 c 0,94.301 -174.176,158.012 -194.039,164.965 l -12.523,4.406 -12.537,-4.374 z" id="path2998" style="fill:#260859"/>
<path d="M 389.637,535.469 C 371.207,528.989 209.688,469.871 209.688,390.375 L 209.7,108.734 c 0,-13.93 12.086,-24.93 26.922,-24.93 14.336,0 26.137,10.309 26.867,23.469 l 0.004,0.012 c 35.363,-3.055 88.648,-12.023 122.41,-27.25 l 2.426,-1.07 6.906,-3.254 7.176,3.387 c 33.441,15.754 87.988,25.031 124.539,28.188 h 0.004 c 0.73,-13.168 12.527,-23.48 26.863,-23.48 14.852,0 26.934,11.098 26.934,24.738 l -0.027,281.832 c 0,79.676 -161.52,138.625 -179.938,145.082 l -5.57,1.961 -5.579,-1.95 z" id="path3000" style="fill:#7c3a00"/>
<path d="m 395.156,112.82 -1.527,0.621 c -37.711,15.406 -92.113,24.406 -127.242,27.43 l -22.969,1.984 -0.008,130.859 H 395.156 V 112.82 z" id="path3002" style="fill:#ffef6f"/>
<path d="m 395.223,75.715 -6.895,3.25 -2.426,1.07 c -33.762,15.227 -87.047,24.195 -122.41,27.25 l -0.004,-0.012 c -0.73,-13.16 -12.531,-23.469 -26.867,-23.469 -14.836,0 -26.922,11 -26.922,24.93 l -0.012,281.641 c 0,79.496 161.52,138.613 179.949,145.094 l 5.578,1.949 0.008,-0.004 V 75.715 z" id="path3004" style="fill:#e8941a"/>
<g id="g3006">
<path d="m 553.816,100.664 c -5.395,0 -9.801,3.312 -10.035,7.539 l -0.941,17.375 -17.34,-1.5 c -38.246,-3.301 -94.871,-13.051 -130.277,-29.734 -0.773,0.363 -1.609,0.707 -2.402,1.062 0.016,-0.004 0.031,-0.012 0.043,-0.016 v 307.098 l 171.008,-126.707 0.02,-167.238 c -0.001,-4.27 -4.615,-7.879 -10.076,-7.879 z" id="path3008" style="fill:#e8941a"/>
</g>
<path d="M 392.863,397.402 V 95.391 c -35.527,16.031 -90.5,25.457 -127.926,28.688 l -17.336,1.5 -0.941,-17.367 c -0.234,-4.234 -4.645,-7.547 -10.039,-7.547 -5.766,0 -10.082,3.891 -10.09,7.219 l 0.027,0.852 -0.004,152.738 166.309,135.928 z" id="path3010" style="fill:#ffef6f"/>
<path d="m 525.5,124.078 c -38.246,-3.301 -94.871,-13.051 -130.277,-29.734 -0.492,0.23 -1.031,0.449 -1.527,0.676 v 41.531 l 30.055,-1.555 113.195,21.496 10.082,-13.633 -4.188,-17.281 -17.34,-1.5 z" id="path3012" style="fill:#e8941a"/>
<path d="m 393.695,95.02 c -35.492,16.262 -91.043,25.805 -128.758,29.059 l -17.336,1.5 -4.184,17.277 19.438,6.918 102.445,-11.754 28.395,-1.469 V 95.02 z" id="path3014" style="fill:#ffffff"/>
<path d="m 226.555,243.211 -0.008,147.652 c 0,55.215 105.336,106.918 168.656,129.176 63.324,-22.238 168.66,-73.918 168.66,-129.176 l 0.016,-147.652 H 226.555 z" id="path3016" style="fill:#ffd26c"/>
<path d="M 393.746,508.047 C 318.914,480.473 243.406,435.543 243.406,397.313 l 0.012,-254.457 22.969,-1.984 c 35.129,-3.023 89.531,-12.023 127.242,-27.43 l 1.594,-0.648 1.59,0.648 c 37.711,15.406 92.109,24.406 127.242,27.43 l 22.973,1.988 -0.023,254.453 c 0,38.266 -75.508,83.188 -150.344,110.734 l -1.457,0.535 -1.458,-0.535 z" id="path3018" style="fill:#ffffff"/>
<path d="m 524.055,140.871 c -35.133,-3.023 -89.531,-12.023 -127.242,-27.43 l -1.59,-0.648 -1.594,0.648 1.527,-0.621 v 160.895 h 151.859 l 0.012,-130.855 -22.972,-1.989 z" id="path3020" style="fill:#ffd26c"/>
<path d="m 395.156,112.82 -1.527,0.621 c -37.711,15.406 -92.113,24.406 -127.242,27.43 l -22.969,1.984 -0.008,130.859 H 395.156 V 112.82 z" id="path3022" style="fill:#ffef6f"/>
<linearGradient x1="395.21631" y1="132.92529" x2="395.21631" y2="491.23761" id="SVGID_1_" gradientUnits="userSpaceOnUse">
<stop id="stop3025" style="stop-color:#e8941a;stop-opacity:1" offset="0"/>
<stop id="stop3027" style="stop-color:#ffef6f;stop-opacity:1" offset="1"/>
</linearGradient>
<path d="M 395.203,494.371 C 315.765,464.609 256.051,422.691 256.051,396.586 l 0.012,-242.137 11.41,-0.984 c 34.82,-2.996 88.879,-11.863 127.75,-27.043 38.859,15.18 92.914,24.047 127.742,27.043 l 11.418,0.988 -0.023,242.133 c -10e-4,26.133 -59.715,68.051 -139.157,97.785 l 0,0 z" id="path3029" style="fill:url(#SVGID_1_)"/>
<path d="m 534.359,377.328 c 0,26.133 -59.715,68.051 -139.156,97.785 C 315.777,445.355 256.07,403.449 256.051,377.34 v 23.805 c 0,26.102 59.715,68.02 139.152,97.785 79.441,-29.734 139.156,-71.652 139.156,-97.785 l 0.023,-246.691 0,0 -0.023,222.874 z" id="path3031" style="fill:#260859"/>
<path d="m 395.156,126.445 c -38.867,15.164 -92.883,24.023 -127.684,27.02 l -11.41,0.984 v 16.746 l 10.133,-0.867 c 35.152,-2.996 89.707,-11.855 128.961,-27.02 v -16.863 z" id="path3033" style="fill:#a84d10"/>
<path d="m 522.965,153.465 c -34.828,-2.996 -88.883,-11.863 -127.742,-27.043 -0.223,0.086 -0.457,0.164 -0.68,0.25 0.199,-0.074 0.41,-0.148 0.613,-0.227 v 16.863 c 0.02,-0.008 0.043,-0.016 0.066,-0.023 39.25,15.18 93.844,24.047 129.023,27.043 l 10.137,0.871 v -16.746 l -11.417,-0.988 z" id="path3035" style="fill:#a84d10"/>
<path d="m 256.062,158.086 -0.012,238.5 c 0,7.613 5.105,16.57 14.262,26.176 1.164,0.109 2.312,0.242 3.492,0.328 0.074,1.215 0.168,2.344 0.277,3.438 23.133,22.137 66.586,47.148 118.852,66.977 l -0.059,-1.766 c -0.59,-18.352 -2.348,-35.594 -4.691,-46.121 -5.438,-24.406 -22.613,-48.82 -37.359,-62.367 -6.184,-15.457 -1.742,-25.371 4.391,-39.07 1.871,-4.168 3.992,-8.887 5.699,-13.855 5.145,-14.93 4.266,-38.211 -1.984,-53.98 -2.898,-9.637 -9.184,-16.973 -16.832,-19.625 -2.328,-0.777 -5.027,-1.312 -8.23,-1.625 -3.215,-10.938 -1.609,-22.059 -1.598,-22.148 l 1.754,-10.398 -9.758,-3.961 c -0.977,-0.398 -9.941,-3.902 -23.02,-3.902 -6.027,0 -12.105,0.785 -18.117,2.34 0.82,-5.617 1.695,-11.391 2.598,-17.309 0,0 1.664,-10.949 2.402,-15.922 l 0.676,-4.617 -15.496,-26.328 c 0,0 -3.242,0.398 -5.836,0.617 l -11.411,4.618 z" id="path3037" style="fill:#260859"/>
<path d="m 514.695,428.105 c 0.016,-1.758 0.035,-3.516 -0.082,-5.402 2.242,-0.328 4.438,-0.699 6.609,-1.102 8.461,-9.164 13.137,-17.707 13.137,-25.016 l -1.562,-226.52 c 0,0 2.516,-0.133 2.234,-0.102 -3.75,-10.566 -23.34,-16.086 -28.695,-16.086 -7.617,0 -11.059,4.895 -12.184,6.996 -5.09,9.496 4.488,69.965 7.688,81.676 0.309,1.141 0.621,2.191 0.938,3.234 l -1.672,8.418 c -14.316,-13.652 -32.395,-28.645 -42.766,-28.645 -0.656,0 -1.289,0.055 -1.898,0.156 -2.648,0.461 -4.789,1.961 -6.016,4.215 -1.777,3.258 -1.703,7.801 0.27,14.133 -5.043,-1.027 -11.129,-1.98 -16.34,-1.98 -6.387,0 -10.586,1.453 -12.836,4.434 -4.473,5.934 -2.328,15.238 6.367,27.66 2.562,3.668 5.789,7.285 8.969,10.848 0.66,0.746 1.328,1.492 1.992,2.242 -9.152,14.402 -11.535,36.027 -6.352,58.285 2.27,9.738 5.41,16.656 8.188,22.77 1.766,3.883 3.203,7.047 4.09,10.273 -21.77,18.578 -37.285,42.938 -42.648,67.016 -2.184,9.785 -3.836,25.16 -4.547,42.199 l -0.23,5.742 c 51.096,-19.35 93.811,-43.674 117.346,-65.444 z" id="path3039" style="fill:#260859"/>
<g id="g3041">
<path d="m 321.699,323.621 c -1.984,-4.289 -5.723,-7.242 -10.809,-8.539 -19.477,-4.91 -29.746,-14.688 -33.309,-31.699 -3.887,-18.574 0.723,-48.887 6.062,-83.988 l 0.188,-1.234 c 0.734,-4.816 1.477,-9.707 2.211,-14.672 l 0.582,-3.965 -13.316,-26.676 -15.652,-2.938 -3.922,12.906 -4.168,2.527 c -0.457,0.281 -46.105,28.566 -66.969,87.457 -16.059,45.312 -11.832,93.551 11.023,125.91 19.73,27.922 51.32,42.691 91.348,42.699 1.902,0.008 3.645,-0.066 5.184,-0.195 4.969,-0.43 12.004,-1.789 15.77,-8.598 2.121,-3.844 2.418,-7.98 0.891,-12.609 3.871,-4.461 6.141,-9.797 6.426,-15.113 0.195,-3.613 -0.395,-9.031 -4.211,-14.613 2.449,-11.852 6.652,-20.379 9.449,-26.062 3.375,-6.86 6.57,-13.344 3.222,-20.598 z" id="path3043" style="fill:#260859"/>
<g id="g3045">
<path d="m 199.93,259.562 c 14.34,-66.531 55.953,-77.508 55.953,-77.508 l -1.012,36.066 0.816,76.336 c 0,0 -2.602,60.848 30.262,73.891 32.863,13.039 6.941,42.215 -36.23,31.109 C 208.5,389.602 184.465,330.16 199.93,259.562 z" id="path3047" style="fill:#ffffff"/>
<g id="g3049">
<path d="m 228.879,317.406 c -10.066,-45.742 11.844,-91.457 22.941,-105.285 l 13.945,-45.91 7.914,15.453 c -4.523,30.586 -9.375,58.598 -9.957,80.766 -0.121,4.418 0.922,20.125 1.629,23.516 3.336,15.93 11.039,27.066 24.359,34.395 2.242,1.234 13.094,-16.984 15.656,-15.965 3.352,1.344 -1.461,21.836 2.469,22.828 9.133,2.305 -6.898,13.684 -11.938,45.445 -21.198,1.91 -56.975,-9.61 -67.018,-55.243 z" id="path3051" style="fill:#f26531"/>
</g>
<path d="m 194.371,256.984 c 19.648,-55.469 61.68,-80.957 61.68,-80.957 l -0.434,15.422 c -15.742,12.766 -62.27,71.184 -44.375,141.27 13.469,48.656 52.797,63.223 80.523,57.949 -4.102,9.059 14.18,18.254 -6.789,18.254 -91.675,-0.02 -114.8,-83.645 -90.605,-151.938 z" id="path3053" style="fill:#7ac143"/>
</g>
<polygon points="213.434,210.594 205.309,222.129 239.594,232.855 " id="polygon3055" style="fill:#260859"/>
<polygon points="191.312,240.535 186.168,257.379 230.988,256.758 " id="polygon3057" style="fill:#260859"/>
<polygon points="183.648,279.039 183.082,296.645 226.18,284.332 " id="polygon3059" style="fill:#260859"/>
<polygon points="180.789,326.434 186,346.004 229.383,318.148 " id="polygon3061" style="fill:#260859"/>
<polygon points="201.324,373.969 214.367,389.453 241.773,345.785 " id="polygon3063" style="fill:#260859"/>
<polygon points="241.258,405.695 259.137,409.742 267.953,367.039 " id="polygon3065" style="fill:#260859"/>
</g>
<path d="m 324.59,268.086 c 0.148,0.016 0.281,0.008 0.438,0.023 -9.18,-17.043 -5.715,-37.352 -5.715,-37.352 0,0 -16.691,-6.77 -37.332,0.254 -24.391,8.301 -27.098,26.691 -24.246,32.094 2.688,5.09 9.637,6.027 15.934,2.34 3.547,9.219 8,15.848 18.32,24.191 l 34.66,-20.137 c -10e-4,10e-4 -0.837,-0.394 -2.059,-1.413 z" id="path3067" style="fill:#fff200"/>
<path d="m 274.98,347.574 c -5.398,0 -8.523,-1.82 -9.355,-2.379 l -9.891,-6.449 12.168,-6.41 c 5.203,-1.285 10.617,-7.621 15.832,-14.719 -1.098,0.18 -2.164,0.363 -3.195,0.535 -6.023,1.031 -11.086,1.895 -14.895,1.895 -0.688,0 -1.328,-0.031 -1.918,-0.082 -0.203,-0.02 -0.395,-0.027 -0.59,-0.027 -4.445,0 -10.258,3.727 -12.125,5.164 l -6.609,5.445 -4.531,-8.77 c -0.234,-0.688 -2.207,-6.969 1.93,-14.664 3.105,-5.758 12.41,-13.875 23.41,-13.875 1.82,0 3.633,0.219 5.387,0.652 0.594,0.148 1.184,0.223 1.734,0.223 2.828,0 5.781,-1.98 8.781,-5.883 0.738,-1.004 1.242,-1.891 1.648,-2.598 0.473,-0.82 0.875,-1.5 1.395,-2.191 1.762,-2.109 4.008,-3.098 6.93,-3.098 5.004,0 11.262,2.648 13.82,10.074 l 0,0 c 0.008,0 0.719,1.465 4.141,4.703 3.637,0.766 6.613,2.043 8.863,3.809 3.297,2.598 5.059,6.297 4.953,10.43 -0.207,8.285 -7.871,17.668 -23.434,28.684 -11.148,7.875 -19.449,9.531 -24.449,9.531 l 0,0 z" id="path3069" style="fill:#260859"/>
<path d="m 285.051,298.215 c -8.113,-6.562 -13.383,-12.48 -17.387,-19.559 -0.559,0.035 -1.121,0.059 -1.676,0.059 -7.941,0 -14.508,-3.812 -18.008,-10.461 -2.953,-5.582 -2.887,-13.828 0.176,-21.527 2.715,-6.844 10.266,-19.348 30.266,-26.16 7.363,-2.504 15.043,-3.773 22.824,-3.773 12.699,0 21.285,3.363 22.227,3.746 l 8.191,3.324 -1.473,8.734 c -0.02,0.137 -2.246,15.293 3.441,28.035 l 16.324,8.004 -17.734,10.391 -41.266,23.961 -5.905,-4.774 z" id="path3071" style="fill:#260859"/>
<path d="m 313.297,327.484 c -4.66,0 -5.777,-4.406 -6.758,-8.293 -0.754,-2.98 -1.535,-6.062 -3.594,-8.117 -7.707,-7.703 -18.688,-12.191 -18.801,-12.238 l -1.008,-0.406 -0.25,-1.062 c -1.91,-8.031 -0.074,-15.875 5.312,-22.695 8.305,-10.508 25,-17.848 40.602,-17.848 4.613,0 8.863,0.637 12.629,1.895 11.438,3.969 18.797,19.078 16.785,34.398 -2.59,19.672 -19.195,32.516 -44.418,34.352 l -0.499,0.014 z" id="path3073" style="fill:#260859"/>
<path d="m 390.77,491.805 c -0.586,-18.211 -2.32,-35.305 -4.645,-45.73 -5.391,-24.207 -22.441,-48.285 -37.074,-61.586 -6.785,-16.547 -1.922,-27.41 4.227,-41.141 1.859,-4.137 3.961,-8.82 5.645,-13.711 6.371,-18.496 3.301,-52.016 -9.367,-64.766 l -0.875,-0.883 -17.277,4.875 -0.285,1.207 c -3.629,15.34 -12.52,35.328 -19.969,40.625 l -5.223,3.711 0.672,6.355 c 1.371,13.16 -5.613,28.898 -23.094,64.328 -6.949,14.074 -9.164,28.023 -7.043,43.664 23.328,21.27 64.762,44.902 114.336,63.93 l -0.028,-0.878 z" id="path3075" style="fill:#260859"/>
<path d="m 286.379,296.578 c -8.293,-6.707 -13.605,-12.801 -17.535,-20.16 -0.957,0.125 -1.914,0.188 -2.855,0.188 -7.129,0 -13.016,-3.398 -16.145,-9.336 -2.672,-5.051 -2.57,-12.621 0.27,-19.766 2.586,-6.512 9.793,-18.406 28.988,-24.941 7.148,-2.434 14.594,-3.664 22.145,-3.664 12.223,0 20.52,3.219 21.426,3.59 l 6.637,2.691 -1.195,7.066 c -0.023,0.141 -2.43,16.492 3.918,29.945 l 13.477,6.609 -14.352,8.406 -40,23.23 -4.779,-3.858 z" id="path3077" style="fill:#260859"/>
<path d="m 324.59,268.086 c 0.148,0.016 0.281,0.008 0.438,0.023 -9.18,-17.043 -5.715,-37.352 -5.715,-37.352 0,0 -16.691,-6.77 -37.332,0.254 -24.391,8.301 -27.098,26.691 -24.246,32.094 2.688,5.09 9.637,6.027 15.934,2.34 3.547,9.219 8,15.848 18.32,24.191 l 34.66,-20.137 c -10e-4,10e-4 -0.837,-0.394 -2.059,-1.413 z" id="path3079" style="fill:#fff200"/>
<path d="m 291.426,228.664 c -3.023,0.5 -6.176,1.234 -9.445,2.348 -1.793,0.609 -3.449,1.281 -5.012,1.988 0.133,0.762 0.27,1.52 0.402,2.305 2.273,13.105 5.395,30.973 23.766,49.016 l 3.77,-2.191 4.488,-4.691 c -0.477,-0.453 -0.945,-0.914 -1.398,-1.371 -15.052,-15.072 -17.841,-27.463 -16.571,-47.404 z" id="path3081" style="fill:#f26531"/>
<path d="m 319.312,230.758 c 0,0 -4.73,-1.898 -12.234,-2.641 -2.945,19.641 0.68,32.223 5.871,43.66 2.867,-2.016 6.352,-4.246 12.078,-3.668 -9.179,-17.043 -5.715,-37.351 -5.715,-37.351 z" id="path3083" style="fill:#f26531"/>
<path d="m 340.762,260.719 c -22.492,-7.508 -62.18,9.445 -55.824,36.164 0,0 11.375,4.582 19.496,12.699 5.488,5.477 3.191,16.223 9.211,15.781 55.078,-4.008 48.554,-57.203 27.117,-64.644 z" id="path3085" style="fill:#260859"/>
<path d="m 384.066,446.531 c -5.324,-23.91 -22.367,-47.836 -36.781,-60.797 -7.402,-17.637 -2.094,-29.484 4.07,-43.25 1.84,-4.102 3.926,-8.746 5.574,-13.535 6.051,-17.562 3.086,-50.559 -8.871,-62.594 l -14.891,4.199 c -3.703,15.652 -12.715,36.105 -20.797,41.855 l -4.219,3 0.543,5.133 c 1.426,13.711 -5.629,29.66 -23.301,65.484 -7.051,14.281 -9.199,28.594 -6.531,44.879 23.355,20.395 62.828,42.727 109.797,60.965 -0.671,-20.983 -2.706,-36.886 -4.593,-45.339 z" id="path3087" style="fill:#260859"/>
<path d="m 375.43,448.457 c -4.688,-21.027 -20.555,-44.578 -35.52,-57.488 -0.02,-0.051 -0.031,-0.094 -0.059,-0.145 -12.793,-28.352 1.895,-44.961 8.711,-64.754 5.188,-15.062 2.301,-44.332 -6.785,-53.473 -2.863,12.109 -12.199,38.438 -24.281,47.027 1.711,16.387 -6.383,34.281 -24.168,70.316 -7.707,15.625 -8.574,31.184 -3.348,49.906 22.02,16.477 53.324,33.613 89.688,48.441 -0.727,-17.475 -2.406,-31.607 -4.238,-39.83 z" id="path3089" style="fill:#f26531"/>
<path d="m 336.758,268.195 c -10.891,-5.066 -39.801,0.785 -49.402,24.723 0,0 8.891,5.105 14.688,12.75 3.91,5.168 3.672,14.082 8.707,14.211 42.866,1.105 42.554,-43.981 26.007,-51.684 z" id="path3091" style="fill:#f26531"/>
<path d="m 272.738,340.402 c -1.93,0 -3.652,-0.895 -4.738,-2.457 -1.965,-2.816 -0.754,-6.086 -0.109,-7.84 3.441,-9.297 12.891,-18.176 21.5,-20.219 l 16.016,-4.246 1.781,0.863 c 4.453,2.152 2.496,-3.094 5.336,-1.969 11.52,4.566 5.867,13.898 4.336,16.051 -3.465,4.875 -7.785,4.871 -10.066,5.266 -9.285,1.57 -14.492,4.105 -18.293,5.965 -1.504,0.734 -2.801,1.367 -4.184,1.898 -0.977,0.379 -3.066,2.02 -4.32,3 -2.829,2.224 -4.7,3.688 -7.259,3.688 l 0,0 z" id="path3093" style="fill:#260859"/>
<path d="m 304.859,311.324 -14.234,3.773 c -6.875,1.625 -14.824,9.062 -17.707,16.859 -2.891,7.805 4.785,-1.43 9.469,-3.234 4.668,-1.812 10.121,-5.887 23.5,-8.156 13.379,-2.265 -1.028,-9.242 -1.028,-9.242 z" id="path3095" style="fill:#f26531"/>
<path d="m 290.859,310.074 c 0,0 -11.062,22.945 -25.168,28.18 l 10.75,1.375 c 0,0 15.59,-9.383 18.441,-19.707 2.852,-10.312 2.551,-15.832 2.551,-15.832 l -6.574,5.984 z" id="path3097" style="fill:#260859"/>
<path d="m 313.492,304.547 c -6.148,-4.84 -21.82,-2.699 -21.82,-2.699 l 8.758,5.172 c -8.391,9.762 -18.191,29.141 -30.828,32.25 0,0 8.082,5.398 25.703,-7.062 17.621,-12.46 24.347,-22.825 18.187,-27.661 z" id="path3099" style="fill:#fff200"/>
<path d="m 289.879,287.703 c -0.773,1.035 -1.34,2.477 -3.102,4.871 -2.613,3.402 -8.828,10.496 -17.902,8.246 -9.074,-2.25 -18.012,4.523 -20.785,9.676 -2.77,5.152 -1.465,8.965 -1.465,8.965 0,0 9.391,-7.359 17.73,-6.609 8.348,0.758 32.059,-6.914 50.258,-4.016 0,0 -14.355,-10.16 -16.457,-16.086 -2.011,-5.848 -7.902,-5.488 -8.277,-5.047 z" id="path3101" style="fill:#fff200"/>
<g id="g3103">
<path d="m 318.129,275.305 c -8.238,10.934 -12.203,12.973 -19.973,17.445 18.227,33.57 52.637,-9.355 19.973,-17.445 z" id="path3105" style="fill:#260859"/>
</g>
<g id="g3107">
<path d="m 315.082,300.051 c -3.988,0 -7.168,-2.73 -9.383,-5.508 5.203,-3.199 9.039,-6.363 14.363,-13.004 4.559,1.859 6.637,4.664 6.188,8.344 -0.574,4.703 -5.578,10.168 -11.168,10.168 l 0,0 z" id="path3109" style="fill:#ffffff"/>
</g>
<path d="m 317.875,282.395 c -2.621,1.168 -5.152,2.633 -7.699,3.727 -1.414,0.609 -3.875,2.562 -3.633,3.16 1.148,2.824 3.023,5.551 6.082,6.75 0.297,0.117 0.602,0.211 0.906,0.289 0.695,-1.246 1.336,-2.582 1.898,-4.008 1.118,-2.86 2.009,-6.751 2.446,-9.918 z" id="path3111" style="fill:#260859"/>
<path d="m 313.707,296.379 c 0,-2.535 -0.289,-4.934 -0.781,-7.129 -2.305,2.059 -4.594,3.672 -7.227,5.293 1.91,2.398 4.551,4.75 7.801,5.352 0.125,-1.137 0.207,-2.305 0.207,-3.516 z" id="path3113" style="fill:#f37344"/>
<path d="m 280.828,251.062 c 0.438,9.141 4.355,19.59 12.043,29.02 l 4.793,-4.211 c -11.609,-7.101 -16.836,-24.809 -16.836,-24.809 z" id="path3115" style="fill:#260859"/>
<path d="m 291.141,238.789 c -1.156,10.66 1.484,23.461 8.672,35.766 l 6.305,-4 c -12.141,-10.336 -14.977,-31.766 -14.977,-31.766 z" id="path3117" style="fill:#260859"/>
<path d="m 306.738,231.996 c -2.785,10.348 -2.152,23.402 3.047,36.672 l 6.844,-2.973 c -10.399,-12.082 -9.891,-33.699 -9.891,-33.699 z" id="path3119" style="fill:#260859"/>
<path d="m 334.832,367.363 c 0.266,-6.996 2.133,-13.312 4.527,-19.387 0.395,-1.004 -7.34,5.734 -15.191,7.973 -7.262,2.07 -14.66,-0.41 -14.953,0.293 -2.434,5.727 -5.387,12.094 -8.852,19.297 19.012,1.656 27.176,-2.223 34.469,-8.176 z" id="path3121" style="fill:#7ac143"/>
<path d="m 337.445,353.391 c 1.25,-3.918 2.812,-7.699 4.465,-11.438 -12.93,7.711 -20.523,10.559 -30.961,10.07 -1.262,3.164 -2.719,6.547 -4.34,10.152 13.114,1.403 24.301,-3.511 30.836,-8.784 z" id="path3123" style="fill:#ffffff"/>
<path d="m 293.305,389.996 c -7.684,15.605 -8.547,31.148 -3.324,49.852 12.047,9.012 26.852,18.227 43.773,27.152 12.125,-36.82 -2.57,-65.309 -8.16,-75.688 -8.168,2.25 -33.199,11.153 -32.289,-1.316 z" id="path3125" style="fill:#ffffff"/>
<g id="g3127">
<path d="m 426.086,226.523 c -3.566,0 -6.266,-1.336 -7.719,-2.051 -0.879,-0.445 -2,-1.012 -7.145,-4.84 -5.227,1.496 -10.617,2.254 -16.043,2.254 -5.52,0 -10.984,-0.777 -16.266,-2.309 -5.227,3.891 -6.375,4.465 -7.148,4.855 -1.371,0.684 -4.199,2.09 -7.797,2.09 -4.008,0 -7.691,-1.621 -10.949,-4.805 -4.895,-4.824 -6.535,-9.34 -5.664,-15.602 0.137,-0.996 0.441,-2.527 1.691,-6.785 -1.613,-2.074 -3.102,-4.285 -4.441,-6.59 -9.844,-3.238 -12.664,-4.578 -14.113,-5.461 -4.582,-2.82 -6.859,-8.195 -6.98,-16.441 -0.117,-8.219 1.965,-13.617 6.363,-16.496 0.707,-0.461 1.859,-1.211 8.289,-4.105 0.246,-1.074 0.527,-2.141 0.832,-3.195 -5.164,-7.422 -5.906,-9.02 -6.309,-9.887 -1.41,-3.07 -2.191,-8.109 2.059,-14.746 1.551,-2.414 7.227,-10.281 15.102,-10.281 h 0.797 l 1.406,0.211 c 0.941,0.184 2.262,0.449 9.582,3.125 4.242,-2.965 8.844,-5.352 13.707,-7.109 2.277,-5.695 3.047,-6.969 3.422,-7.594 2.812,-4.617 8.301,-6.957 16.328,-6.957 8.25,0 13.824,2.352 16.57,6.988 0.398,0.676 1.055,1.781 3.363,7.566 4.855,1.758 9.449,4.141 13.676,7.098 7.227,-2.648 8.523,-2.91 9.391,-3.09 l 0.734,-0.148 1.395,-0.086 c 7.91,0 13.363,7.188 15.348,10.277 4.25,6.629 3.473,11.66 2.07,14.715 -0.418,0.906 -1.176,2.527 -6.289,9.871 0.312,1.074 0.594,2.156 0.844,3.246 6.473,2.906 7.566,3.625 8.297,4.105 4.398,2.875 6.48,8.273 6.359,16.496 -0.113,8.125 -2.453,13.648 -6.957,16.414 -1.016,0.625 -2.891,1.785 -14.141,5.484 -1.391,2.406 -2.957,4.711 -4.656,6.879 1.348,4.625 1.508,5.785 1.605,6.496 0.871,6.266 -0.762,10.777 -5.645,15.582 -3.269,3.201 -6.956,4.826 -10.968,4.826 l 0,0 z" id="path3129" style="fill:#260859"/>
<path d="m 426.086,224.414 c -3.094,0 -5.512,-1.203 -6.812,-1.844 -0.746,-0.379 -1.895,-0.953 -7.633,-5.262 -5.348,1.641 -10.875,2.473 -16.461,2.473 -5.676,0 -11.273,-0.848 -16.676,-2.527 -5.824,4.367 -6.977,4.941 -7.691,5.301 -1.324,0.656 -3.75,1.859 -6.844,1.859 -3.438,0 -6.621,-1.41 -9.477,-4.203 -4.41,-4.344 -5.828,-8.211 -5.051,-13.801 0.137,-1 0.48,-2.645 1.938,-7.531 -1.992,-2.461 -3.781,-5.105 -5.344,-7.887 -10.289,-3.363 -13.133,-4.711 -14.441,-5.512 -3.922,-2.41 -5.867,-7.207 -5.973,-14.668 -0.109,-7.441 1.664,-12.254 5.41,-14.707 0.672,-0.438 1.848,-1.199 8.965,-4.375 0.359,-1.711 0.801,-3.402 1.309,-5.059 -5.598,-8.008 -6.336,-9.613 -6.707,-10.41 -1.188,-2.582 -1.816,-6.879 1.922,-12.715 1.41,-2.195 6.527,-9.309 13.328,-9.309 h 0.797 l 1.051,0.18 c 0.832,0.164 2.242,0.457 10.258,3.406 4.551,-3.312 9.668,-5.977 14.992,-7.801 2.492,-6.301 3.273,-7.594 3.621,-8.176 2.402,-3.941 7.281,-5.934 14.523,-5.934 7.453,0 12.418,2.004 14.754,5.953 0.371,0.625 1.039,1.754 3.574,8.16 5.312,1.828 10.418,4.48 14.965,7.789 8.012,-2.953 9.32,-3.219 10.129,-3.383 l 0.738,-0.152 1.047,-0.039 c 7.012,0 12.098,7.125 13.496,9.305 3.738,5.828 3.109,10.125 1.93,12.703 -0.387,0.828 -1.137,2.441 -6.684,10.375 0.52,1.68 0.957,3.387 1.32,5.109 7.172,3.195 8.281,3.922 8.969,4.375 3.746,2.449 5.52,7.262 5.414,14.707 -0.105,7.359 -2.109,12.285 -5.953,14.648 -0.922,0.566 -2.789,1.711 -14.469,5.527 -1.621,2.895 -3.48,5.625 -5.559,8.168 1.59,5.371 1.758,6.566 1.852,7.25 0.777,5.59 -0.633,9.457 -5.035,13.785 -2.863,2.808 -6.055,4.222 -9.492,4.222 l 0,0 z" id="path3131" style="fill:#260859"/>
<path d="m 409.34,122.93 c 0,0 -5.383,-14.453 -6.75,-16.773 -1.375,-2.324 -13.246,-2.52 -14.793,0.031 -1.535,2.566 -6.75,16.742 -6.75,16.742 h 28.293 z" id="path3133" style="fill:#7f3f98"/>
<path d="m 380.41,205.176 c 0,0 -11.297,8.801 -13.367,9.836 -2.066,1.031 -3.934,1.836 -6.656,-0.824 -2.676,-2.637 -2.969,-3.941 -2.598,-6.621 0.371,-2.668 4.875,-16.578 4.875,-16.578 l 17.746,14.187 z" id="path3135" style="fill:#7f3f98"/>
<path d="m 409.645,205.176 c 0,0 11.297,8.801 13.367,9.836 2.066,1.031 3.938,1.836 6.656,-0.824 2.676,-2.637 2.969,-3.941 2.598,-6.621 -0.371,-2.668 -4.875,-16.578 -4.875,-16.578 l -17.746,14.187 z" id="path3137" style="fill:#7f3f98"/>
<path d="m 352.539,155.469 c 0,0 -14.668,6.238 -16.891,7.691 -2.211,1.449 -2.164,13.59 0.336,15.125 2.5,1.531 17.105,6.121 17.105,6.121 l -0.55,-28.937 z" id="path3139" style="fill:#7f3f98"/>
<path d="m 437.82,155.469 c 0,0 14.668,6.238 16.891,7.691 2.215,1.449 2.164,13.59 -0.336,15.125 -2.5,1.531 -17.105,6.121 -17.105,6.121 l 0.55,-28.937 z" id="path3141" style="fill:#7f3f98"/>
<path d="m 348.719,153.543 -7.418,3.023 c -0.465,4.172 0.828,11.812 6.457,16.184 5.629,4.359 0.961,-19.207 0.961,-19.207 z" id="path3143" style="fill:#260859"/>
<path d="m 441.246,153.543 7.418,3.023 c 0.465,4.172 -0.828,11.812 -6.457,16.184 -5.629,4.359 -0.961,-19.207 -0.961,-19.207 z" id="path3145" style="fill:#260859"/>
<path d="m 367.875,129.066 c 0,0 -15.164,-5.836 -17.758,-6.371 -2.598,-0.547 -9.082,7.383 -7.863,10.039 1.223,2.66 10.508,15.508 10.508,15.508 l 15.113,-19.176 z" id="path3147" style="fill:#7f3f98"/>
<path d="m 422.438,129.066 c 0,0 15.164,-5.836 17.754,-6.371 2.598,-0.547 9.086,7.383 7.867,10.039 -1.223,2.66 -10.508,15.508 -10.508,15.508 l -15.113,-19.176 z" id="path3149" style="fill:#7f3f98"/>
<path d="m 359.793,121.949 c -2.27,3.754 -6.828,9.09 -3.359,17.332 2.875,-3.961 11.133,-15.062 11.43,-16.781 l -8.071,-0.551 z" id="path3151" style="fill:#260859"/>
<path d="m 423.164,198.848 10.629,-4.398 3.391,4.844 c -4.156,3.188 -7.762,6.547 -14.637,6.121 -8.692,-0.552 0.617,-6.567 0.617,-6.567 z" id="path3153" style="fill:#260859"/>
<path d="m 366.891,198.848 -10.629,-4.398 -3.391,4.844 c 4.156,3.188 7.762,6.547 14.637,6.121 8.695,-0.552 -0.617,-6.567 -0.617,-6.567 z" id="path3155" style="fill:#260859"/>
<path d="m 430.566,121.949 c 2.266,3.754 6.824,9.09 3.352,17.332 -2.867,-3.961 -11.133,-15.062 -11.43,-16.781 l 8.078,-0.551 z" id="path3157" style="fill:#260859"/>
<path d="m 443.168,163.367 c 0,26.504 -21.488,47.984 -47.988,47.984 -26.508,0 -47.992,-21.48 -47.992,-47.984 0,-26.504 21.484,-47.996 47.992,-47.996 26.5,0 47.988,21.492 47.988,47.996 z" id="path3159" style="fill:#7f3f98"/>
</g>
<path d="m 499.035,379.352 c -24.148,-14.574 -50.754,-40.875 -46.246,-67.043 1.824,-10.57 -6.406,-19.781 -14.363,-28.688 -3.098,-3.477 -6.305,-7.066 -8.812,-10.648 -5.574,-7.969 -11.219,-18.809 -6.41,-25.188 1.828,-2.426 5.477,-3.598 11.152,-3.598 6.438,0 14.301,1.52 19.57,2.734 -2.094,-5.496 -3.824,-12.004 -1.652,-15.984 0.93,-1.707 2.5,-2.793 4.531,-3.148 0.484,-0.082 1,-0.121 1.535,-0.121 10.691,0 31.27,18.254 44.082,30.727 l 2.691,-13.578 1.098,2.488 c 0.855,0.215 21,5.586 31.73,37.02 15.328,14.332 30.715,37.719 28.375,60.863 l -1.391,13.645 -24.707,-4.203 -10.504,-1.258 10.469,12.742 -6.188,10.285 -10.91,2.156 c -4.785,0.953 -10.098,1.809 -15.793,2.543 l -4.441,0.562 -3.816,-2.308 z" id="path3161" style="fill:#260859"/>
<path d="m 499.883,380.711 c -8.453,-18.031 -15.75,-33.605 -11.617,-49.598 5.477,-21.191 4.926,-37.324 -1.633,-47.938 -5.246,-8.492 -12.496,-10.414 -13.895,-10.715 l -2.395,-0.512 -14.07,3.293 -0.402,0.141 c -17.102,5.828 -29.754,33.488 -21.32,69.688 2.219,9.543 5.305,16.332 8.051,22.375 2.02,4.445 3.656,8.047 4.535,11.91 -21.969,18.406 -37.598,42.656 -42.953,66.711 -2.16,9.668 -3.797,24.91 -4.496,41.824 l -0.195,4.844 c 48.699,-18.664 89.555,-41.785 113.125,-62.766 0.469,-12.918 -2.551,-27.004 -8.867,-40.922 -1.278,-2.812 -2.579,-5.585 -3.868,-8.335 z" id="path3163" style="fill:#260859"/>
<path d="m 506.766,421.48 c -5.457,0 -10.125,-2.191 -13.5,-6.348 -7.531,-9.25 -5.598,-25.848 -2.543,-41.09 -0.52,-0.402 -1.082,-0.809 -1.566,-1.16 -2.887,-2.09 -6.832,-4.945 -8.84,-9.621 l -3.844,-8.93 8.52,-4.684 c 26.812,-14.766 36.797,-42.465 29.68,-82.332 -0.055,-0.164 -0.105,-0.328 -0.164,-0.488 -2.809,-3.723 -6.871,-11.012 -10.637,-24.828 -3.523,-12.914 -12.309,-71.832 -7.859,-80.129 0.945,-1.77 3.852,-5.887 10.324,-5.887 4.68,0 18.578,3.645 26.375,12.609 0.766,-0.152 1.562,-0.227 2.363,-0.227 4.25,0 7.562,2.121 11.398,4.578 1.246,0.789 2.582,1.645 4.094,2.543 2.199,1.305 6.66,3.945 10.301,10.102 5.594,0.066 9.668,3.734 19.703,14.016 0.09,0.102 1.055,1.09 1.293,1.32 2.059,2.035 3.676,4.914 5.316,9.516 10.035,2.156 17.312,18.496 18.711,25.281 0.051,0.262 0.152,0.633 0.289,1.145 l 0.273,1.02 c 15.062,57.008 11.363,104.316 -10.707,136.801 -17.801,26.219 -46.922,41.906 -86.551,46.645 -0.859,0.102 -1.648,0.148 -2.429,0.148 l 0,0 z" id="path3165" style="fill:#260859"/>
<path d="m 595.402,237.887 c -1.785,-8.676 -10.664,-20.074 -11.051,-16.191 -0.199,2.031 1.609,13.535 2.16,20.777 -0.961,-0.691 -2.066,-1.449 -2.703,-1.969 -2.98,-14.074 -6.547,-29.066 -9.473,-31.961 -2.211,-2.176 -14.605,-15.398 -13.977,-11.551 2.879,17.59 7.48,30.176 11.324,39.035 -3.078,-2.262 -4.473,-3.328 -6.055,-4.422 -6.062,-4.195 -8.738,-30.359 -13.359,-39.422 -2.363,-4.652 -5.629,-6.57 -7.172,-7.484 -9.922,-5.895 -12.629,-9.863 -10.863,2.387 1.348,9.336 2.344,21.816 14.742,47.855 l -8.207,-6.227 c -6.785,-5.465 -11.07,-36.008 -13.523,-48.082 -2.031,-10.043 -21.027,-15.141 -21.797,-13.699 -1.18,2.203 3.742,53.883 8.754,72.25 5.008,18.359 10.426,24.254 10.836,25.301 3.816,20.711 5.387,47.844 -8.359,70.012 -2.25,3.625 -31.09,-18.539 -34.219,-15.227 -3.129,3.312 19.469,32.105 15.348,34.992 -2.375,1.672 -4.922,3.262 -7.656,4.766 1.898,4.414 10.406,6.801 11.91,12.598 0.566,0 -11.004,41.09 5.863,39.07 136.755,-16.336 89.27,-164.133 87.477,-172.808 z" id="path3167" style="fill:#ffffff"/>
<path d="m 519.477,379.414 c -7.062,-21.422 -20.711,-50.16 -20.711,-50.16 l -28.637,-11.207 19.031,63.051 -6.086,-5.641 -1.121,4.543 10.562,19.551 c 0,0 27.895,4.254 31.422,-6.102 -1.546,-5.48 -2.132,-6.969 -4.46,-14.035 z" id="path3169" style="fill:#aaa4c4"/>
<path d="m 506.004,389.176 c -3.055,0.156 -3.531,-6.988 -5.977,-6.602 -1.98,12.016 -3.234,29.453 7.898,28.121 105.012,-12.539 101.387,-102.594 93.238,-147.656 -1.339,28.816 1.314,121.039 -95.159,126.137 z" id="path3171" style="fill:#aaa4c4"/>
<path d="m 499.363,403.242 c 1.051,4.82 3.543,8.055 8.562,7.453 48.184,-5.754 73.453,-27.84 85.957,-54.395 -20.577,34.462 -64.964,42.055 -94.519,46.942 z" id="path3173" style="fill:#f2f1f8"/>
<path d="m 536.113,285.504 c -10.438,-31.156 -30.414,-36.156 -30.414,-36.156 l -1.723,13.531 c -1.992,-1.926 -34.645,-35.117 -46.812,-33.012 -8.293,1.434 -0.457,18.242 0.266,20.086 -6.105,-1.598 -28.066,-6.832 -32.543,-0.898 -4.473,5.93 2.891,17.617 6.457,22.711 8.84,12.648 26.277,24.93 23.523,40.898 -4.41,25.586 22.328,51.047 45.258,64.883 l 3.191,1.93 3.703,-0.469 c 5.539,-0.715 10.805,-1.555 15.66,-2.52 l 9.992,-1.977 4.938,-8.211 -12.848,-15.648 15.812,1.898 22.48,3.824 1.168,-11.398 c 2.088,-20.64 -11.045,-43.691 -28.108,-59.472 z" id="path3175" style="fill:#260859"/>
<path d="m 511.441,262.371 0.492,25.715 20.984,8.66 C 522.91,277.422 511.441,262.371 511.441,262.371 z" id="path3177" style="fill:#6b5f91"/>
<path d="m 497.977,381.613 c -8.625,-18.406 -16.074,-34.297 -11.754,-51.027 5.328,-20.621 4.859,-36.199 -1.383,-46.305 -4.797,-7.758 -11.289,-9.492 -12.543,-9.758 l -1.934,-0.414 -13.398,3.125 -0.414,0.145 c -16.215,5.523 -28.109,32.18 -19.945,67.215 2.172,9.352 5.219,16.047 7.902,21.953 2.297,5.055 4.102,9.027 4.949,13.617 -21.887,18.016 -37.902,42.508 -43.219,66.363 -2.129,9.539 -3.754,24.648 -4.445,41.449 l -0.16,3.938 c 46.297,-17.949 85.312,-39.887 108.82,-60.047 0.855,-12.961 -2.055,-27.484 -8.621,-41.949 -1.273,-2.801 -2.57,-5.566 -3.855,-8.305 z" id="path3179" style="fill:#260859"/>
<path d="m 414.879,448.457 c -1.84,8.227 -3.52,22.379 -4.238,39.879 36.367,-14.809 67.703,-31.938 89.738,-48.418 3.441,-14 0.355,-31.008 -6.609,-46.34 -10.18,-22.418 -21.957,-42.605 -16.117,-65.207 10.691,-41.363 -7.207,-45.191 -7.207,-45.191 l -11.039,2.574 c -11.812,4.027 -21.172,26.828 -14.184,56.836 4.383,18.836 12.867,26.305 13.566,41.469 -24.969,19.054 -39.453,44.386 -43.91,64.398 z" id="path3181" style="fill:#ffffff"/>
<polygon points="524.949,339.023 553.559,343.895 540.949,327.73 497.863,316.172 478.828,322.18 505.211,341.258 515.43,341.129 518.512,346.129 " id="polygon3183" style="fill:#42316f"/>
<path d="m 512.535,370.281 c -0.336,-0.113 -1.629,-0.211 -2.324,-0.262 -3.934,-0.297 -11.246,-0.84 -16.621,-8.176 -9.293,-12.676 -18.852,-30.539 -18.945,-30.719 l -1.918,-3.586 16.203,-15.387 3.598,2.605 c 10.914,7.887 31.23,22.578 27.18,51.25 l -0.93,6.586 -6.243,-2.311 z" id="path3185" style="fill:#260859"/>
<path d="m 507.996,273.875 c -8.719,-4.91 -1.633,8.113 -1.633,8.113 l 17.391,8.082 c 0,0 -7.035,-11.285 -15.758,-16.195 z" id="path3187" style="fill:#260859"/>
<path d="m 497.902,358.684 c 5.445,7.418 12.918,5.246 16.504,6.578 3.41,-24.145 -12.492,-37.109 -25.016,-46.168 l -10.02,9.52 c 10e-4,-10e-4 9.341,17.515 18.532,30.07 z" id="path3189" style="fill:#f6a0a6"/>
<polygon points="494.504,330.309 504.152,329.352 500.211,320.141 490.449,320.98 " id="polygon3191" style="fill:#260859"/>
<path d="m 477.652,328.371 c 0.344,-1.586 0.645,-3.094 0.914,-4.574 l -0.824,-32.625 -7.129,-5.695 -4.527,-1.277 -6.68,1.555 c -3.07,1.051 -5.98,3.387 -8.48,6.777 4.602,7.199 9.676,10.191 8.43,22.199 -1.32,12.793 10.867,25.336 16.84,28.691 -0.165,-4.742 0.284,-9.649 1.456,-15.051 z" id="path3193" style="fill:#aaa4c4"/>
<path d="m 515.441,362.789 c 2.875,-0.449 5.562,-0.922 8.031,-1.41 -11.984,-10.055 -24.457,-25.418 -30.348,-40.766 1.129,-0.16 2.285,-0.285 3.461,-0.387 l -12.074,-9.066 -1.441,17.137 20.973,26.426 11.398,8.066 z" id="path3195" style="fill:#260859"/>
<g id="g3197">
<path d="m 520.594,365.984 c -11.988,-10.059 -24.457,-25.414 -30.348,-40.773 14.125,-2 31.789,-0.09 63.312,18.684 2.402,-23.664 -8.188,-64.223 -120.211,-88.75 19.531,31.617 35.305,33.953 32.133,58.875 -2.477,19.504 17.742,40.82 40.18,54.359 5.613,-0.715 10.582,-1.535 14.934,-2.395 z" id="path3199" style="fill:#ffffff"/>
</g>
<polygon points="502.367,321.293 514.109,336.367 528.445,325.293 " id="polygon3201" style="fill:#ffffff"/>
<polygon points="474.906,256.566 459.891,260.328 490.523,302.746 " id="polygon3203" style="fill:#260859"/>
<polygon points="519.023,350.121 506.781,357.129 514.066,363.012 " id="polygon3205" style="fill:#ffffff"/>
<polygon points="515.023,361.746 520.594,365.984 523.348,358.102 " id="polygon3207" style="fill:#ffffff"/>
<g id="g3209">
<path d="m 462.426,239.809 c 11.012,28.695 21.609,50.254 24.316,56.68 3.309,7.875 6.352,11.246 16.789,16.836 6.863,3.676 19.824,10.402 31.555,10.422 -8.766,-21.824 -16.195,-32.91 -38.211,-53.188 -11.328,-10.43 -24.145,-21.758 -34.449,-30.75 z" id="path3211" style="fill:#594c82"/>
</g>
<g id="g3213">
<path d="M 521.926,322.051 C 505.781,318.36 496.391,306.5 495.473,288.668 l -0.387,-7.441 6.039,4.363 c 20.16,14.57 24.406,26.645 25.137,32.543 l 0.633,5.062 -4.969,-1.144 z" id="path3215" style="fill:#260859"/>
</g>
<g id="g3217">
<path d="m 504.73,294.418 c 0.742,14.297 7.789,26.453 23.688,30.094 -0.844,-6.774 -6.383,-17.582 -23.688,-30.094 z" id="path3219" style="fill:#ffffff"/>
</g>
<path d="m 521.02,307.242 -1.926,-0.84 c -0.336,0.574 -0.645,1.184 -0.918,1.844 -1.785,4.355 -1.414,8.625 0.836,9.559 1.629,0.664 3.961,-0.762 5.711,-3.18 l -3.703,-7.383 z" id="path3221" style="fill:#260859"/>
<path d="m 493.77,393.578 c -0.039,-0.094 -0.086,-0.184 -0.125,-0.273 -1.496,2.672 -2.945,5.547 -2.945,5.547 0,0 -19.02,-6.207 -25.148,-7.895 -5.867,10.895 -20.238,38.785 -8.461,75.781 16.672,-8.82 31.375,-17.91 43.289,-26.82 3.44,-14 0.354,-31.008 -6.61,-46.34 z" id="path3223" style="fill:#6b5f91"/>
<path d="m 282.871,437.34 c 22.973,18.836 59.043,39.004 101.59,55.91 v -12.582 c -45.699,-18.512 -81.504,-40.121 -101.59,-58.043 v 14.715 z" id="path3225" style="fill:#260859"/>
<path d="m 406.188,493.156 c 42.293,-16.809 78.164,-36.848 101.102,-55.594 v -14.703 c -19.926,17.703 -55.094,38.953 -99.949,57.25 l -1.153,13.047 z" id="path3227" style="fill:#260859"/>
<path d="m 403.402,148.789 c -0.047,-0.816 0.004,-1.645 0.133,-2.477 1.031,-5.996 6.719,-10.016 12.719,-8.984 5.887,1.008 9.855,6.508 9.039,12.367 -7.613,-1.144 -14.949,-1.629 -21.891,-0.906 z" id="path3229" style="fill:#ffffff"/>
<path d="m 365.43,162.512 c 6.816,9.141 17.707,15.051 29.973,15.051 12.27,0 23.16,-5.91 29.973,-15.051 H 365.43 z" id="path3231" style="fill:#260859"/>
<path d="m 416.684,170.902 c -4.809,-1.645 -12.762,-2.707 -21.766,-2.707 -8.594,0 -16.242,0.969 -21.102,2.484 6.102,4.32 13.543,6.883 21.586,6.883 7.907,0 15.239,-2.472 21.282,-6.66 z" id="path3233" style="fill:#b30838"/>
<path d="m 359.141,162.512 0.438,1.328 c 3.145,9.539 4.848,12.016 8.293,12.043 h 0.027 c 3.363,0 5.168,-2.57 8.371,-11.938 l 0.496,-1.434 h -17.625 z" id="path3235" style="fill:#260859"/>
<g id="g3237">
<path d="m 363.828,162.43 c 0,0 2.953,8.965 4.055,8.973 1.102,0.012 4.156,-8.914 4.156,-8.914 l -8.211,-0.059 z" id="path3239" style="fill:#ffffff"/>
</g>
<path d="m 414.266,162.512 0.438,1.328 c 3.145,9.539 4.848,12.016 8.293,12.043 h 0.027 c 3.363,0 5.168,-2.57 8.375,-11.938 l 0.492,-1.434 h -17.625 z" id="path3241" style="fill:#260859"/>
<g id="g3243">
<path d="m 418.953,162.43 c 0,0 2.953,8.965 4.055,8.973 1.105,0.012 4.156,-8.914 4.156,-8.914 l -8.211,-0.059 z" id="path3245" style="fill:#ffffff"/>
</g>
<path d="m 411.148,196.828 c 0,4.668 -7.133,8.449 -15.926,8.449 -8.797,0 -15.93,-3.781 -15.93,-8.449 0,-4.668 7.133,-8.449 15.93,-8.449 8.794,0 15.926,3.781 15.926,8.449 z" id="path3247" style="fill:#ffef6f"/>
<path d="m 400.66,196.828 c 0,1.594 -2.434,2.883 -5.438,2.883 -3,0 -5.438,-1.289 -5.438,-2.883 0,-1.598 2.438,-2.891 5.438,-2.891 3.005,10e-4 5.438,1.293 5.438,2.891 z" id="path3249" style="fill:#7f3f98"/>
<path d="m 410.676,141.695 c 0.078,2.168 1.301,3.871 2.711,3.816 1.422,-0.066 2.496,-1.863 2.402,-4.031 -0.082,-2.176 -1.309,-3.895 -2.727,-3.832 -1.414,0.059 -2.492,1.868 -2.386,4.047 z" id="path3251" style="fill:#260859"/>
<path d="m 386.906,148.789 c 0.051,-0.816 -0.004,-1.645 -0.133,-2.477 -1.027,-5.996 -6.715,-10.016 -12.715,-8.984 -5.887,1.008 -9.855,6.508 -9.039,12.367 7.614,-1.144 14.95,-1.629 21.887,-0.906 z" id="path3253" style="fill:#ffffff"/>
<path d="m 379.633,141.695 c -0.082,2.168 -1.301,3.871 -2.707,3.816 -1.426,-0.066 -2.504,-1.863 -2.402,-4.031 0.082,-2.176 1.309,-3.895 2.723,-3.832 1.417,0.059 2.495,1.868 2.386,4.047 z" id="path3255" style="fill:#260859"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="197.72693mm"
height="190.30197mm"
viewBox="0 0 197.72693 190.30197"
version="1.1"
id="svg5"
xml:space="preserve"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="Toyhouse.svg"
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="0.5"
inkscape:cx="969"
inkscape:cy="450"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs2" /><g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(6.2923729,-54.858845)"><g
id="g1824"><path
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M -6.2923729,154.22213 92.377447,54.858845 121.69053,84.093271 V 69.076617 h 31.14565 v 46.273543 l 38.59837,38.59837 h -32.36923 v 91.21228 H 25.02776 v -91.10105 z"
id="path428"
sodipodi:nodetypes="cccccccccccc" /><path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 54.338047,170.6337 v 8.39821 H 70.80075 v 53.94872 h 11.012215 v -54.00434 h 17.920872 c 0,0 2.72e-4,-0.23357 2.72e-4,2.12606 v 51.91208 h 8.966631 v -29.12162 h 26.03469 v 28.76768 h 9.28125 v -61.66526 h -8.96663 v 25.72008 h -26.42796 l 0.22247,-26.09986 z"
id="path526"
sodipodi:nodetypes="cccccccscccccccccccc" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 248 204">
<path fill="#1d9bf0" d="M221.95 51.29c.15 2.17.15 4.34.15 6.53 0 66.73-50.8 143.69-143.69 143.69v-.04c-27.44.04-54.31-7.82-77.41-22.64 3.99.48 8 .72 12.02.73 22.74.02 44.83-7.61 62.72-21.66-21.61-.41-40.56-14.5-47.18-35.07 7.57 1.46 15.37 1.16 22.8-.87-23.56-4.76-40.51-25.46-40.51-49.5v-.64c7.02 3.91 14.88 6.08 22.92 6.32C11.58 63.31 4.74 33.79 18.14 10.71c25.64 31.55 63.47 50.73 104.08 52.76-4.07-17.54 1.49-35.92 14.61-48.25 20.34-19.12 52.33-18.14 71.45 2.19 11.31-2.23 22.15-6.38 32.07-12.26-3.77 11.69-11.66 21.62-22.2 27.93 10.01-1.18 19.79-3.86 29-7.95-6.78 10.16-15.32 19.01-25.2 26.16z"/>
</svg>

After

Width:  |  Height:  |  Size: 732 B

View File

@@ -1,7 +1 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Ubiquiti" role="img"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15"
fill="#399cdb"/><path d="M112 94v18h18V94h-18zm288 0c-82 0-90 31-90 61v172a147 147 0 01-3 28c43-9 72-36 86-82l7-23V94zm-234 18v18h18v-18h-18zm-18 18v18h18v-18h-18zm36 9v18h18v-18h-18zm-72 4v147c0 73 53 128 144 128 0 0-54-30-54-91V197h-18v66h-18v-39h-18v17h-18v-98h-18zm54 18v18h18v-18h-18zm-18 27v18h18v-18h-18zm252 87c-19 64-65 92-131 89-24-1-43-7-57-16 10 42 46 63 48 64l10 6c82-5 130-59 130-128v-15z" fill="#ffffff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M494.2 0h-31.8v31.8h31.8zM383.1 222.4v-63.6h63.5v63.5h63.5c1.1 58.9-3.4 110.2-33.3 161.6-86.6 152.4-300.5 172.9-414 39.2C36.3 392.4 17.2 355 8.3 315c-4.5-21.7-6.5-49.2-6.5-72.5V4h127l.2 242c.6 31.3 6.3 63.5 25 88 53.9 73 167.9 66.3 212.1-13.1 15.9-26.6 17.3-68.7 17-98.5m15.8-174.8h47.6v47.6H510v63.5h-63.5V95.3h-47.6z" style="fill:#005ed9"/></svg>

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 440 B

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="240.000000pt" height="240.000000pt" viewBox="0 0 240.000000 240.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,240.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M765 2069 c-179 -12 -224 -23 -273 -65 -74 -63 -73 -55 -70 -619 l3
-500 34 -70 c61 -125 213 -256 420 -362 110 -56 289 -126 321 -125 14 0 77 21
140 47 313 126 514 275 597 442 l38 77 3 483 c2 341 0 495 -9 525 -7 28 -27
59 -58 89 -56 56 -85 64 -309 79 -178 12 -648 11 -837 -1z m850 -403 c39 -1
99 2 135 7 l65 7 -100 -34 c-95 -33 -103 -38 -167 -105 l-68 -71 0 -73 c0
-100 -22 -138 -128 -222 -116 -93 -137 -142 -167 -390 -9 -71 -18 -141 -21
-154 l-5 -24 -21 20 c-11 12 -27 45 -35 74 -21 80 -27 325 -10 434 9 55 11 97
5 107 -4 10 -36 36 -69 58 -33 23 -92 71 -132 106 -55 51 -102 80 -202 129
l-130 64 183 0 182 1 122 -45 c66 -24 136 -47 154 -51 31 -6 35 -3 63 42 71
118 103 137 211 127 36 -3 97 -6 135 -7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -40,8 +40,10 @@ Future<void> initSystemTray() async {
if (PlatformUtil.isMobile()) return;
String path = Platform.isWindows
? 'assets/icons/auth-icon.ico'
: 'assets/icons/auth-icon.png';
await trayManager.setIcon(path);
: Platform.isMacOS
? 'assets/icons/auth-icon-monochrome.png'
: 'assets/icons/auth-icon.png';
await trayManager.setIcon(path, isTemplate: true);
Menu menu = Menu(
items: [
MenuItem(

View File

@@ -10,12 +10,12 @@ commit](https://github.com/ente-io/ente/commit/a8cdc811fd20ca4289d8e779c97f08ef5
Hello world
To know more about Ente, see [our main README](../README.md) or visit
To know more about Ente, see [our main README](../../../README.md) or visit
[ente.io](https://ente.io).
To use Ente Photos on the web, see [../web](../web/README.md). To use Ente
Photos on the desktop, see [../desktop](../desktop/README.md). There is a also a
[CLI tool](../cli/README.md) for easy / automated exports.
To use Ente Photos on the web, see [../../../web](../../../web/README.md). To use Ente
Photos on the desktop, see [../../../desktop](../../../desktop/README.md). There is a also a
[CLI tool](../../../cli/README.md) for easy / automated exports.
If you're looking for Ente Auth instead, see [../auth](../auth/README.md).
@@ -32,16 +32,16 @@ without relying on third party stores.
You can alternatively install the build from PlayStore or F-Droid.
<a href="https://play.google.com/store/apps/details?id=io.ente.photos">
<img height="59" src="../.github/assets/play-store-badge.png">
<img height="59" src="../../../.github/assets/play-store-badge.png">
</a>
<a href="https://f-droid.org/packages/io.ente.photos.fdroid/">
<img height="59" src="../.github/assets/f-droid-badge.png">
<img height="59" src="../../../.github/assets/f-droid-badge.png">
</a>
### iOS
<a href="https://apps.apple.com/in/app/ente-photos/id1542026904">
<img height="59" src="../.github/assets/app-store-badge.svg">
<img height="59" src="../../../.github/assets/app-store-badge.svg">
</a>
## 🧑‍💻 Building from source
@@ -99,4 +99,4 @@ apksigner verify --print-certs <path_to_apk>
## 💚 Contribute
For more ways to contribute, see [../CONTRIBUTING.md](../CONTRIBUTING.md).
For more ways to contribute, see [../../../CONTRIBUTING.md](../../../CONTRIBUTING.md).

View File

@@ -12,6 +12,8 @@ PODS:
- Flutter
- device_info_plus (0.0.1):
- Flutter
- emoji_picker_flutter (0.0.1):
- Flutter
- ffmpeg_kit_custom (6.0.3)
- ffmpeg_kit_flutter (6.0.3):
- ffmpeg_kit_custom
@@ -127,9 +129,6 @@ PODS:
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
- local_auth_ios (0.0.1):
- Flutter
- Mantle (2.2.0):
@@ -230,6 +229,8 @@ PODS:
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- vibration (1.7.5):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -250,6 +251,7 @@ DEPENDENCIES:
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
- dart_ui_isolate (from `.symlinks/plugins/dart_ui_isolate/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
- ffmpeg_kit_flutter (from `.symlinks/plugins/ffmpeg_kit_flutter/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@@ -269,7 +271,6 @@ DEPENDENCIES:
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- launcher_icon_switcher (from `.symlinks/plugins/launcher_icon_switcher/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
- maps_launcher (from `.symlinks/plugins/maps_launcher/ios`)
- media_extension (from `.symlinks/plugins/media_extension/ios`)
@@ -297,6 +298,7 @@ DEPENDENCIES:
- thermal (from `.symlinks/plugins/thermal/ios`)
- ua_client_hints (from `.symlinks/plugins/ua_client_hints/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- vibration (from `.symlinks/plugins/vibration/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
@@ -304,7 +306,7 @@ DEPENDENCIES:
- workmanager (from `.symlinks/plugins/workmanager/ios`)
SPEC REPOS:
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios:
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git:
- ffmpeg_kit_custom
trunk:
- Firebase
@@ -339,6 +341,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/dart_ui_isolate/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
emoji_picker_flutter:
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
ffmpeg_kit_flutter:
:path: ".symlinks/plugins/ffmpeg_kit_flutter/ios"
file_saver:
@@ -377,8 +381,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios"
launcher_icon_switcher:
:path: ".symlinks/plugins/launcher_icon_switcher/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
local_auth_ios:
:path: ".symlinks/plugins/local_auth_ios/ios"
maps_launcher:
@@ -433,6 +435,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/ua_client_hints/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
vibration:
:path: ".symlinks/plugins/vibration/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
video_thumbnail:
@@ -445,83 +449,84 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6
battery_info: b6c551049266af31556b93c9d9b9452cfec0219f
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
emoji_picker_flutter: fe2e6151c5b548e975d546e6eeb567daf0962a58
ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99
ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
ffmpeg_kit_flutter: 9dce4803991478c78c6fb9f972703301101095fe
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_sodium: a00383520fc689c688b66fd3092984174712493e
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
launcher_icon_switcher: 8e0ad2131a20c51c1dd939896ee32e70cd845b37
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203
media_extension: 6618f07abd762cdbfaadf1b0c56a287e820f0c84
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91
motionphoto: 8b65ce50c7d7ff3c767534fc3768b2eed9ac24e4
move_to_background: cd3091014529ec7829e342ad2d75c0a11f4378a5
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
native_video_player: 6809dec117e8997161dbfb42a6f90d6df71a504d
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
native_video_player: 29ab24a926804ac8c4a57eb6d744c7d927c2bc3e
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832
system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
thermal: d4c48be750d1ddbab36b0e2dcb2471531bc8df41
ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
thermal: a9261044101ae8f532fa29cab4e8270b51b3f55c
ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
video_thumbnail: 94ba6705afbaa120b77287080424930f23ea0c40
volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: a8ef88ad74ba499756207e7592c6071a96756d18

View File

@@ -532,6 +532,7 @@
"${BUILT_PRODUCTS_DIR}/cupertino_http/cupertino_http.framework",
"${BUILT_PRODUCTS_DIR}/dart_ui_isolate/dart_ui_isolate.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/emoji_picker_flutter/emoji_picker_flutter.framework",
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
"${BUILT_PRODUCTS_DIR}/flutter_email_sender/flutter_email_sender.framework",
"${BUILT_PRODUCTS_DIR}/flutter_image_compress_common/flutter_image_compress_common.framework",
@@ -548,7 +549,6 @@
"${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
"${BUILT_PRODUCTS_DIR}/launcher_icon_switcher/launcher_icon_switcher.framework",
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
"${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework",
"${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework",
"${BUILT_PRODUCTS_DIR}/maps_launcher/maps_launcher.framework",
"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
@@ -576,6 +576,7 @@
"${BUILT_PRODUCTS_DIR}/thermal/thermal.framework",
"${BUILT_PRODUCTS_DIR}/ua_client_hints/ua_client_hints.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
"${BUILT_PRODUCTS_DIR}/vibration/vibration.framework",
"${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework",
"${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework",
"${BUILT_PRODUCTS_DIR}/volume_controller/volume_controller.framework",
@@ -628,6 +629,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cupertino_http.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/dart_ui_isolate.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/emoji_picker_flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_email_sender.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress_common.framework",
@@ -644,7 +646,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/launcher_icon_switcher.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/maps_launcher.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
@@ -672,6 +673,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/thermal.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ua_client_hints.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/vibration.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/volume_controller.framework",

View File

@@ -157,6 +157,23 @@ class UploadLocksDB {
);
}
Future<String> getLockData(String id) async {
final db = await instance.database;
final rows = await db.query(
_uploadLocksTable.table,
where: '${_uploadLocksTable.columnID} = ?',
whereArgs: [id],
);
if (rows.isEmpty) {
return "No lock found for $id";
}
final row = rows.first;
final time = row[_uploadLocksTable.columnTime] as int;
final owner = row[_uploadLocksTable.columnOwner] as String;
final duration = DateTime.now().millisecondsSinceEpoch - time;
return "Lock for $id acquired by $owner since ${Duration(milliseconds: duration)}";
}
Future<bool> isLocked(String id, String owner) async {
final db = await instance.database;
final rows = await db.query(

View File

@@ -12495,6 +12495,118 @@ class S {
args: [],
);
}
/// `Undo`
String get undo {
return Intl.message(
'Undo',
name: 'undo',
desc: '',
args: [],
);
}
/// `Redo`
String get redo {
return Intl.message(
'Redo',
name: 'redo',
desc: '',
args: [],
);
}
/// `Filter`
String get filter {
return Intl.message(
'Filter',
name: 'filter',
desc: '',
args: [],
);
}
/// `Adjust`
String get adjust {
return Intl.message(
'Adjust',
name: 'adjust',
desc: '',
args: [],
);
}
/// `Draw`
String get draw {
return Intl.message(
'Draw',
name: 'draw',
desc: '',
args: [],
);
}
/// `Sticker`
String get sticker {
return Intl.message(
'Sticker',
name: 'sticker',
desc: '',
args: [],
);
}
/// `Brush Color`
String get brushColor {
return Intl.message(
'Brush Color',
name: 'brushColor',
desc: '',
args: [],
);
}
/// `Font`
String get font {
return Intl.message(
'Font',
name: 'font',
desc: '',
args: [],
);
}
/// `Background`
String get background {
return Intl.message(
'Background',
name: 'background',
desc: '',
args: [],
);
}
/// `Align`
String get align {
return Intl.message(
'Align',
name: 'align',
desc: '',
args: [],
);
}
/// `{count, plural, =1{Added successfully to 1 album} other{Added successfully to {count} albums}}`
String addedToAlbums(int count) {
return Intl.plural(
count,
one: 'Added successfully to 1 album',
other: 'Added successfully to $count albums',
name: 'addedToAlbums',
desc: 'Message shown when items are added to albums',
args: [count],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -1808,5 +1808,24 @@
"automaticallyAnalyzeAndSplitGrouping": "We will automatically analyze the grouping to determine if there are multiple people present, and separate them out again. This may take a few seconds.",
"layout": "Layout",
"day": "Day",
"peopleAutoAddDesc": "Select the people you want to automatically add to the album"
"peopleAutoAddDesc": "Select the people you want to automatically add to the album",
"undo": "Undo",
"redo": "Redo",
"filter": "Filter",
"adjust": "Adjust",
"draw": "Draw",
"sticker": "Sticker",
"brushColor": "Brush Color",
"font": "Font",
"background": "Background",
"align": "Align",
"addedToAlbums": "{count, plural, =1{Added successfully to 1 album} other{Added successfully to {count} albums}}",
"@addedToAlbums": {
"description": "Message shown when items are added to albums",
"placeholders": {
"count": {
"type": "int"
}
}
}
}

View File

@@ -224,18 +224,20 @@ class GalleryGroups {
int i = 0;
while (!endOfListReached) {
gridRowChildren.add(
GalleryFileWidget(
RepaintBoundary(
key: ValueKey(
tagPrefix +
filesInGroup[firstIndexOfRowWrtFilesInGroup + i]
.tag,
),
file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
selectedFiles: selectedFiles,
limitSelectionToOne: limitSelectionToOne,
tag: tagPrefix,
photoGridSize: crossAxisCount,
currentUserID: currentUserID,
child: GalleryFileWidget(
file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
selectedFiles: selectedFiles,
limitSelectionToOne: limitSelectionToOne,
tag: tagPrefix,
photoGridSize: crossAxisCount,
currentUserID: currentUserID,
),
),
);
@@ -247,18 +249,20 @@ class GalleryGroups {
} else {
for (int i = 0; i < crossAxisCount; i++) {
gridRowChildren.add(
GalleryFileWidget(
RepaintBoundary(
key: ValueKey(
tagPrefix +
filesInGroup[firstIndexOfRowWrtFilesInGroup + i]
.tag,
),
file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
selectedFiles: selectedFiles,
limitSelectionToOne: limitSelectionToOne,
tag: tagPrefix,
photoGridSize: crossAxisCount,
currentUserID: currentUserID,
child: GalleryFileWidget(
file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
selectedFiles: selectedFiles,
limitSelectionToOne: limitSelectionToOne,
tag: tagPrefix,
photoGridSize: crossAxisCount,
currentUserID: currentUserID,
),
),
);
}

View File

@@ -310,9 +310,7 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
if (result) {
showShortToast(
context,
"Added successfully to " +
_selectedCollections.length.toString() +
" albums",
S.of(context).addedToAlbums(_selectedCollections.length),
);
widget.selectedFiles?.clearAll();
}

View File

@@ -1,110 +0,0 @@
import 'dart:math';
import 'package:flutter/widgets.dart';
import 'package:image_editor/image_editor.dart';
class FilteredImage extends StatelessWidget {
const FilteredImage({
required this.child,
this.brightness,
this.saturation,
this.hue,
super.key,
});
final double? brightness, saturation, hue;
final Widget child;
@override
Widget build(BuildContext context) {
return ColorFiltered(
colorFilter: ColorFilter.matrix(
ColorFilterGenerator.brightnessAdjustMatrix(
value: brightness ?? 1,
),
),
child: ColorFiltered(
colorFilter: ColorFilter.matrix(
ColorFilterGenerator.saturationAdjustMatrix(
value: saturation ?? 1,
),
),
child: ColorFiltered(
colorFilter: ColorFilter.matrix(
ColorFilterGenerator.hueAdjustMatrix(
value: hue ?? 0,
),
),
child: child,
),
),
);
}
}
class ColorFilterGenerator {
static List<double> hueAdjustMatrix({double value = 1}) {
value = value * pi;
if (value == 0) {
return [
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
1,
0,
];
}
final double cosVal = cos(value);
final double sinVal = sin(value);
const double lumR = 0.213;
const double lumG = 0.715;
const double lumB = 0.072;
return List<double>.from(<double>[
(lumR + (cosVal * (1 - lumR))) + (sinVal * (-lumR)),
(lumG + (cosVal * (-lumG))) + (sinVal * (-lumG)),
(lumB + (cosVal * (-lumB))) + (sinVal * (1 - lumB)),
0,
0,
(lumR + (cosVal * (-lumR))) + (sinVal * 0.143),
(lumG + (cosVal * (1 - lumG))) + (sinVal * 0.14),
(lumB + (cosVal * (-lumB))) + (sinVal * (-0.283)),
0,
0,
(lumR + (cosVal * (-lumR))) + (sinVal * (-(1 - lumR))),
(lumG + (cosVal * (-lumG))) + (sinVal * lumG),
(lumB + (cosVal * (1 - lumB))) + (sinVal * lumB),
0,
0,
0,
0,
0,
1,
0,
]).map((i) => i.toDouble()).toList();
}
static List<double> brightnessAdjustMatrix({double value = 1}) {
return ColorOption.brightness(value).matrix;
}
static List<double> saturationAdjustMatrix({double value = 1}) {
return ColorOption.saturation(value).matrix;
}
}

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/ente_theme_data.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
@@ -43,7 +44,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
enableUndo ? close() : Navigator.of(context).pop();
},
child: Text(
'Cancel',
S.of(context).cancel,
style: getEnteTextTheme(context).body,
),
),
@@ -52,7 +53,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
tooltip: 'Undo',
tooltip: S.of(context).undo,
onPressed: () {
undo != null ? undo!() : null;
},
@@ -66,7 +67,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
),
const SizedBox(width: 12),
IconButton(
tooltip: 'Redo',
tooltip: S.of(context).redo,
onPressed: () {
redo != null ? redo!() : null;
},
@@ -88,7 +89,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
key: ValueKey(isMainEditor ? 'save_copy' : 'done'),
onPressed: done,
child: Text(
isMainEditor ? 'Save Copy' : 'Done',
isMainEditor ? S.of(context).saveCopy : S.of(context).done,
style: getEnteTextTheme(context).body.copyWith(
color: isMainEditor
? (enableUndo

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
@@ -113,7 +114,7 @@ class _ImageEditorCropRotateBarState extends State<ImageEditorCropRotateBar>
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop-rotate.svg",
label: "Rotate",
label: S.of(context).rotate,
onTap: () {
widget.editor.rotate();
},
@@ -121,7 +122,7 @@ class _ImageEditorCropRotateBarState extends State<ImageEditorCropRotateBar>
const SizedBox(width: 6),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-flip.svg",
label: "Flip",
label: S.of(context).flip,
onTap: () {
widget.editor.flip();
},

View File

@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:flutter/material.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
@@ -90,7 +91,7 @@ class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
children: <Widget>[
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop.svg",
label: "Crop",
label: S.of(context).crop,
onTap: () {
widget.editor.openCropRotateEditor();
},
@@ -98,21 +99,21 @@ class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-filter.svg",
label: "Filter",
label: S.of(context).filter,
onTap: () {
widget.editor.openFilterEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-tune.svg",
label: "Adjust",
label: S.of(context).adjust,
onTap: () {
widget.editor.openTuneEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-paint.svg",
label: "Draw",
label: S.of(context).draw,
onTap: () {
widget.editor.openPaintingEditor();
},
@@ -120,7 +121,7 @@ class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-sticker.svg",
label: "Sticker",
label: S.of(context).sticker,
onTap: () {
widget.editor.openEmojiEditor();
},

View File

@@ -37,12 +37,12 @@ import "package:photos/utils/navigation_util.dart";
import "package:pro_image_editor/models/editor_configs/main_editor_configs.dart";
import 'package:pro_image_editor/pro_image_editor.dart';
class NewImageEditor extends StatefulWidget {
class ImageEditorPage extends StatefulWidget {
final ente.EnteFile originalFile;
final File file;
final DetailPageConfiguration detailPageConfig;
const NewImageEditor({
const ImageEditorPage({
super.key,
required this.file,
required this.originalFile,
@@ -50,10 +50,10 @@ class NewImageEditor extends StatefulWidget {
});
@override
State<NewImageEditor> createState() => _NewImageEditorState();
State<ImageEditorPage> createState() => _ImageEditorPageState();
}
class _NewImageEditorState extends State<NewImageEditor> {
class _ImageEditorPageState extends State<ImageEditorPage> {
final _mainEditorBarKey = GlobalKey<ImageEditorMainBottomBarState>();
final editorKey = GlobalKey<ProImageEditorState>();
final _logger = Logger("ImageEditor");

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
@@ -63,7 +64,7 @@ class _ImageEditorPaintBarState extends State<ImageEditorPaintBar>
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text(
"Brush Color",
S.of(context).brushColor,
style: getEnteTextTheme(context).body,
),
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
@@ -75,7 +76,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-color.svg",
label: "Color",
label: S.of(context).color,
isSelected: selectedActionIndex == 0,
onTap: () {
_selectAction(0);
@@ -83,7 +84,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-font.svg",
label: "Font",
label: S.of(context).font,
isSelected: selectedActionIndex == 1,
onTap: () {
_selectAction(1);
@@ -91,7 +92,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-background.svg",
label: "Background",
label: S.of(context).background,
isSelected: selectedActionIndex == 2,
onTap: () {
setState(() {
@@ -101,7 +102,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-align-left.svg",
label: "Align",
label: S.of(context).align,
isSelected: selectedActionIndex == 3,
onTap: () {
setState(() {

View File

@@ -1,553 +0,0 @@
import "dart:async";
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui show Image;
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import "package:flutter_image_compress/flutter_image_compress.dart";
import 'package:image_editor/image_editor.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/file/file.dart' as ente;
import 'package:photos/models/location/location.dart';
import 'package:photos/services/sync/sync_service.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/components/action_sheet_widget.dart';
import 'package:photos/ui/components/buttons/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/notification/toast.dart';
import 'package:photos/ui/tools/editor/filtered_image.dart';
import 'package:photos/ui/viewer/file/detail_page.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
import 'package:syncfusion_flutter_core/theme.dart';
import 'package:syncfusion_flutter_sliders/sliders.dart';
class ImageEditorPage extends StatefulWidget {
final ImageProvider imageProvider;
final DetailPageConfiguration detailPageConfig;
final ente.EnteFile originalFile;
const ImageEditorPage(
this.imageProvider,
this.originalFile,
this.detailPageConfig, {
super.key,
});
@override
State<ImageEditorPage> createState() => _ImageEditorPageState();
}
class _ImageEditorPageState extends State<ImageEditorPage> {
static const double kBrightnessDefault = 1;
static const double kBrightnessMin = 0;
static const double kBrightnessMax = 2;
static const double kSaturationDefault = 1;
static const double kSaturationMin = 0;
static const double kSaturationMax = 2;
final _logger = Logger("ImageEditor");
final GlobalKey<ExtendedImageEditorState> editorKey =
GlobalKey<ExtendedImageEditorState>();
double? _brightness = kBrightnessDefault;
double? _saturation = kSaturationDefault;
bool _hasEdited = false;
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) async {
if (_hasBeenEdited()) {
await _showExitConfirmationDialog(context);
} else {
replacePage(context, DetailPage(widget.detailPageConfig));
}
},
child: Scaffold(
appBar: AppBar(
backgroundColor: const Color(0x00000000),
elevation: 0,
actions: _hasBeenEdited()
? [
IconButton(
padding: const EdgeInsets.only(right: 16, left: 16),
onPressed: () {
editorKey.currentState!.reset();
setState(() {
_brightness = kBrightnessDefault;
_saturation = kSaturationDefault;
});
},
icon: const Icon(Icons.history),
),
]
: [],
),
body: Column(
children: [
Expanded(child: _buildImage()),
const Padding(padding: EdgeInsets.all(4)),
Column(
children: [
_buildBrightness(),
_buildSat(),
],
),
const Padding(padding: EdgeInsets.all(8)),
SafeArea(child: _buildBottomBar()),
Padding(padding: EdgeInsets.all(Platform.isIOS ? 16 : 6)),
],
),
),
);
}
bool _hasBeenEdited() {
return _hasEdited ||
_saturation != kSaturationDefault ||
_brightness != kBrightnessDefault;
}
Widget _buildImage() {
return Hero(
tag: widget.detailPageConfig.tagPrefix + widget.originalFile.tag,
child: ExtendedImage(
image: widget.imageProvider,
extendedImageEditorKey: editorKey,
mode: ExtendedImageMode.editor,
fit: BoxFit.contain,
initEditorConfigHandler: (_) => EditorConfig(
maxScale: 8.0,
cropRectPadding: const EdgeInsets.all(20.0),
hitTestSize: 20.0,
cornerColor: const Color.fromRGBO(45, 150, 98, 1),
editActionDetailsIsChanged: (_) {
setState(() {
_hasEdited = true;
});
},
),
loadStateChanged: (state) {
if (state.extendedImageLoadState == LoadState.completed) {
return FilteredImage(
brightness: _brightness,
saturation: _saturation,
child: state.completedWidget,
);
}
return const EnteLoadingWidget();
},
),
);
}
Widget _buildBottomBar() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildFlipButton(),
_buildRotateLeftButton(),
_buildRotateRightButton(),
_buildSaveButton(),
],
);
}
Widget _buildFlipButton() {
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
flip();
},
child: SizedBox(
width: 80,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Icon(
Icons.flip,
color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
size: 20,
),
),
const Padding(padding: EdgeInsets.all(2)),
Text(
S.of(context).flip,
style: subtitle2.copyWith(
color: subtitle2.color!.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildRotateLeftButton() {
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
rotate(false);
},
child: SizedBox(
width: 80,
child: Column(
children: [
Icon(
Icons.rotate_left,
color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
),
const Padding(padding: EdgeInsets.all(2)),
Text(
S.of(context).rotateLeft,
style: subtitle2.copyWith(
color: subtitle2.color!.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildRotateRightButton() {
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
rotate(true);
},
child: SizedBox(
width: 80,
child: Column(
children: [
Icon(
Icons.rotate_right,
color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
),
const Padding(padding: EdgeInsets.all(2)),
Text(
S.of(context).rotateRight,
style: subtitle2.copyWith(
color: subtitle2.color!.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildSaveButton() {
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
_saveEdits();
},
child: SizedBox(
width: 80,
child: Column(
children: [
Icon(
Icons.save_alt_outlined,
color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
),
const Padding(padding: EdgeInsets.all(2)),
Text(
S.of(context).saveCopy,
style: subtitle2.copyWith(
color: subtitle2.color!.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Future<void> _saveEdits() async {
final dialog = createProgressDialog(context, S.of(context).saving);
await dialog.show();
final ExtendedImageEditorState? state = editorKey.currentState;
if (state == null) {
return;
}
final Rect? rect = state.getCropRect();
if (rect == null) {
return;
}
final EditActionDetails action = state.editAction!;
final double radian = action.rotateAngle;
final bool flipHorizontal = action.flipY;
final bool flipVertical = action.flipX;
final Uint8List img = state.rawImageData;
// ignore: unnecessary_null_comparison
if (img == null) {
_logger.severe("null rawImageData");
showToast(context, S.of(context).somethingWentWrong);
return;
}
final ImageEditorOption option = ImageEditorOption();
option.addOption(ClipOption.fromRect(rect));
option.addOption(
FlipOption(horizontal: flipHorizontal, vertical: flipVertical),
);
if (action.hasRotateAngle) {
option.addOption(RotateOption(radian.toInt()));
}
option.addOption(ColorOption.saturation(_saturation!));
option.addOption(ColorOption.brightness(_brightness!));
option.outputFormat = const OutputFormat.jpeg(100);
final DateTime start = DateTime.now();
Uint8List? result = await ImageEditor.editImage(
image: img,
imageEditorOption: option,
);
if (result == null) {
_logger.severe("null result");
showToast(context, S.of(context).somethingWentWrong);
return;
}
_logger.info('Size before compression = ${result.length}');
final ui.Image decodedResult = await decodeImageFromList(result);
result = await FlutterImageCompress.compressWithList(
result,
minWidth: decodedResult.width,
minHeight: decodedResult.height,
);
_logger.info('Size after compression = ${result.length}');
final Duration diff = DateTime.now().difference(start);
_logger.info('image_editor time : $diff');
try {
final fileName =
path.basenameWithoutExtension(widget.originalFile.title!) +
"_edited_" +
DateTime.now().microsecondsSinceEpoch.toString() +
".JPEG";
//Disabling notifications for assets changing to insert the file into
//files db before triggering a sync.
await PhotoManager.stopChangeNotify();
final AssetEntity newAsset =
await (PhotoManager.editor.saveImage(result, filename: fileName));
final newFile = await ente.EnteFile.fromAsset(
widget.originalFile.deviceFolder ?? '',
newAsset,
);
newFile.creationTime = widget.originalFile.creationTime;
newFile.collectionID = widget.originalFile.collectionID;
newFile.location = widget.originalFile.location;
if (!newFile.hasLocation && widget.originalFile.localID != null) {
final assetEntity = await widget.originalFile.getAsset;
if (assetEntity != null) {
final latLong = await assetEntity.latlngAsync();
newFile.location = Location(
latitude: latLong.latitude,
longitude: latLong.longitude,
);
}
}
newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile);
Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
unawaited(SyncService.instance.sync());
showShortToast(context, S.of(context).editsSaved);
_logger.info("Original file " + widget.originalFile.toString());
_logger.info("Saved edits to file " + newFile.toString());
final files = widget.detailPageConfig.files;
// the index could be -1 if the files fetched doesn't contain the newly
// edited files
int selectionIndex =
files.indexWhere((file) => file.generatedID == newFile.generatedID);
if (selectionIndex == -1) {
files.add(newFile);
selectionIndex = files.length - 1;
}
await dialog.hide();
replacePage(
context,
DetailPage(
widget.detailPageConfig.copyWith(
files: files,
selectedIndex: min(selectionIndex, files.length - 1),
),
),
);
} catch (e, s) {
await dialog.hide();
showToast(context, S.of(context).oopsCouldNotSaveEdits);
_logger.severe(e, s);
} finally {
await PhotoManager.startChangeNotify();
}
}
void flip() {
editorKey.currentState?.flip();
}
void rotate(bool right) {
editorKey.currentState?.rotate(right: right);
}
Widget _buildSat() {
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
return Container(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Row(
children: [
SizedBox(
width: 42,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
S.of(context).color,
style: subtitle2.copyWith(
color: subtitle2.color!.withOpacity(0.8),
),
),
),
),
Expanded(
child: SfSliderTheme(
data: SfSliderThemeData(
activeTrackHeight: 4,
inactiveTrackHeight: 2,
inactiveTrackColor: Colors.grey[900],
activeTrackColor: const Color.fromRGBO(45, 150, 98, 1),
thumbColor: const Color.fromRGBO(45, 150, 98, 1),
thumbRadius: 10,
tooltipBackgroundColor: Colors.grey[900],
),
child: SfSlider(
onChanged: (value) {
setState(() {
_saturation = value;
});
},
value: _saturation,
enableTooltip: true,
stepSize: 0.01,
min: kSaturationMin,
max: kSaturationMax,
),
),
),
],
),
);
}
Widget _buildBrightness() {
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
return Container(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Row(
children: [
SizedBox(
width: 42,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
S.of(context).light,
style: subtitle2.copyWith(
color: subtitle2.color!.withOpacity(0.8),
),
),
),
),
Expanded(
child: SfSliderTheme(
data: SfSliderThemeData(
activeTrackHeight: 4,
inactiveTrackHeight: 2,
activeTrackColor: const Color.fromRGBO(45, 150, 98, 1),
inactiveTrackColor: Colors.grey[900],
thumbColor: const Color.fromRGBO(45, 150, 98, 1),
thumbRadius: 10,
tooltipBackgroundColor: Colors.grey[900],
),
child: SfSlider(
onChanged: (value) {
setState(() {
_brightness = value;
});
},
value: _brightness,
enableTooltip: true,
stepSize: 0.01,
min: kBrightnessMin,
max: kBrightnessMax,
),
),
),
],
),
);
}
Future<void> _showExitConfirmationDialog(BuildContext context) async {
final actionResult = await showActionSheet(
context: context,
buttons: [
ButtonWidget(
labelText: S.of(context).yesDiscardChanges,
buttonType: ButtonType.critical,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.first,
isInAlert: true,
),
ButtonWidget(
labelText: S.of(context).no,
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
isInAlert: true,
),
],
body: S.of(context).doYouWantToDiscardTheEditsYouHaveMade,
actionSheetType: ActionSheetType.defaultActionSheet,
);
if (actionResult?.action != null &&
actionResult!.action == ButtonAction.first) {
replacePage(context, DetailPage(widget.detailPageConfig));
}
}
}

View File

@@ -19,8 +19,7 @@ import "package:photos/services/local_authentication_service.dart";
import "package:photos/states/detail_page_state.dart";
import "package:photos/ui/common/fast_scroll_physics.dart";
import 'package:photos/ui/notification/toast.dart';
import "package:photos/ui/tools/editor/image_editor/image_editor_page_new.dart";
import 'package:photos/ui/tools/editor/image_editor_page.dart';
import "package:photos/ui/tools/editor/image_editor/image_editor_page.dart";
import "package:photos/ui/tools/editor/video_editor_page.dart";
import "package:photos/ui/viewer/file/file_app_bar.dart";
import "package:photos/ui/viewer/file/file_bottom_bar.dart";
@@ -64,16 +63,30 @@ class DetailPageConfiguration {
}
}
class DetailPage extends StatefulWidget {
class DetailPage extends StatelessWidget {
final DetailPageConfiguration config;
const DetailPage(this.config, {super.key});
@override
State<DetailPage> createState() => _DetailPageState();
Widget build(BuildContext context) {
// Separating body to a different widget to avoid
// unnecessary reinitialization of the InheritedDetailPageState
// when the body is rebuilt, which can reset state stored in it.
return InheritedDetailPageState(child: _Body(config));
}
}
class _DetailPageState extends State<DetailPage> {
class _Body extends StatefulWidget {
final DetailPageConfiguration config;
const _Body(this.config);
@override
State<_Body> createState() => _BodyState();
}
class _BodyState extends State<_Body> {
final _logger = Logger("DetailPageState");
bool _shouldDisableScroll = false;
List<EnteFile>? _files;
@@ -137,102 +150,100 @@ class _DetailPageState extends State<DetailPage> {
_files!.length.toString() +
" files .",
);
return InheritedDetailPageState(
child: PopScope(
canPop: !isGuestView,
onPopInvokedWithResult: (didPop, _) async {
if (isGuestView) {
final authenticated = await _requestAuthentication();
if (authenticated) {
Bus.instance.fire(GuestViewEvent(false, false));
await localSettings.setOnGuestView(false);
}
return PopScope(
canPop: !isGuestView,
onPopInvokedWithResult: (didPop, _) async {
if (isGuestView) {
final authenticated = await _requestAuthentication();
if (authenticated) {
Bus.instance.fire(GuestViewEvent(false, false));
await localSettings.setOnGuestView(false);
}
},
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(80),
child: ValueListenableBuilder(
builder: (BuildContext context, int selectedIndex, _) {
return FileAppBar(
_files![selectedIndex],
_onFileRemoved,
widget.config.mode == DetailPageMode.full,
enableFullScreenNotifier: InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
);
},
valueListenable: _selectedIndexNotifier,
),
}
},
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(80),
child: ValueListenableBuilder(
builder: (BuildContext context, int selectedIndex, _) {
return FileAppBar(
_files![selectedIndex],
_onFileRemoved,
widget.config.mode == DetailPageMode.full,
enableFullScreenNotifier: InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
);
},
valueListenable: _selectedIndexNotifier,
),
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: false,
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
_buildPageView(),
ValueListenableBuilder(
builder: (BuildContext context, int selectedIndex, _) {
return FileBottomBar(
_files![selectedIndex],
_onNewImageEditor,
widget.config.mode == DetailPageMode.minimalistic &&
!isGuestView,
onFileRemoved: _onFileRemoved,
userID: Configuration.instance.getUserID(),
enableFullScreenNotifier:
InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
);
},
valueListenable: _selectedIndexNotifier,
),
ValueListenableBuilder(
valueListenable: _selectedIndexNotifier,
builder: (BuildContext context, int selectedIndex, _) {
if (_files![selectedIndex].isPanorama() == true) {
return ValueListenableBuilder(
valueListenable: InheritedDetailPageState.of(context)
),
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: false,
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
_buildPageView(),
ValueListenableBuilder(
builder: (BuildContext context, int selectedIndex, _) {
return FileBottomBar(
_files![selectedIndex],
_onEditFileRequested,
widget.config.mode == DetailPageMode.minimalistic &&
!isGuestView,
onFileRemoved: _onFileRemoved,
userID: Configuration.instance.getUserID(),
enableFullScreenNotifier:
InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
builder: (context, value, child) {
return IgnorePointer(
ignoring: value,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: !value ? 1.0 : 0.0,
child: Align(
alignment: Alignment.center,
child: Tooltip(
message: S.of(context).panorama,
child: IconButton(
style: IconButton.styleFrom(
backgroundColor: const Color(0xAA252525),
fixedSize: const Size(44, 44),
),
icon: const Icon(
Icons.threesixty,
color: Colors.white,
size: 26,
),
onPressed: () async {
await openPanoramaViewerPage(
_files![selectedIndex],
);
},
);
},
valueListenable: _selectedIndexNotifier,
),
ValueListenableBuilder(
valueListenable: _selectedIndexNotifier,
builder: (BuildContext context, int selectedIndex, _) {
if (_files![selectedIndex].isPanorama() == true) {
return ValueListenableBuilder(
valueListenable: InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
builder: (context, value, child) {
return IgnorePointer(
ignoring: value,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: !value ? 1.0 : 0.0,
child: Align(
alignment: Alignment.center,
child: Tooltip(
message: S.of(context).panorama,
child: IconButton(
style: IconButton.styleFrom(
backgroundColor: const Color(0xAA252525),
fixedSize: const Size(44, 44),
),
icon: const Icon(
Icons.threesixty,
color: Colors.white,
size: 26,
),
onPressed: () async {
await openPanoramaViewerPage(
_files![selectedIndex],
);
},
),
),
),
);
},
);
}
return const SizedBox();
},
),
],
),
),
);
},
);
}
return const SizedBox();
},
),
],
),
),
),
@@ -358,68 +369,6 @@ class _DetailPageState extends State<DetailPage> {
}
}
Future<void> _onNewImageEditor(EnteFile file) async {
if (file.uploadedFileID != null &&
file.ownerID != Configuration.instance.getUserID()) {
_logger.severe(
"Attempt to edit unowned file",
UnauthorizedEditError(),
StackTrace.current,
);
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).weDontSupportEditingPhotosAndAlbumsThatYouDont,
);
return;
}
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
await dialog.show();
try {
final ioFile = await getFile(file);
if (ioFile == null) {
showShortToast(context, S.of(context).failedToFetchOriginalForEdit);
await dialog.hide();
return;
}
if (file.fileType == FileType.video) {
await dialog.hide();
replacePage(
context,
VideoEditorPage(
file: file,
ioFile: ioFile,
detailPageConfig: widget.config.copyWith(
files: _files,
selectedIndex: _selectedIndexNotifier.value,
),
),
);
return;
}
final imageProvider =
ExtendedFileImageProvider(ioFile, cacheRawData: true);
await precacheImage(imageProvider, context);
await dialog.hide();
replacePage(
context,
NewImageEditor(
originalFile: file,
file: ioFile,
detailPageConfig: widget.config.copyWith(
files: _files,
selectedIndex: _selectedIndexNotifier.value,
),
),
);
} catch (e) {
await dialog.hide();
_logger.warning("Failed to initiate edit", e);
}
}
Future<void> _onEditFileRequested(EnteFile file) async {
if (file.uploadedFileID != null &&
file.ownerID != Configuration.instance.getUserID()) {
@@ -438,6 +387,7 @@ class _DetailPageState extends State<DetailPage> {
}
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
await dialog.show();
try {
final ioFile = await getFile(file);
if (ioFile == null) {
@@ -467,9 +417,9 @@ class _DetailPageState extends State<DetailPage> {
replacePage(
context,
ImageEditorPage(
imageProvider,
file,
widget.config.copyWith(
originalFile: file,
file: ioFile,
detailPageConfig: widget.config.copyWith(
files: _files,
selectedIndex: _selectedIndexNotifier.value,
),

View File

@@ -519,7 +519,8 @@ class FileUploader {
DateTime.now().microsecondsSinceEpoch,
);
} catch (e) {
_logger.warning("Lock was already taken for " + file.toString());
final lockInfo = await _uploadLocks.getLockData(lockKey);
_logger.warning("Lock was already taken ($lockInfo) for " + file.tag);
throw LockAlreadyAcquiredError();
}

View File

@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
version: "72.0.0"
version: "76.0.0"
_flutterfire_internals:
dependency: transitive
description:
@@ -21,7 +21,7 @@ packages:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
version: "0.3.3"
adaptive_theme:
dependency: "direct main"
description:
@@ -34,10 +34,10 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
version: "6.7.0"
version: "6.11.0"
android_intent_plus:
dependency: "direct main"
description:
@@ -317,10 +317,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
computer:
dependency: "direct main"
description:
@@ -1271,38 +1271,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.3.0"
image_editor:
dependency: "direct main"
description:
name: image_editor
sha256: "38070067264fd9fea4328ca630d2ff7bd65ebe6aa4ed375d983b732d2ae7146b"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
image_editor_common:
dependency: transitive
description:
name: image_editor_common
sha256: "93d2f5c8b636f862775dd62a9ec20d09c8272598daa02f935955a4640e1844ee"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
image_editor_ohos:
dependency: transitive
description:
name: image_editor_ohos
sha256: "06756859586d5acefec6e3b4f356f9b1ce05ef09213bcb9a0ce1680ecea2d054"
url: "https://pub.dev"
source: hosted
version: "0.0.9"
image_editor_platform_interface:
dependency: transitive
description:
name: image_editor_platform_interface
sha256: "474517efc770464f7d99942472d8cfb369a3c378e95466ec17f74d2b80bd40de"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
in_app_purchase:
dependency: "direct main"
description:
@@ -1424,18 +1392,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
@@ -1544,10 +1512,10 @@ packages:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
version: "0.1.3-main.0"
maps_launcher:
dependency: "direct main"
description:
@@ -1586,24 +1554,24 @@ packages:
description:
path: media_kit
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.1.11"
version: "1.2.0"
media_kit_libs_android_video:
dependency: transitive
description:
name: media_kit_libs_android_video
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7
url: "https://pub.dev"
source: hosted
version: "1.3.6"
version: "1.3.7"
media_kit_libs_ios_video:
dependency: "direct main"
description:
path: "libs/ios/media_kit_libs_ios_video"
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.1.4"
@@ -1611,10 +1579,10 @@ packages:
dependency: transitive
description:
name: media_kit_libs_linux
sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310
sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
version: "1.2.1"
media_kit_libs_macos_video:
dependency: transitive
description:
@@ -1628,27 +1596,27 @@ packages:
description:
path: "libs/universal/media_kit_libs_video"
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.0.5"
version: "1.0.6"
media_kit_libs_windows_video:
dependency: transitive
description:
name: media_kit_libs_windows_video
sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887"
sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab
url: "https://pub.dev"
source: hosted
version: "1.0.10"
version: "1.0.11"
media_kit_video:
dependency: "direct main"
description:
path: media_kit_video
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.2.5"
version: "1.3.0"
meta:
dependency: transitive
description:
@@ -1712,7 +1680,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "7814e2c61ee1fa74cef73b946eb08519c35bdaa5"
resolved-ref: "64e47a446bf3b64f012f2076481cebea51ca27cf"
url: "https://github.com/ente-io/motionphoto.git"
source: git
version: "0.0.1"
@@ -2325,7 +2293,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
version: "0.0.0"
source_gen:
dependency: transitive
description:
@@ -2450,10 +2418,10 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.12.0"
step_progress_indicator:
dependency: "direct main"
description:
@@ -2482,10 +2450,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
styled_text:
dependency: "direct main"
description:
@@ -2546,26 +2514,26 @@ packages:
dependency: "direct dev"
description:
name: test
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
url: "https://pub.dev"
source: hosted
version: "1.25.7"
version: "1.25.8"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.7.3"
test_core:
dependency: transitive
description:
name: test_core
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
url: "https://pub.dev"
source: hosted
version: "0.6.4"
version: "0.6.5"
thermal:
dependency: "direct main"
description:
@@ -2845,10 +2813,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "14.2.5"
version: "14.3.0"
volume_controller:
dependency: transitive
description:
@@ -2909,10 +2877,10 @@ packages:
dependency: transitive
description:
name: webdriver
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.4"
webkit_inspection_protocol:
dependency: transitive
description:

View File

@@ -114,7 +114,6 @@ dependencies:
html_unescape: ^2.0.0
http: ^1.1.0
image: ^4.0.17
image_editor: ^1.6.0
in_app_purchase: ^3.0.7
intl: ^0.19.0
latlong2: ^0.9.0

View File

@@ -5,6 +5,9 @@ import (
"database/sql"
b64 "encoding/base64"
"fmt"
"github.com/ente-io/museum/pkg/controller/collections"
publicCtrl "github.com/ente-io/museum/pkg/controller/public"
"github.com/ente-io/museum/pkg/repo/public"
"net/http"
"os"
"os/signal"
@@ -14,8 +17,6 @@ import (
"syscall"
"time"
"github.com/ente-io/museum/pkg/controller/collections"
"github.com/ente-io/museum/ente/base"
"github.com/ente-io/museum/pkg/controller/emergency"
"github.com/ente-io/museum/pkg/controller/file_copy"
@@ -97,6 +98,7 @@ func main() {
}
viper.SetDefault("apps.public-albums", "https://albums.ente.io")
viper.SetDefault("apps.public-locker", "https://locker.ente.io")
viper.SetDefault("apps.accounts", "https://accounts.ente.io")
viper.SetDefault("apps.cast", "https://cast.ente.io")
viper.SetDefault("apps.family", "https://family.ente.io")
@@ -174,11 +176,13 @@ func main() {
fileRepo := &repo.FileRepository{DB: db, S3Config: s3Config, QueueRepo: queueRepo,
ObjectRepo: objectRepo, ObjectCleanupRepo: objectCleanupRepo,
ObjectCopiesRepo: objectCopiesRepo, UsageRepo: usageRepo}
fileLinkRepo := public.NewFileLinkRepo(db)
fileDataRepo := &fileDataRepo.Repository{DB: db, ObjectCleanupRepo: objectCleanupRepo}
familyRepo := &repo.FamilyRepository{DB: db}
trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo}
publicCollectionRepo := repo.NewPublicCollectionRepository(db, viper.GetString("apps.public-albums"))
collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, PublicCollectionRepo: publicCollectionRepo,
trashRepo := &repo.TrashRepository{DB: db, ObjectRepo: objectRepo, FileRepo: fileRepo, QueueRepo: queueRepo, FileLinkRepo: fileLinkRepo}
collectionLinkRepo := public.NewCollectionLinkRepository(db, viper.GetString("apps.public-albums"))
collectionRepo := &repo.CollectionRepository{DB: db, FileRepo: fileRepo, CollectionLinkRepo: collectionLinkRepo,
TrashRepo: trashRepo, SecretEncryptionKey: secretEncryptionKeyBytes, QueueRepo: queueRepo, LatencyLogger: latencyLogger}
pushRepo := &repo.PushTokenRepository{DB: db}
kexRepo := &kex.Repository{
@@ -300,26 +304,27 @@ func main() {
UsageRepo: usageRepo,
}
publicCollectionCtrl := &controller.PublicCollectionController{
collectionLinkCtrl := &publicCtrl.CollectionLinkController{
FileController: fileController,
EmailNotificationCtrl: emailNotificationCtrl,
PublicCollectionRepo: publicCollectionRepo,
CollectionLinkRepo: collectionLinkRepo,
FileLinkRepo: fileLinkRepo,
CollectionRepo: collectionRepo,
UserRepo: userRepo,
JwtSecret: jwtSecretBytes,
}
collectionController := &collections.CollectionController{
CollectionRepo: collectionRepo,
EmailCtrl: emailNotificationCtrl,
AccessCtrl: accessCtrl,
PublicCollectionCtrl: publicCollectionCtrl,
UserRepo: userRepo,
FileRepo: fileRepo,
CastRepo: &castDb,
BillingCtrl: billingController,
QueueRepo: queueRepo,
TaskRepo: taskLockingRepo,
CollectionRepo: collectionRepo,
EmailCtrl: emailNotificationCtrl,
AccessCtrl: accessCtrl,
CollectionLinkCtrl: collectionLinkCtrl,
UserRepo: userRepo,
FileRepo: fileRepo,
CastRepo: &castDb,
BillingCtrl: billingController,
QueueRepo: queueRepo,
TaskRepo: taskLockingRepo,
}
kexCtrl := &kexCtrl.Controller{
@@ -351,6 +356,12 @@ func main() {
userCache,
userCacheCtrl,
)
fileLinkCtrl := &publicCtrl.FileLinkController{
FileController: fileController,
FileLinkRepo: fileLinkRepo,
FileRepo: fileRepo,
JwtSecret: jwtSecretBytes,
}
passkeyCtrl := &controller.PasskeyController{
Repo: passkeysRepo,
@@ -358,14 +369,21 @@ func main() {
}
authMiddleware := middleware.AuthMiddleware{UserAuthRepo: userAuthRepo, Cache: authCache, UserController: userController}
accessTokenMiddleware := middleware.AccessTokenMiddleware{
PublicCollectionRepo: publicCollectionRepo,
PublicCollectionCtrl: publicCollectionCtrl,
collectionLinkMiddleware := middleware.CollectionLinkMiddleware{
CollectionLinkRepo: collectionLinkRepo,
PublicCollectionCtrl: collectionLinkCtrl,
CollectionRepo: collectionRepo,
Cache: accessTokenCache,
BillingCtrl: billingController,
DiscordController: discordController,
}
fileLinkMiddleware := &middleware.FileLinkMiddleware{
FileLinkRepo: fileLinkRepo,
FileLinkCtrl: fileLinkCtrl,
Cache: accessTokenCache,
BillingCtrl: billingController,
DiscordController: discordController,
}
if environment != "local" {
gin.SetMode(gin.ReleaseMode)
@@ -404,7 +422,9 @@ func main() {
familiesJwtAuthAPI.Use(rateLimiter.GlobalRateLimiter(), authMiddleware.TokenAuthMiddleware(jwt.FAMILIES.Ptr()), rateLimiter.APIRateLimitForUserMiddleware(urlSanitizer))
publicCollectionAPI := server.Group("/public-collection")
publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), accessTokenMiddleware.AccessTokenAuthMiddleware(urlSanitizer))
publicCollectionAPI.Use(rateLimiter.GlobalRateLimiter(), collectionLinkMiddleware.Authenticate(urlSanitizer))
fileLinkApi := server.GET("/file-link")
fileLinkApi.Use(rateLimiter.GlobalRateLimiter(), fileLinkMiddleware.Authenticate(urlSanitizer))
healthCheckHandler := &api.HealthCheckHandler{
DB: db,
@@ -432,6 +452,7 @@ func main() {
Controller: fileController,
FileCopyCtrl: fileCopyCtrl,
FileDataCtrl: fileDataCtrl,
FileUrlCtrl: fileLinkCtrl,
}
privateAPI.GET("/files/upload-urls", fileHandler.GetUploadURLs)
privateAPI.GET("/files/multipart-upload-urls", fileHandler.GetMultipartUploadURLs)
@@ -440,6 +461,11 @@ func main() {
privateAPI.GET("/files/preview/:fileID", fileHandler.GetThumbnail)
privateAPI.GET("/files/preview/v2/:fileID", fileHandler.GetThumbnail)
privateAPI.POST("/files/share-url", fileHandler.ShareUrl)
privateAPI.PUT("/files/share-url", fileHandler.UpdateFileURL)
privateAPI.DELETE("/files/share-url/:fileID", fileHandler.DisableUrl)
privateAPI.GET("/files/share-urls/", fileHandler.GetUrls)
privateAPI.PUT("/files/data", fileHandler.PutFileData)
privateAPI.PUT("/files/video-data", fileHandler.PutVideoData)
privateAPI.POST("/files/data/status-diff", fileHandler.FileDataStatusDiff)
@@ -566,13 +592,19 @@ func main() {
privateAPI.PUT("/collections/sharee-magic-metadata", collectionHandler.ShareeMagicMetadataUpdate)
publicCollectionHandler := &api.PublicCollectionHandler{
Controller: publicCollectionCtrl,
Controller: collectionLinkCtrl,
FileCtrl: fileController,
CollectionCtrl: collectionController,
FileDataCtrl: fileDataCtrl,
StorageBonusController: storageBonusCtrl,
}
fileLinkApi.GET("/info", fileHandler.LinkInfo)
fileLinkApi.GET("/pass-info", fileHandler.PasswordInfo)
fileLinkApi.GET("/thumbnail", fileHandler.LinkThumbnail)
fileLinkApi.GET("/file", fileHandler.LinkFile)
fileLinkApi.POST("/verify-password", fileHandler.VerifyPassword)
publicCollectionAPI.GET("/files/preview/:fileID", publicCollectionHandler.GetThumbnail)
publicCollectionAPI.GET("/files/download/:fileID", publicCollectionHandler.GetFile)
publicCollectionAPI.GET("/files/data/fetch", publicCollectionHandler.GetFileData)
@@ -770,7 +802,7 @@ func main() {
setKnownAPIs(server.Routes())
setupAndStartBackgroundJobs(objectCleanupController, replicationController3, fileDataCtrl)
setupAndStartCrons(
userAuthRepo, publicCollectionRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl,
userAuthRepo, collectionLinkRepo, fileLinkRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl,
trashController, pushController, objectController, dataCleanupController, storageBonusCtrl, emergencyCtrl,
embeddingController, healthCheckHandler, kexCtrl, castDb)
@@ -899,7 +931,8 @@ func setupAndStartBackgroundJobs(
objectCleanupController.StartClearingOrphanObjects()
}
func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionRepo *repo.PublicCollectionRepository,
func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, collectionLinkRepo *public.CollectionLinkRepo,
fileLinkRepo *public.FileLinkRepository,
twoFactorRepo *repo.TwoFactorRepository, passkeysRepo *passkey.Repository, fileController *controller.FileController,
taskRepo *repo.TaskLockRepository, emailNotificationCtrl *email.EmailNotificationController,
trashController *controller.TrashController, pushController *controller.PushController,
@@ -925,7 +958,8 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR
schedule(c, "@every 24h", func() {
_ = userAuthRepo.RemoveDeletedTokens(timeUtil.MicrosecondsBeforeDays(30))
_ = castDb.DeleteOldSessions(context.Background(), timeUtil.MicrosecondsBeforeDays(7))
_ = publicCollectionRepo.CleanupAccessHistory(context.Background())
_ = collectionLinkRepo.CleanupAccessHistory(context.Background())
_ = fileLinkRepo.CleanupAccessHistory(context.Background())
})
schedule(c, "@every 1m", func() {

View File

@@ -79,9 +79,14 @@ http:
apps:
# Default is https://albums.ente.io
#
# If you're running a self hosted instance and wish to serve public links,
# If you're running a self hosted instance and wish to serve public links for photos,
# set this to the URL where your albums web app is running.
public-albums:
# Default is https://locker.ente.io
#
# If you're running a self-hosted instance and wish to serve public links for locker,
# set this to the URL where your albums web app is running.
public-locker:
# Default is https://cast.ente.io
cast:
# Default is https://accounts.ente.io

View File

@@ -97,8 +97,8 @@ var ErrUserDeleted = errors.New("user account has been deleted")
// ErrLockUnavailable is thrown when a lock could not be acquired
var ErrLockUnavailable = errors.New("could not acquire lock")
// ErrActiveLinkAlreadyExists is thrown when the collection already has active public link
var ErrActiveLinkAlreadyExists = errors.New("Collection already has active public link")
// ErrActiveLinkAlreadyExists is thrown when an active link already exists for entity
var ErrActiveLinkAlreadyExists = errors.New("link already exists for this entity")
// ErrNotImplemented indicates that the action that we tried to perform is not
// available at this museum instance. e.g. this could be something that is not
@@ -176,6 +176,11 @@ var ErrMaxPasskeysReached = ApiError{
Message: "Max passkeys limit reached",
HttpStatusCode: http.StatusConflict,
}
var ErrPassProtectedResource = ApiError{
Code: "PASS_PROTECTED_RESOURCE",
Message: "This resource is password protected",
HttpStatusCode: http.StatusForbidden,
}
var ErrCastPermissionDenied = ApiError{
Code: "CAST_PERMISSION_DENIED",

94
server/ente/file_link.go Normal file
View File

@@ -0,0 +1,94 @@
package ente
import (
"fmt"
"github.com/ente-io/museum/pkg/utils/time"
)
// CreateFileUrl represents an encrypted file in the system
type CreateFileUrl struct {
FileID int64 `json:"fileID" binding:"required"`
App App `json:"app" binding:"required"`
}
// UpdateFileUrl ..
type UpdateFileUrl struct {
LinkID string `json:"linkID" binding:"required"`
FileID int64 `json:"fileID" binding:"required"`
ValidTill *int64 `json:"validTill"`
DeviceLimit *int `json:"deviceLimit"`
PassHash *string
Nonce *string
MemLimit *int64
OpsLimit *int64
EnableDownload *bool `json:"enableDownload"`
DisablePassword *bool `json:"disablePassword"`
}
func (ut *UpdateFileUrl) Validate() error {
if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil &&
ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil {
return NewBadRequestWithMessage("all parameters are missing")
}
if ut.DeviceLimit != nil && (*ut.DeviceLimit < 0 || *ut.DeviceLimit > 50) {
return NewBadRequestWithMessage(fmt.Sprintf("device limit: %d out of range [0-50]", *ut.DeviceLimit))
}
if ut.ValidTill != nil && *ut.ValidTill != 0 && *ut.ValidTill < time.Microseconds() {
return NewBadRequestWithMessage("valid till should be greater than current timestamp")
}
var allPassParamsMissing = ut.Nonce == nil && ut.PassHash == nil && ut.MemLimit == nil && ut.OpsLimit == nil
var allPassParamsPresent = ut.Nonce != nil && ut.PassHash != nil && ut.MemLimit != nil && ut.OpsLimit != nil
if !(allPassParamsMissing || allPassParamsPresent) {
return NewBadRequestWithMessage("all password params should be either present or missing")
}
if allPassParamsPresent && ut.DisablePassword != nil && *ut.DisablePassword {
return NewBadRequestWithMessage("can not set and disable password in same request")
}
return nil
}
type FileLinkRow struct {
LinkID string
OwnerID int64
FileID int64
Token string
DeviceLimit int
ValidTill int64
IsDisabled bool
PassHash *string
Nonce *string
MemLimit *int64
OpsLimit *int64
EnableDownload bool
CreatedAt int64
UpdatedAt int64
}
type FileUrl struct {
LinkID string `json:"linkID" binding:"required"`
URL string `json:"url" binding:"required"`
OwnerID int64 `json:"ownerID" binding:"required"`
FileID int64 `json:"fileID" binding:"required"`
ValidTill int64 `json:"validTill"`
DeviceLimit int `json:"deviceLimit"`
PasswordEnabled bool `json:"passwordEnabled"`
// Nonce contains the nonce value for the password if the link is password protected.
Nonce *string `json:"nonce,omitempty"`
MemLimit *int64 `json:"memLimit,omitempty"`
OpsLimit *int64 `json:"opsLimit,omitempty"`
EnableDownload bool `json:"enableDownload"`
CreatedAt int64 `json:"createdAt"`
}
type FileLinkAccessContext struct {
LinkID string
IP string
UserAgent string
FileID int64
OwnerID int64
}

View File

@@ -40,13 +40,13 @@ func (w WebCommonJWTClaim) Valid() error {
return nil
}
// PublicAlbumPasswordClaim refer to token granted post public album password verification
type PublicAlbumPasswordClaim struct {
// LinkPasswordClaim refer to token granted post link password verification
type LinkPasswordClaim struct {
PassHash string `json:"passKey"`
ExpiryTime int64 `json:"expiryTime"`
}
func (c PublicAlbumPasswordClaim) Valid() error {
func (c LinkPasswordClaim) Valid() error {
if c.ExpiryTime < time.Microseconds() {
return errors.New("token expired")
}

View File

@@ -3,7 +3,7 @@ package ente
import (
"database/sql/driver"
"encoding/json"
"fmt"
"github.com/ente-io/museum/pkg/utils/time"
"github.com/ente-io/stacktrace"
)
@@ -32,6 +32,33 @@ type UpdatePublicAccessTokenRequest struct {
EnableJoin *bool `json:"enableJoin"`
}
func (ut *UpdatePublicAccessTokenRequest) Validate() error {
if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil &&
ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil && ut.EnableCollect == nil {
return NewBadRequestWithMessage("all parameters are missing")
}
if ut.DeviceLimit != nil && (*ut.DeviceLimit < 0 || *ut.DeviceLimit > 50) {
return NewBadRequestWithMessage(fmt.Sprintf("device limit: %d out of range [0-50]", *ut.DeviceLimit))
}
if ut.ValidTill != nil && *ut.ValidTill != 0 && *ut.ValidTill < time.Microseconds() {
return NewBadRequestWithMessage("valid till should be greater than current timestamp")
}
var allPassParamsMissing = ut.Nonce == nil && ut.PassHash == nil && ut.MemLimit == nil && ut.OpsLimit == nil
var allPassParamsPresent = ut.Nonce != nil && ut.PassHash != nil && ut.MemLimit != nil && ut.OpsLimit != nil
if !(allPassParamsMissing || allPassParamsPresent) {
return NewBadRequestWithMessage("all password params should be either present or missing")
}
if allPassParamsPresent && ut.DisablePassword != nil && *ut.DisablePassword {
return NewBadRequestWithMessage("can not set and disable password in same request")
}
return nil
}
type VerifyPasswordRequest struct {
PassHash string `json:"passHash" binding:"required"`
}
@@ -40,8 +67,8 @@ type VerifyPasswordResponse struct {
JWTToken string `json:"jwtToken"`
}
// PublicCollectionToken represents row entity for public_collection_token table
type PublicCollectionToken struct {
// CollectionLinkRow represents row entity for public_collection_token table
type CollectionLinkRow struct {
ID int64
CollectionID int64
Token string
@@ -57,7 +84,7 @@ type PublicCollectionToken struct {
EnableJoin bool
}
func (p PublicCollectionToken) CanJoin() error {
func (p CollectionLinkRow) CanJoin() error {
if p.IsDisabled {
return NewBadRequestWithMessage("link disabled")
}

View File

@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS public_file_tokens_access_history;
DROP TABLE IF EXISTS public_file_tokens;

View File

@@ -0,0 +1,46 @@
CREATE TABLE IF NOT EXISTS public_file_tokens
(
id text primary key,
file_id bigint NOT NULL,
owner_id bigint NOT NULL,
app text NOT NULL,
access_token text not null,
valid_till bigint not null DEFAULT 0,
device_limit int not null DEFAULT 0,
is_disabled bool not null DEFAULT FALSE,
enable_download bool not null DEFAULT TRUE,
pw_hash TEXT,
pw_nonce TEXT,
mem_limit BIGINT,
ops_limit BIGINT,
created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(),
updated_at bigint NOT NULL DEFAULT now_utc_micro_seconds()
);
CREATE OR REPLACE TRIGGER update_public_file_tokens_updated_at
BEFORE UPDATE
ON public_file_tokens
FOR EACH ROW
EXECUTE PROCEDURE
trigger_updated_at_microseconds_column();
CREATE TABLE IF NOT EXISTS public_file_tokens_access_history
(
id text NOT NULL,
ip text not null,
user_agent text not null,
created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(),
CONSTRAINT unique_access_id_ip_ua UNIQUE (id, ip, user_agent),
CONSTRAINT fk_public_file_history_token_id
FOREIGN KEY (id)
REFERENCES public_file_tokens (id)
ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS public_file_token_unique_idx ON public_file_tokens (access_token) WHERE is_disabled = FALSE;
CREATE INDEX IF NOT EXISTS public_file_tokens_owner_id_updated_at_idx ON public_file_tokens (owner_id, updated_at);
CREATE UNIQUE INDEX IF NOT EXISTS public_active_file_link_unique_idx ON public_file_tokens (file_id, is_disabled) WHERE is_disabled = FALSE;

View File

@@ -10,7 +10,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/ente-io/museum/ente"
"github.com/ente-io/museum/pkg/controller"
"github.com/ente-io/museum/pkg/utils/auth"
"github.com/ente-io/museum/pkg/utils/handler"
"github.com/ente-io/museum/pkg/utils/time"
@@ -172,35 +171,6 @@ func (h *CollectionHandler) UpdateShareURL(c *gin.Context) {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
if req.DeviceLimit == nil && req.ValidTill == nil && req.DisablePassword == nil &&
req.Nonce == nil && req.PassHash == nil && req.EnableDownload == nil && req.EnableCollect == nil {
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "all parameters are missing"))
return
}
if req.DeviceLimit != nil && (*req.DeviceLimit < 0 || *req.DeviceLimit > controller.DeviceLimitThreshold) {
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("device limit: %d out of range", *req.DeviceLimit)))
return
}
if req.ValidTill != nil && *req.ValidTill != 0 && *req.ValidTill < time.Microseconds() {
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "valid till should be greater than current timestamp"))
return
}
var allPassParamsMissing = req.Nonce == nil && req.PassHash == nil && req.MemLimit == nil && req.OpsLimit == nil
var allPassParamsPresent = req.Nonce != nil && req.PassHash != nil && req.MemLimit != nil && req.OpsLimit != nil
if !(allPassParamsMissing || allPassParamsPresent) {
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "all password params should be either present or missing"))
return
}
if allPassParamsPresent && req.DisablePassword != nil && *req.DisablePassword {
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "can not set and disable password in same request"))
return
}
response, err := h.Controller.UpdateShareURL(c, auth.GetUserID(c.Request.Header), req)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/ente-io/museum/pkg/controller/file_copy"
"github.com/ente-io/museum/pkg/controller/filedata"
"github.com/ente-io/museum/pkg/controller/public"
"net/http"
"os"
"strconv"
@@ -24,6 +25,7 @@ import (
// FileHandler exposes request handlers for all encrypted file related requests
type FileHandler struct {
Controller *controller.FileController
FileUrlCtrl *public.FileLinkController
FileCopyCtrl *file_copy.FileCopyController
FileDataCtrl *filedata.Controller
}

141
server/pkg/api/file_link.go Normal file
View File

@@ -0,0 +1,141 @@
package api
import (
"github.com/ente-io/museum/ente"
"github.com/ente-io/museum/pkg/utils/auth"
"github.com/ente-io/museum/pkg/utils/handler"
"github.com/ente-io/stacktrace"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)
// ShareUrl a sharable url for the file
func (h *FileHandler) ShareUrl(c *gin.Context) {
var file ente.CreateFileUrl
if err := c.ShouldBindJSON(&file); err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
response, err := h.FileUrlCtrl.CreateLink(c, file)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
c.JSON(http.StatusOK, response)
}
func (h *FileHandler) LinkInfo(c *gin.Context) {
resp, err := h.FileUrlCtrl.Info(c)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
c.JSON(http.StatusOK, gin.H{
"file": resp,
})
}
func (h *FileHandler) PasswordInfo(c *gin.Context) {
resp, err := h.FileUrlCtrl.PassInfo(c)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
c.JSON(http.StatusOK, gin.H{
"nonce": resp.Nonce,
"opsLimit": resp.OpsLimit,
"memLimit": resp.MemLimit,
})
}
func (h *FileHandler) LinkThumbnail(c *gin.Context) {
linkCtx := auth.MustGetFileLinkAccessContext(c)
url, err := h.Controller.GetThumbnailURL(c, linkCtx.OwnerID, linkCtx.FileID)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
c.Redirect(http.StatusTemporaryRedirect, url)
}
func (h *FileHandler) LinkFile(c *gin.Context) {
linkCtx := auth.MustGetFileLinkAccessContext(c)
url, err := h.Controller.GetFileURL(c, linkCtx.OwnerID, linkCtx.FileID)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
c.Redirect(http.StatusTemporaryRedirect, url)
}
func (h *FileHandler) DisableUrl(c *gin.Context) {
cID, err := strconv.ParseInt(c.Param("fileID"), 10, 64)
if err != nil {
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, ""))
return
}
err = h.FileUrlCtrl.Disable(c, cID)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
c.JSON(http.StatusOK, gin.H{})
}
func (h *FileHandler) GetUrls(c *gin.Context) {
sinceTime, err := strconv.ParseInt(c.Query("sinceTime"), 10, 64)
if err != nil {
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "sinceTime parsing failed"))
return
}
limit := 500
if c.Query("limit") != "" {
limit, err = strconv.Atoi(c.Query("limit"))
if err != nil || limit < 1 {
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, ""))
return
}
}
response, err := h.FileUrlCtrl.GetUrls(c, sinceTime, int64(limit))
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
c.JSON(http.StatusOK, gin.H{
"diff": response,
})
}
// VerifyPassword verifies the password for given link access token and return signed jwt token if it's valid
func (h *FileHandler) VerifyPassword(c *gin.Context) {
var req ente.VerifyPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
resp, err := h.FileUrlCtrl.VerifyPassword(c, req)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
c.JSON(http.StatusOK, resp)
}
// UpdateFileURL updates the share URL for a file
func (h *FileHandler) UpdateFileURL(c *gin.Context) {
var req ente.UpdateFileUrl
if err := c.ShouldBindJSON(&req); err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
response, err := h.FileUrlCtrl.UpdateSharedUrl(c, req)
if err != nil {
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
c.JSON(http.StatusOK, gin.H{
"result": response,
})
}

View File

@@ -5,6 +5,7 @@ import (
fileData "github.com/ente-io/museum/ente/filedata"
"github.com/ente-io/museum/pkg/controller/collections"
"github.com/ente-io/museum/pkg/controller/filedata"
"github.com/ente-io/museum/pkg/controller/public"
"net/http"
"strconv"
@@ -20,7 +21,7 @@ import (
// PublicCollectionHandler exposes request handlers for publicly accessible collections
type PublicCollectionHandler struct {
Controller *controller.PublicCollectionController
Controller *public.CollectionLinkController
FileCtrl *controller.FileController
CollectionCtrl *collections.CollectionController
FileDataCtrl *filedata.Controller

View File

@@ -6,6 +6,7 @@ import (
"github.com/ente-io/museum/pkg/controller"
"github.com/ente-io/museum/pkg/controller/access"
"github.com/ente-io/museum/pkg/controller/email"
"github.com/ente-io/museum/pkg/controller/public"
"github.com/ente-io/museum/pkg/repo/cast"
"github.com/ente-io/museum/pkg/utils/array"
"github.com/ente-io/museum/pkg/utils/auth"
@@ -24,16 +25,16 @@ const (
// CollectionController encapsulates logic that deals with collections
type CollectionController struct {
PublicCollectionCtrl *controller.PublicCollectionController
EmailCtrl *email.EmailNotificationController
AccessCtrl access.Controller
BillingCtrl *controller.BillingController
CollectionRepo *repo.CollectionRepository
UserRepo *repo.UserRepository
FileRepo *repo.FileRepository
QueueRepo *repo.QueueRepository
CastRepo *cast.Repository
TaskRepo *repo.TaskLockRepository
CollectionLinkCtrl *public.CollectionLinkController
EmailCtrl *email.EmailNotificationController
AccessCtrl access.Controller
BillingCtrl *controller.BillingController
CollectionRepo *repo.CollectionRepository
UserRepo *repo.UserRepository
FileRepo *repo.FileRepository
QueueRepo *repo.QueueRepository
CastRepo *cast.Repository
TaskRepo *repo.TaskLockRepository
}
// Create creates a collection
@@ -148,7 +149,7 @@ func (c *CollectionController) TrashV3(ctx *gin.Context, req ente.TrashCollectio
}
}
err = c.PublicCollectionCtrl.Disable(ctx, cID)
err = c.CollectionLinkCtrl.Disable(ctx, cID)
if err != nil {
return stacktrace.Propagate(err, "failed to disabled public share url")
}
@@ -209,7 +210,7 @@ func (c *CollectionController) HandleAccountDeletion(ctx context.Context, userID
if err != nil {
return stacktrace.Propagate(err, "failed to revoke cast token for user")
}
err = c.PublicCollectionCtrl.HandleAccountDeletion(ctx, userID, logger)
err = c.CollectionLinkCtrl.HandleAccountDeletion(ctx, userID, logger)
return stacktrace.Propagate(err, "")
}

View File

@@ -70,21 +70,21 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec
if !collection.AllowSharing() {
return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("joining %s is not allowed", collection.Type))
}
publicCollectionToken, err := c.PublicCollectionCtrl.GetActivePublicCollectionToken(ctx, req.CollectionID)
collectionLinkToken, err := c.CollectionLinkCtrl.GetActiveCollectionLinkToken(ctx, req.CollectionID)
if err != nil {
return stacktrace.Propagate(err, "")
}
if canJoin := publicCollectionToken.CanJoin(); canJoin != nil {
if canJoin := collectionLinkToken.CanJoin(); canJoin != nil {
return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("can not join collection: %s", canJoin.Error()))
}
accessToken := auth.GetAccessToken(ctx)
if publicCollectionToken.Token != accessToken {
if collectionLinkToken.Token != accessToken {
return stacktrace.Propagate(ente.ErrPermissionDenied, "token doesn't match collection")
}
if publicCollectionToken.PassHash != nil && *publicCollectionToken.PassHash != "" {
if collectionLinkToken.PassHash != nil && *collectionLinkToken.PassHash != "" {
accessTokenJWT := auth.GetAccessTokenJWT(ctx)
if passCheckErr := c.PublicCollectionCtrl.ValidateJWTToken(ctx, accessTokenJWT, *publicCollectionToken.PassHash); passCheckErr != nil {
if passCheckErr := c.CollectionLinkCtrl.ValidateJWTToken(ctx, accessTokenJWT, *collectionLinkToken.PassHash); passCheckErr != nil {
return stacktrace.Propagate(passCheckErr, "")
}
}
@@ -93,7 +93,7 @@ func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollec
return stacktrace.Propagate(err, "")
}
role := ente.VIEWER
if publicCollectionToken.EnableCollect {
if collectionLinkToken.EnableCollect {
role = ente.COLLABORATOR
}
joinErr := c.CollectionRepo.Share(req.CollectionID, collection.Owner.ID, userID, req.EncryptedKey, role, time.Microseconds())
@@ -197,7 +197,7 @@ func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req e
if err != nil {
return ente.PublicURL{}, stacktrace.Propagate(err, "")
}
response, err := c.PublicCollectionCtrl.CreateAccessToken(ctx, req)
response, err := c.CollectionLinkCtrl.CreateLink(ctx, req)
if err != nil {
return ente.PublicURL{}, stacktrace.Propagate(err, "")
}
@@ -205,20 +205,26 @@ func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req e
}
// UpdateShareURL updates the shared url configuration
func (c *CollectionController) UpdateShareURL(ctx context.Context, userID int64, req ente.UpdatePublicAccessTokenRequest) (
ente.PublicURL, error) {
func (c *CollectionController) UpdateShareURL(
ctx context.Context,
userID int64,
req ente.UpdatePublicAccessTokenRequest,
) (*ente.PublicURL, error) {
if err := req.Validate(); err != nil {
return nil, stacktrace.Propagate(err, "")
}
if err := c.verifyOwnership(req.CollectionID, userID); err != nil {
return ente.PublicURL{}, stacktrace.Propagate(err, "")
return nil, stacktrace.Propagate(err, "")
}
err := c.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true)
if err != nil {
return ente.PublicURL{}, stacktrace.Propagate(err, "")
return nil, stacktrace.Propagate(err, "")
}
response, err := c.PublicCollectionCtrl.UpdateSharedUrl(ctx, req)
response, err := c.CollectionLinkCtrl.UpdateSharedUrl(ctx, req)
if err != nil {
return ente.PublicURL{}, stacktrace.Propagate(err, "")
return nil, stacktrace.Propagate(err, "")
}
return response, nil
return &response, nil
}
// DisableSharedURL disable a public auth-token for the given collectionID
@@ -226,7 +232,7 @@ func (c *CollectionController) DisableSharedURL(ctx context.Context, userID int6
if err := c.verifyOwnership(cID, userID); err != nil {
return stacktrace.Propagate(err, "")
}
err := c.PublicCollectionCtrl.Disable(ctx, cID)
err := c.CollectionLinkCtrl.Disable(ctx, cID)
return stacktrace.Propagate(err, "")
}

View File

@@ -1,12 +1,13 @@
package controller
package public
import (
"context"
"errors"
"fmt"
"github.com/ente-io/museum/pkg/controller"
"github.com/ente-io/museum/pkg/repo/public"
"github.com/ente-io/museum/ente"
enteJWT "github.com/ente-io/museum/ente/jwt"
emailCtrl "github.com/ente-io/museum/pkg/controller/email"
"github.com/ente-io/museum/pkg/repo"
"github.com/ente-io/museum/pkg/utils/auth"
@@ -14,7 +15,6 @@ import (
"github.com/ente-io/museum/pkg/utils/time"
"github.com/ente-io/stacktrace"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/lithammer/shortuuid/v3"
"github.com/sirupsen/logrus"
)
@@ -49,23 +49,24 @@ const (
AbuseLimitExceededTemplate = "report_limit_exceeded_alert.html"
)
// PublicCollectionController controls share collection operations
type PublicCollectionController struct {
FileController *FileController
// CollectionLinkController controls share collection operations
type CollectionLinkController struct {
FileController *controller.FileController
EmailNotificationCtrl *emailCtrl.EmailNotificationController
PublicCollectionRepo *repo.PublicCollectionRepository
CollectionLinkRepo *public.CollectionLinkRepo
FileLinkRepo *public.FileLinkRepository
CollectionRepo *repo.CollectionRepository
UserRepo *repo.UserRepository
JwtSecret []byte
}
func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) {
func (c *CollectionLinkController) CreateLink(ctx context.Context, req ente.CreatePublicAccessTokenRequest) (ente.PublicURL, error) {
accessToken := shortuuid.New()[0:AccessTokenLength]
err := c.PublicCollectionRepo.
err := c.CollectionLinkRepo.
Insert(ctx, req.CollectionID, accessToken, req.ValidTill, req.DeviceLimit, req.EnableCollect, req.EnableJoin)
if err != nil {
if errors.Is(err, ente.ErrActiveLinkAlreadyExists) {
collectionToPubUrlMap, err2 := c.PublicCollectionRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID})
collectionToPubUrlMap, err2 := c.CollectionLinkRepo.GetCollectionToActivePublicURLMap(ctx, []int64{req.CollectionID})
if err2 != nil {
return ente.PublicURL{}, stacktrace.Propagate(err2, "")
}
@@ -81,7 +82,7 @@ func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req
}
}
response := ente.PublicURL{
URL: c.PublicCollectionRepo.GetAlbumUrl(accessToken),
URL: c.CollectionLinkRepo.GetAlbumUrl(accessToken),
ValidTill: req.ValidTill,
DeviceLimit: req.DeviceLimit,
EnableDownload: true,
@@ -91,11 +92,11 @@ func (c *PublicCollectionController) CreateAccessToken(ctx context.Context, req
return response, nil
}
func (c *PublicCollectionController) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) {
return c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, collectionID)
func (c *CollectionLinkController) GetActiveCollectionLinkToken(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) {
return c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, collectionID)
}
func (c *PublicCollectionController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) {
func (c *CollectionLinkController) CreateFile(ctx *gin.Context, file ente.File, app ente.App) (ente.File, error) {
collection, err := c.GetPublicCollection(ctx, true)
if err != nil {
return ente.File{}, stacktrace.Propagate(err, "")
@@ -118,13 +119,13 @@ func (c *PublicCollectionController) CreateFile(ctx *gin.Context, file ente.File
}
// Disable all public accessTokens generated for the given cID till date.
func (c *PublicCollectionController) Disable(ctx context.Context, cID int64) error {
err := c.PublicCollectionRepo.DisableSharing(ctx, cID)
func (c *CollectionLinkController) Disable(ctx context.Context, cID int64) error {
err := c.CollectionLinkRepo.DisableSharing(ctx, cID)
return stacktrace.Propagate(err, "")
}
func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) {
publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, req.CollectionID)
func (c *CollectionLinkController) UpdateSharedUrl(ctx context.Context, req ente.UpdatePublicAccessTokenRequest) (ente.PublicURL, error) {
publicCollectionToken, err := c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, req.CollectionID)
if err != nil {
return ente.PublicURL{}, err
}
@@ -154,12 +155,12 @@ func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req en
if req.EnableJoin != nil {
publicCollectionToken.EnableJoin = *req.EnableJoin
}
err = c.PublicCollectionRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken)
err = c.CollectionLinkRepo.UpdatePublicCollectionToken(ctx, publicCollectionToken)
if err != nil {
return ente.PublicURL{}, stacktrace.Propagate(err, "")
}
return ente.PublicURL{
URL: c.PublicCollectionRepo.GetAlbumUrl(publicCollectionToken.Token),
URL: c.CollectionLinkRepo.GetAlbumUrl(publicCollectionToken.Token),
DeviceLimit: publicCollectionToken.DeviceLimit,
ValidTill: publicCollectionToken.ValidTill,
EnableDownload: publicCollectionToken.EnableDownload,
@@ -176,58 +177,23 @@ func (c *PublicCollectionController) UpdateSharedUrl(ctx context.Context, req en
// used by the client to pass in other requests for public collection.
// Having a separate endpoint for password validation allows us to easily rate-limit the attempts for brute-force
// attack for guessing password.
func (c *PublicCollectionController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
func (c *CollectionLinkController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
accessContext := auth.MustGetPublicAccessContext(ctx)
publicCollectionToken, err := c.PublicCollectionRepo.GetActivePublicCollectionToken(ctx, accessContext.CollectionID)
collectionLinkRow, err := c.CollectionLinkRepo.GetActiveCollectionLinkRow(ctx, accessContext.CollectionID)
if err != nil {
return nil, stacktrace.Propagate(err, "failed to get public collection info")
}
if publicCollectionToken.PassHash == nil || *publicCollectionToken.PassHash == "" {
return nil, stacktrace.Propagate(ente.ErrBadRequest, "password is not configured for the link")
}
if req.PassHash != *publicCollectionToken.PassHash {
return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link")
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.PublicAlbumPasswordClaim{
PassHash: req.PassHash,
ExpiryTime: time.NDaysFromNow(365),
})
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(c.JwtSecret)
if err != nil {
return nil, stacktrace.Propagate(err, "")
}
return &ente.VerifyPasswordResponse{
JWTToken: tokenString,
}, nil
return verifyPassword(c.JwtSecret, collectionLinkRow.PassHash, req)
}
func (c *PublicCollectionController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.PublicAlbumPasswordClaim{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return stacktrace.Propagate(fmt.Errorf("unexpected signing method: %v", token.Header["alg"]), ""), nil
}
return c.JwtSecret, nil
})
if err != nil {
return stacktrace.Propagate(err, "JWT parsed failed")
}
claims, ok := token.Claims.(*enteJWT.PublicAlbumPasswordClaim)
if !ok {
return stacktrace.Propagate(errors.New("no claim in jwt token"), "")
}
if token.Valid && claims.PassHash == passwordHash {
return nil
}
return ente.ErrInvalidPassword
func (c *CollectionLinkController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
return validateJWTToken(c.JwtSecret, jwtToken, passwordHash)
}
// ReportAbuse captures abuse report for a publicly shared collection.
// It will also disable the accessToken for the collection if total abuse reports for the said collection
// reaches AutoDisableAbuseThreshold
func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.AbuseReportRequest) error {
func (c *CollectionLinkController) ReportAbuse(ctx *gin.Context, req ente.AbuseReportRequest) error {
accessContext := auth.MustGetPublicAccessContext(ctx)
readableReason, found := AllowedReasons[req.Reason]
if !found {
@@ -235,11 +201,11 @@ func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.Abus
}
logrus.WithField("collectionID", accessContext.CollectionID).Error("CRITICAL: received abuse report")
err := c.PublicCollectionRepo.RecordAbuseReport(ctx, accessContext, req.URL, req.Reason, req.Details)
err := c.CollectionLinkRepo.RecordAbuseReport(ctx, accessContext, req.URL, req.Reason, req.Details)
if err != nil {
return stacktrace.Propagate(err, "")
}
count, err := c.PublicCollectionRepo.GetAbuseReportCount(ctx, accessContext)
count, err := c.CollectionLinkRepo.GetAbuseReportCount(ctx, accessContext)
if err != nil {
return stacktrace.Propagate(err, "")
}
@@ -253,7 +219,7 @@ func (c *PublicCollectionController) ReportAbuse(ctx *gin.Context, req ente.Abus
return nil
}
func (c *PublicCollectionController) onAbuseReportReceived(collectionID int64, report ente.AbuseReportRequest, readableReason string, abuseCount int64) {
func (c *CollectionLinkController) onAbuseReportReceived(collectionID int64, report ente.AbuseReportRequest, readableReason string, abuseCount int64) {
collection, err := c.CollectionRepo.Get(collectionID)
if err != nil {
logrus.Error("Could not get collection for abuse report")
@@ -292,9 +258,9 @@ func (c *PublicCollectionController) onAbuseReportReceived(collectionID int64, r
}
}
func (c *PublicCollectionController) HandleAccountDeletion(ctx context.Context, userID int64, logger *logrus.Entry) error {
func (c *CollectionLinkController) HandleAccountDeletion(ctx context.Context, userID int64, logger *logrus.Entry) error {
logger.Info("updating public collection on account deletion")
collectionIDs, err := c.PublicCollectionRepo.GetActivePublicTokenForUser(ctx, userID)
collectionIDs, err := c.CollectionLinkRepo.GetActivePublicTokenForUser(ctx, userID)
if err != nil {
return stacktrace.Propagate(err, "")
}
@@ -305,12 +271,12 @@ func (c *PublicCollectionController) HandleAccountDeletion(ctx context.Context,
return stacktrace.Propagate(err, "")
}
}
return nil
return c.FileLinkRepo.DisableLinksForUser(ctx, userID)
}
// GetPublicCollection will return collection info for a public url.
// is mustAllowCollect is set to true but the underlying collection doesn't allow uploading
func (c *PublicCollectionController) GetPublicCollection(ctx *gin.Context, mustAllowCollect bool) (ente.Collection, error) {
func (c *CollectionLinkController) GetPublicCollection(ctx *gin.Context, mustAllowCollect bool) (ente.Collection, error) {
accessContext := auth.MustGetPublicAccessContext(ctx)
collection, err := c.CollectionRepo.Get(accessContext.CollectionID)
if err != nil {

View File

@@ -0,0 +1,162 @@
package public
import (
"github.com/ente-io/museum/ente"
"github.com/ente-io/museum/pkg/controller"
"github.com/ente-io/museum/pkg/repo"
"github.com/ente-io/museum/pkg/repo/public"
"github.com/ente-io/museum/pkg/utils/auth"
"github.com/ente-io/stacktrace"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
)
// FileLinkController controls share collection operations
type FileLinkController struct {
FileController *controller.FileController
FileLinkRepo *public.FileLinkRepository
FileRepo *repo.FileRepository
JwtSecret []byte
}
func (c *FileLinkController) CreateLink(ctx *gin.Context, req ente.CreateFileUrl) (*ente.FileUrl, error) {
actorUserID := auth.GetUserID(ctx.Request.Header)
app := auth.GetApp(ctx)
if req.App != app {
return nil, stacktrace.Propagate(ente.NewBadRequestWithMessage("app mismatch"), "app mismatch")
}
file, err := c.FileRepo.GetFileAttributes(req.FileID)
if err != nil {
return nil, stacktrace.Propagate(err, "failed to get file attributes")
}
if actorUserID != file.OwnerID {
return nil, stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "")
}
accessToken := shortuuid.New()[0:AccessTokenLength]
_, err = c.FileLinkRepo.Insert(ctx, req.FileID, actorUserID, accessToken, app)
if err == nil || err == ente.ErrActiveLinkAlreadyExists {
row, rowErr := c.FileLinkRepo.GetFileUrlRowByFileID(ctx, req.FileID)
if rowErr != nil {
return nil, stacktrace.Propagate(rowErr, "failed to get active file url token")
}
return c.mapRowToFileUrl(ctx, row), nil
}
return nil, stacktrace.Propagate(err, "failed to create public file link")
}
// Disable all public accessTokens generated for the given fileID till date.
func (c *FileLinkController) Disable(ctx *gin.Context, fileID int64) error {
userID := auth.GetUserID(ctx.Request.Header)
file, err := c.FileRepo.GetFileAttributes(fileID)
if err != nil {
return stacktrace.Propagate(err, "failed to get file attributes")
}
if userID != file.OwnerID {
return stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "")
}
return c.FileLinkRepo.DisableLinkForFiles(ctx, []int64{fileID})
}
func (c *FileLinkController) GetUrls(ctx *gin.Context, sinceTime int64, limit int64) ([]*ente.FileUrl, error) {
userID := auth.GetUserID(ctx.Request.Header)
app := auth.GetApp(ctx)
fileLinks, err := c.FileLinkRepo.GetFileUrls(ctx, userID, sinceTime, limit, app)
if err != nil {
return nil, stacktrace.Propagate(err, "failed to get file urls")
}
var fileUrls []*ente.FileUrl
for _, row := range fileLinks {
fileUrls = append(fileUrls, c.mapRowToFileUrl(ctx, row))
}
return fileUrls, nil
}
func (c *FileLinkController) UpdateSharedUrl(ctx *gin.Context, req ente.UpdateFileUrl) (*ente.FileUrl, error) {
if err := req.Validate(); err != nil {
return nil, stacktrace.Propagate(err, "invalid request")
}
fileLinkRow, err := c.FileLinkRepo.GetActiveFileUrlToken(ctx, req.FileID)
if err != nil {
return nil, stacktrace.Propagate(err, "failed to get file link info")
}
if fileLinkRow.OwnerID != auth.GetUserID(ctx.Request.Header) {
return nil, stacktrace.Propagate(ente.NewPermissionDeniedError("not file owner"), "")
}
if req.ValidTill != nil {
fileLinkRow.ValidTill = *req.ValidTill
}
if req.DeviceLimit != nil {
fileLinkRow.DeviceLimit = *req.DeviceLimit
}
if req.PassHash != nil && req.Nonce != nil && req.OpsLimit != nil && req.MemLimit != nil {
fileLinkRow.PassHash = req.PassHash
fileLinkRow.Nonce = req.Nonce
fileLinkRow.OpsLimit = req.OpsLimit
fileLinkRow.MemLimit = req.MemLimit
} else if req.DisablePassword != nil && *req.DisablePassword {
fileLinkRow.PassHash = nil
fileLinkRow.Nonce = nil
fileLinkRow.OpsLimit = nil
fileLinkRow.MemLimit = nil
}
if req.EnableDownload != nil {
fileLinkRow.EnableDownload = *req.EnableDownload
}
err = c.FileLinkRepo.UpdateLink(ctx, *fileLinkRow)
if err != nil {
return nil, stacktrace.Propagate(err, "")
}
return c.mapRowToFileUrl(ctx, fileLinkRow), nil
}
func (c *FileLinkController) Info(ctx *gin.Context) (*ente.File, error) {
accessContext := auth.MustGetFileLinkAccessContext(ctx)
return c.FileRepo.GetFileAttributes(accessContext.FileID)
}
func (c *FileLinkController) PassInfo(ctx *gin.Context) (*ente.FileLinkRow, error) {
accessContext := auth.MustGetFileLinkAccessContext(ctx)
return c.FileLinkRepo.GetFileUrlRowByFileID(ctx, accessContext.FileID)
}
// VerifyPassword verifies if the user has provided correct pw hash. If yes, it returns a signed jwt token which can be
// used by the client to pass in other requests for public collection.
// Having a separate endpoint for password validation allows us to easily rate-limit the attempts for brute-force
// attack for guessing password.
func (c *FileLinkController) VerifyPassword(ctx *gin.Context, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
accessContext := auth.MustGetFileLinkAccessContext(ctx)
collectionLinkRow, err := c.FileLinkRepo.GetActiveFileUrlToken(ctx, accessContext.FileID)
if err != nil {
return nil, stacktrace.Propagate(err, "failed to get public collection info")
}
return verifyPassword(c.JwtSecret, collectionLinkRow.PassHash, req)
}
func (c *FileLinkController) ValidateJWTToken(ctx *gin.Context, jwtToken string, passwordHash string) error {
return validateJWTToken(c.JwtSecret, jwtToken, passwordHash)
}
func (c *FileLinkController) mapRowToFileUrl(ctx *gin.Context, row *ente.FileLinkRow) *ente.FileUrl {
app := auth.GetApp(ctx)
var url string
if app == ente.Locker {
url = c.FileLinkRepo.LockerFileLink(row.Token)
} else {
url = c.FileLinkRepo.PhotoLink(row.Token)
}
return &ente.FileUrl{
LinkID: row.LinkID,
FileID: row.FileID,
URL: url,
OwnerID: row.OwnerID,
ValidTill: row.ValidTill,
DeviceLimit: row.DeviceLimit,
PasswordEnabled: row.PassHash != nil,
Nonce: row.Nonce,
OpsLimit: row.OpsLimit,
MemLimit: row.MemLimit,
EnableDownload: row.EnableDownload,
CreatedAt: row.CreatedAt,
}
}

View File

@@ -0,0 +1,54 @@
package public
import (
"errors"
"fmt"
"github.com/ente-io/museum/ente"
enteJWT "github.com/ente-io/museum/ente/jwt"
"github.com/ente-io/museum/pkg/utils/time"
"github.com/ente-io/stacktrace"
"github.com/golang-jwt/jwt"
)
func validateJWTToken(secret []byte, jwtToken string, passwordHash string) error {
token, err := jwt.ParseWithClaims(jwtToken, &enteJWT.LinkPasswordClaim{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return stacktrace.Propagate(fmt.Errorf("unexpected signing method: %v", token.Header["alg"]), ""), nil
}
return secret, nil
})
if err != nil {
return stacktrace.Propagate(err, "JWT parsed failed")
}
claims, ok := token.Claims.(*enteJWT.LinkPasswordClaim)
if !ok {
return stacktrace.Propagate(errors.New("no claim in jwt token"), "")
}
if token.Valid && claims.PassHash == passwordHash {
return nil
}
return ente.ErrInvalidPassword
}
func verifyPassword(secret []byte, expectedPassHash *string, req ente.VerifyPasswordRequest) (*ente.VerifyPasswordResponse, error) {
if expectedPassHash == nil || *expectedPassHash == "" {
return nil, stacktrace.Propagate(ente.ErrBadRequest, "password is not configured for the link")
}
if req.PassHash != *expectedPassHash {
return nil, stacktrace.Propagate(ente.ErrInvalidPassword, "incorrect password for link")
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &enteJWT.LinkPasswordClaim{
PassHash: req.PassHash,
ExpiryTime: time.NDaysFromNow(365),
})
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(secret)
if err != nil {
return nil, stacktrace.Propagate(err, "")
}
return &ente.VerifyPasswordResponse{
JWTToken: tokenString,
}, nil
}

View File

@@ -5,6 +5,8 @@ import (
"context"
"crypto/sha256"
"fmt"
public2 "github.com/ente-io/museum/pkg/controller/public"
"github.com/ente-io/museum/pkg/repo/public"
"net/http"
"github.com/ente-io/museum/ente"
@@ -24,20 +26,20 @@ import (
var passwordWhiteListedURLs = []string{"/public-collection/info", "/public-collection/report-abuse", "/public-collection/verify-password"}
var whitelistedCollectionShareIDs = []int64{111}
// AccessTokenMiddleware intercepts and authenticates incoming requests
type AccessTokenMiddleware struct {
PublicCollectionRepo *repo.PublicCollectionRepository
PublicCollectionCtrl *controller.PublicCollectionController
// CollectionLinkMiddleware intercepts and authenticates incoming requests
type CollectionLinkMiddleware struct {
CollectionLinkRepo *public.CollectionLinkRepo
PublicCollectionCtrl *public2.CollectionLinkController
CollectionRepo *repo.CollectionRepository
Cache *cache.Cache
BillingCtrl *controller.BillingController
DiscordController *discord.DiscordController
}
// AccessTokenAuthMiddleware returns a middle ware that extracts the `X-Auth-Access-Token`
// Authenticate returns a middle ware that extracts the `X-Auth-Access-Token`
// within the header of a request and uses it to validate the access token and set the
// ente.PublicAccessContext with auth.PublicAccessKey as key
func (m *AccessTokenMiddleware) AccessTokenAuthMiddleware(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc {
func (m *CollectionLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc {
return func(c *gin.Context) {
accessToken := auth.GetAccessToken(c)
if accessToken == "" {
@@ -52,7 +54,7 @@ func (m *AccessTokenMiddleware) AccessTokenAuthMiddleware(urlSanitizer func(_ *g
cacheKey := computeHashKeyForList([]string{accessToken, clientIP, userAgent}, ":")
cachedValue, cacheHit := m.Cache.Get(cacheKey)
if !cacheHit {
publicCollectionSummary, err = m.PublicCollectionRepo.GetCollectionSummaryByToken(c, accessToken)
publicCollectionSummary, err = m.CollectionLinkRepo.GetCollectionSummaryByToken(c, accessToken)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
@@ -112,7 +114,7 @@ func (m *AccessTokenMiddleware) AccessTokenAuthMiddleware(urlSanitizer func(_ *g
c.Next()
}
}
func (m *AccessTokenMiddleware) validateOwnersSubscription(cID int64) error {
func (m *CollectionLinkMiddleware) validateOwnersSubscription(cID int64) error {
userID, err := m.CollectionRepo.GetOwnerID(cID)
if err != nil {
return stacktrace.Propagate(err, "")
@@ -120,7 +122,7 @@ func (m *AccessTokenMiddleware) validateOwnersSubscription(cID int64) error {
return m.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, false)
}
func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context,
func (m *CollectionLinkMiddleware) isDeviceLimitReached(ctx context.Context,
collectionSummary ente.PublicCollectionSummary, ip string, ua string) (bool, error) {
// skip deviceLimit check & record keeping for requests via CF worker
if network.IsCFWorkerIP(ip) {
@@ -128,7 +130,7 @@ func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context,
}
sharedID := collectionSummary.ID
hasAccessedInPast, err := m.PublicCollectionRepo.AccessedInPast(ctx, sharedID, ip, ua)
hasAccessedInPast, err := m.CollectionLinkRepo.AccessedInPast(ctx, sharedID, ip, ua)
if err != nil {
return false, stacktrace.Propagate(err, "")
}
@@ -136,17 +138,17 @@ func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context,
if hasAccessedInPast {
return false, nil
}
count, err := m.PublicCollectionRepo.GetUniqueAccessCount(ctx, sharedID)
count, err := m.CollectionLinkRepo.GetUniqueAccessCount(ctx, sharedID)
if err != nil {
return false, stacktrace.Propagate(err, "failed to get unique access count")
}
deviceLimit := int64(collectionSummary.DeviceLimit)
if deviceLimit == controller.DeviceLimitThreshold {
deviceLimit = controller.DeviceLimitThresholdMultiplier * controller.DeviceLimitThreshold
if deviceLimit == public2.DeviceLimitThreshold {
deviceLimit = public2.DeviceLimitThresholdMultiplier * public2.DeviceLimitThreshold
}
if count >= controller.DeviceLimitWarningThreshold {
if count >= public2.DeviceLimitWarningThreshold {
if !array.Int64InList(sharedID, whitelistedCollectionShareIDs) {
m.DiscordController.NotifyPotentialAbuse(
fmt.Sprintf("Album exceeds warning threshold: {CollectionID: %d, ShareID: %d}",
@@ -157,12 +159,12 @@ func (m *AccessTokenMiddleware) isDeviceLimitReached(ctx context.Context,
if deviceLimit > 0 && count >= deviceLimit {
return true, nil
}
err = m.PublicCollectionRepo.RecordAccessHistory(ctx, sharedID, ip, ua)
err = m.CollectionLinkRepo.RecordAccessHistory(ctx, sharedID, ip, ua)
return false, stacktrace.Propagate(err, "failed to record access history")
}
// validatePassword will verify if the user is provided correct password for the public album
func (m *AccessTokenMiddleware) validatePassword(c *gin.Context, reqPath string,
func (m *CollectionLinkMiddleware) validatePassword(c *gin.Context, reqPath string,
collectionSummary ente.PublicCollectionSummary) error {
if array.StringInList(reqPath, passwordWhiteListedURLs) {
return nil

View File

@@ -0,0 +1,168 @@
package middleware
import (
"context"
"fmt"
publicCtrl "github.com/ente-io/museum/pkg/controller/public"
"github.com/ente-io/museum/pkg/repo/public"
"github.com/ente-io/museum/pkg/utils/array"
"net/http"
"github.com/ente-io/museum/ente"
"github.com/ente-io/museum/pkg/controller"
"github.com/ente-io/museum/pkg/controller/discord"
"github.com/ente-io/museum/pkg/utils/auth"
"github.com/ente-io/museum/pkg/utils/network"
"github.com/ente-io/museum/pkg/utils/time"
"github.com/ente-io/stacktrace"
"github.com/gin-gonic/gin"
"github.com/patrickmn/go-cache"
"github.com/sirupsen/logrus"
)
var filePasswordWhiteListedURLs = []string{"/file-link/pass-info", "/file-link/verify-password"}
// FileLinkMiddleware intercepts and authenticates incoming requests
type FileLinkMiddleware struct {
FileLinkRepo *public.FileLinkRepository
FileLinkCtrl *publicCtrl.FileLinkController
Cache *cache.Cache
BillingCtrl *controller.BillingController
DiscordController *discord.DiscordController
}
// Authenticate returns a middle ware that extracts the `X-Auth-Access-Token`
// within the header of a request and uses it to validate the access token and set the
// ente.PublicAccessContext with auth.PublicAccessKey as key
func (m *FileLinkMiddleware) Authenticate(urlSanitizer func(_ *gin.Context) string) gin.HandlerFunc {
return func(c *gin.Context) {
accessToken := auth.GetAccessToken(c)
if accessToken == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing accessToken"})
return
}
clientIP := network.GetClientIP(c)
userAgent := c.GetHeader("User-Agent")
cacheKey := computeHashKeyForList([]string{accessToken, clientIP, userAgent}, ":")
cachedValue, cacheHit := m.Cache.Get(cacheKey)
var fileLinkRow *ente.FileLinkRow
var err error
if !cacheHit {
fileLinkRow, err = m.FileLinkRepo.GetFileUrlRowByToken(c, accessToken)
if err != nil {
logrus.WithError(err).Info("failed to get file link row by token")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
if fileLinkRow.IsDisabled {
c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "disabled token"})
return
}
// validate if user still has active paid subscription
if err = m.BillingCtrl.HasActiveSelfOrFamilySubscription(fileLinkRow.OwnerID, true); err != nil {
logrus.WithError(err).Info("failed to verify active paid subscription")
c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "no active subscription"})
return
}
// validate device limit
reached, limitErr := m.isDeviceLimitReached(c, fileLinkRow, clientIP, userAgent)
if limitErr != nil {
logrus.WithError(limitErr).Error("failed to check device limit")
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "something went wrong"})
return
}
if reached {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "reached device limit"})
return
}
} else {
fileLinkRow = cachedValue.(*ente.FileLinkRow)
}
if fileLinkRow.ValidTill > 0 && // expiry time is defined, 0 indicates no expiry
fileLinkRow.ValidTill < time.Microseconds() {
c.AbortWithStatusJSON(http.StatusGone, gin.H{"error": "expired token"})
return
}
// checks password protected public collection
if fileLinkRow.PassHash != nil && *fileLinkRow.PassHash != "" {
reqPath := urlSanitizer(c)
if err = m.validatePassword(c, reqPath, fileLinkRow); err != nil {
logrus.WithError(err).Warn("password validation failed")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err})
return
}
}
if !cacheHit {
m.Cache.Set(cacheKey, fileLinkRow, cache.DefaultExpiration)
}
c.Set(auth.FileLinkAccessKey, &ente.FileLinkAccessContext{
LinkID: fileLinkRow.LinkID,
IP: clientIP,
UserAgent: userAgent,
FileID: fileLinkRow.FileID,
OwnerID: fileLinkRow.OwnerID,
})
c.Next()
}
}
func (m *FileLinkMiddleware) isDeviceLimitReached(ctx context.Context,
collectionSummary *ente.FileLinkRow, ip string, ua string) (bool, error) {
// skip deviceLimit check & record keeping for requests via CF worker
if network.IsCFWorkerIP(ip) {
return false, nil
}
sharedID := collectionSummary.LinkID
hasAccessedInPast, err := m.FileLinkRepo.AccessedInPast(ctx, sharedID, ip, ua)
if err != nil {
return false, stacktrace.Propagate(err, "")
}
// if the device has accessed the url in the past, let it access it now as well, irrespective of device limit.
if hasAccessedInPast {
return false, nil
}
count, err := m.FileLinkRepo.GetUniqueAccessCount(ctx, sharedID)
if err != nil {
return false, stacktrace.Propagate(err, "failed to get unique access count")
}
deviceLimit := int64(collectionSummary.DeviceLimit)
if deviceLimit == publicCtrl.DeviceLimitThreshold {
deviceLimit = publicCtrl.DeviceLimitThresholdMultiplier * publicCtrl.DeviceLimitThreshold
}
if count >= publicCtrl.DeviceLimitWarningThreshold {
m.DiscordController.NotifyPotentialAbuse(
fmt.Sprintf("FileLink exceeds warning threshold: {FileID: %d, ShareID: %s}",
collectionSummary.FileID, collectionSummary.LinkID))
}
if deviceLimit > 0 && count >= deviceLimit {
return true, nil
}
err = m.FileLinkRepo.RecordAccessHistory(ctx, sharedID, ip, ua)
return false, stacktrace.Propagate(err, "failed to record access history")
}
// validatePassword will verify if the user is provided correct password for the public album
func (m *FileLinkMiddleware) validatePassword(
c *gin.Context,
reqPath string,
fileLinkRow *ente.FileLinkRow,
) error {
accessTokenJWT := auth.GetAccessTokenJWT(c)
if accessTokenJWT == "" {
if array.StringInList(reqPath, filePasswordWhiteListedURLs) {
return nil
}
return &ente.ErrPassProtectedResource
}
return m.FileLinkCtrl.ValidateJWTToken(c, accessTokenJWT, *fileLinkRow.PassHash)
}

View File

@@ -140,6 +140,7 @@ func (r *RateLimitMiddleware) getLimiter(reqPath string, reqMethod string) *limi
reqPath == "/users/verify-email" ||
reqPath == "/user/change-email" ||
reqPath == "/public-collection/verify-password" ||
reqPath == "/file-link/verify-password" ||
reqPath == "/family/accept-invite" ||
reqPath == "/users/srp/attributes" ||
(reqPath == "/cast/device-info" && reqMethod == "POST") ||

View File

@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"github.com/ente-io/museum/pkg/repo/public"
"strconv"
t "time"
@@ -22,13 +23,13 @@ import (
// CollectionRepository defines the methods for inserting, updating and
// retrieving collection entities from the underlying repository
type CollectionRepository struct {
DB *sql.DB
FileRepo *FileRepository
PublicCollectionRepo *PublicCollectionRepository
TrashRepo *TrashRepository
SecretEncryptionKey []byte
QueueRepo *QueueRepository
LatencyLogger *prometheus.HistogramVec
DB *sql.DB
FileRepo *FileRepository
CollectionLinkRepo *public.CollectionLinkRepo
TrashRepo *TrashRepository
SecretEncryptionKey []byte
QueueRepo *QueueRepository
LatencyLogger *prometheus.HistogramVec
}
type SharedCollection struct {
@@ -74,7 +75,7 @@ func (repo *CollectionRepository) Get(collectionID int64) (ente.Collection, erro
c.EncryptedName = encryptedName.String
c.NameDecryptionNonce = nameDecryptionNonce.String
}
urlMap, err := repo.PublicCollectionRepo.GetCollectionToActivePublicURLMap(context.Background(), []int64{collectionID})
urlMap, err := repo.CollectionLinkRepo.GetCollectionToActivePublicURLMap(context.Background(), []int64{collectionID})
if err != nil {
return ente.Collection{}, stacktrace.Propagate(err, "failed to get publicURL info")
}
@@ -174,7 +175,7 @@ pct.access_token, pct.valid_till, pct.device_limit, pct.created_at, pct.updated_
if _, ok := addPublicUrlMap[pctToken.String]; !ok {
addPublicUrlMap[pctToken.String] = true
url := ente.PublicURL{
URL: repo.PublicCollectionRepo.GetAlbumUrl(pctToken.String),
URL: repo.CollectionLinkRepo.GetAlbumUrl(pctToken.String),
DeviceLimit: int(pctDeviceLimit.Int32),
ValidTill: pctValidTill.Int64,
EnableDownload: pctEnableDownload.Bool,

View File

@@ -638,6 +638,16 @@ func (repo *FileRepository) GetFileAttributesForCopy(fileIDs []int64) ([]ente.Fi
return result, nil
}
func (repo *FileRepository) GetFileAttributes(fileID int64) (*ente.File, error) {
rows := repo.DB.QueryRow(`SELECT file_id, owner_id, file_decryption_header, thumbnail_decryption_header, metadata_decryption_header, encrypted_metadata, pub_magic_metadata FROM files WHERE file_id = $1`, fileID)
var file ente.File
err := rows.Scan(&file.ID, &file.OwnerID, &file.File.DecryptionHeader, &file.Thumbnail.DecryptionHeader, &file.Metadata.DecryptionHeader, &file.Metadata.EncryptedData, &file.PubicMagicMetadata)
if err != nil {
return nil, stacktrace.Propagate(err, "")
}
return &file, nil
}
// GetUsage gets the Storage usage of a user
// Deprecated: GetUsage is deprecated, use UsageRepository.GetUsage
func (repo *FileRepository) GetUsage(userID int64) (int64, error) {

View File

@@ -1,4 +1,4 @@
package repo
package public
import (
"context"
@@ -13,29 +13,29 @@ import (
const BaseShareURL = "https://albums.ente.io/?t=%s"
// PublicCollectionRepository defines the methods for inserting, updating and
// CollectionLinkRepo defines the methods for inserting, updating and
// retrieving entities related to public collections
type PublicCollectionRepository struct {
type CollectionLinkRepo struct {
DB *sql.DB
albumHost string
}
// NewPublicCollectionRepository ..
func NewPublicCollectionRepository(db *sql.DB, albumHost string) *PublicCollectionRepository {
// NewCollectionLinkRepository ..
func NewCollectionLinkRepository(db *sql.DB, albumHost string) *CollectionLinkRepo {
if albumHost == "" {
albumHost = "https://albums.ente.io"
}
return &PublicCollectionRepository{
return &CollectionLinkRepo{
DB: db,
albumHost: albumHost,
}
}
func (pcr *PublicCollectionRepository) GetAlbumUrl(token string) string {
func (pcr *CollectionLinkRepo) GetAlbumUrl(token string) string {
return fmt.Sprintf("%s/?t=%s", pcr.albumHost, token)
}
func (pcr *PublicCollectionRepository) Insert(ctx context.Context,
func (pcr *CollectionLinkRepo) Insert(ctx context.Context,
cID int64, token string, validTill int64, deviceLimit int, enableCollect bool, enableJoin *bool) error {
// default value for enableJoin is true
join := true
@@ -51,7 +51,7 @@ func (pcr *PublicCollectionRepository) Insert(ctx context.Context,
return stacktrace.Propagate(err, "failed to insert")
}
func (pcr *PublicCollectionRepository) DisableSharing(ctx context.Context, cID int64) error {
func (pcr *CollectionLinkRepo) DisableSharing(ctx context.Context, cID int64) error {
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_collection_tokens SET is_disabled = true where
collection_id = $1 and is_disabled = false`, cID)
return stacktrace.Propagate(err, "failed to disable sharing")
@@ -59,7 +59,7 @@ func (pcr *PublicCollectionRepository) DisableSharing(ctx context.Context, cID i
// GetCollectionToActivePublicURLMap will return map of collectionID to PublicURLs which are not disabled yet.
// Note: The url could be expired or deviceLimit is already reached
func (pcr *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx context.Context, collectionIDs []int64) (map[int64][]ente.PublicURL, error) {
func (pcr *CollectionLinkRepo) GetCollectionToActivePublicURLMap(ctx context.Context, collectionIDs []int64) (map[int64][]ente.PublicURL, error) {
rows, err := pcr.DB.QueryContext(ctx, `SELECT collection_id, access_token, valid_till, device_limit, enable_download, enable_collect, enable_join, pw_nonce, mem_limit, ops_limit FROM
public_collection_tokens WHERE collection_id = ANY($1) and is_disabled = FALSE`,
pq.Array(collectionIDs))
@@ -92,26 +92,26 @@ func (pcr *PublicCollectionRepository) GetCollectionToActivePublicURLMap(ctx con
return result, nil
}
// GetActivePublicCollectionToken will return ente.PublicCollectionToken for given collection ID
// GetActiveCollectionLinkRow will return ente.CollectionLinkRow for given collection ID
// Note: The token could be expired or deviceLimit is already reached
func (pcr *PublicCollectionRepository) GetActivePublicCollectionToken(ctx context.Context, collectionID int64) (ente.PublicCollectionToken, error) {
func (pcr *CollectionLinkRepo) GetActiveCollectionLinkRow(ctx context.Context, collectionID int64) (ente.CollectionLinkRow, error) {
row := pcr.DB.QueryRowContext(ctx, `SELECT id, collection_id, access_token, valid_till, device_limit,
is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download, enable_collect, enable_join FROM
public_collection_tokens WHERE collection_id = $1 and is_disabled = FALSE`,
collectionID)
//defer rows.Close()
ret := ente.PublicCollectionToken{}
ret := ente.CollectionLinkRow{}
err := row.Scan(&ret.ID, &ret.CollectionID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit,
&ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload, &ret.EnableCollect, &ret.EnableJoin)
if err != nil {
return ente.PublicCollectionToken{}, stacktrace.Propagate(err, "")
return ente.CollectionLinkRow{}, stacktrace.Propagate(err, "")
}
return ret, nil
}
// UpdatePublicCollectionToken will update the row for corresponding public collection token
func (pcr *PublicCollectionRepository) UpdatePublicCollectionToken(ctx context.Context, pct ente.PublicCollectionToken) error {
func (pcr *CollectionLinkRepo) UpdatePublicCollectionToken(ctx context.Context, pct ente.CollectionLinkRow) error {
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_collection_tokens SET valid_till = $1, device_limit = $2,
pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7, enable_collect = $8, enable_join = $9
where id = $10`,
@@ -119,7 +119,7 @@ func (pcr *PublicCollectionRepository) UpdatePublicCollectionToken(ctx context.C
return stacktrace.Propagate(err, "failed to update public collection token")
}
func (pcr *PublicCollectionRepository) RecordAbuseReport(ctx context.Context, accessCtx ente.PublicAccessContext,
func (pcr *CollectionLinkRepo) RecordAbuseReport(ctx context.Context, accessCtx ente.PublicAccessContext,
url string, reason string, details ente.AbuseReportDetails) error {
_, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_abuse_report
(share_id, ip, user_agent, url, reason, details) VALUES ($1, $2, $3, $4, $5, $6)
@@ -128,7 +128,7 @@ func (pcr *PublicCollectionRepository) RecordAbuseReport(ctx context.Context, ac
return stacktrace.Propagate(err, "failed to record abuse report")
}
func (pcr *PublicCollectionRepository) GetAbuseReportCount(ctx context.Context, accessCtx ente.PublicAccessContext) (int64, error) {
func (pcr *CollectionLinkRepo) GetAbuseReportCount(ctx context.Context, accessCtx ente.PublicAccessContext) (int64, error) {
row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_abuse_report WHERE share_id = $1`, accessCtx.ID)
var count int64 = 0
err := row.Scan(&count)
@@ -138,7 +138,7 @@ func (pcr *PublicCollectionRepository) GetAbuseReportCount(ctx context.Context,
return count, nil
}
func (pcr *PublicCollectionRepository) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) {
func (pcr *CollectionLinkRepo) GetUniqueAccessCount(ctx context.Context, shareId int64) (int64, error) {
row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_collection_access_history WHERE share_id = $1`, shareId)
var count int64 = 0
err := row.Scan(&count)
@@ -148,7 +148,7 @@ func (pcr *PublicCollectionRepository) GetUniqueAccessCount(ctx context.Context,
return count, nil
}
func (pcr *PublicCollectionRepository) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error {
func (pcr *CollectionLinkRepo) RecordAccessHistory(ctx context.Context, shareID int64, ip string, ua string) error {
_, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_collection_access_history
(share_id, ip, user_agent) VALUES ($1, $2, $3)
ON CONFLICT ON CONSTRAINT unique_access_sid_ip_ua DO NOTHING;`,
@@ -157,7 +157,7 @@ func (pcr *PublicCollectionRepository) RecordAccessHistory(ctx context.Context,
}
// AccessedInPast returns true if the given ip, ua agent combination has accessed the url in the past
func (pcr *PublicCollectionRepository) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) {
func (pcr *CollectionLinkRepo) AccessedInPast(ctx context.Context, shareID int64, ip string, ua string) (bool, error) {
row := pcr.DB.QueryRowContext(ctx, `select share_id from public_collection_access_history where share_id =$1 and ip = $2 and user_agent = $3`,
shareID, ip, ua)
var tempID int64
@@ -168,7 +168,7 @@ func (pcr *PublicCollectionRepository) AccessedInPast(ctx context.Context, share
return true, stacktrace.Propagate(err, "failed to record access history")
}
func (pcr *PublicCollectionRepository) GetCollectionSummaryByToken(ctx context.Context, accessToken string) (ente.PublicCollectionSummary, error) {
func (pcr *CollectionLinkRepo) GetCollectionSummaryByToken(ctx context.Context, accessToken string) (ente.PublicCollectionSummary, error) {
row := pcr.DB.QueryRowContext(ctx,
`SELECT sct.id, sct.collection_id, sct.is_disabled, sct.valid_till, sct.device_limit, sct.pw_hash,
sct.created_at, sct.updated_at, count(ah.share_id)
@@ -185,7 +185,7 @@ func (pcr *PublicCollectionRepository) GetCollectionSummaryByToken(ctx context.C
return result, nil
}
func (pcr *PublicCollectionRepository) GetActivePublicTokenForUser(ctx context.Context, userID int64) ([]int64, error) {
func (pcr *CollectionLinkRepo) GetActivePublicTokenForUser(ctx context.Context, userID int64) ([]int64, error) {
rows, err := pcr.DB.QueryContext(ctx, `select pt.collection_id from public_collection_tokens pt left join collections c on pt.collection_id = c.collection_id where pt.is_disabled = FALSE and c.owner_id= $1;`, userID)
if err != nil {
return nil, stacktrace.Propagate(err, "")
@@ -204,7 +204,7 @@ func (pcr *PublicCollectionRepository) GetActivePublicTokenForUser(ctx context.C
}
// CleanupAccessHistory public_collection_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days
func (pcr *PublicCollectionRepository) CleanupAccessHistory(ctx context.Context) error {
func (pcr *CollectionLinkRepo) CleanupAccessHistory(ctx context.Context) error {
_, err := pcr.DB.ExecContext(ctx, `DELETE FROM public_collection_access_history WHERE share_id IN (SELECT id FROM public_collection_tokens WHERE is_disabled = TRUE AND updated_at < (now_utc_micro_seconds() - (24::BIGINT * 30 * 60 * 60 * 1000 * 1000)))`)
if err != nil {
return stacktrace.Propagate(err, "failed to clean up public collection access history")

View File

@@ -0,0 +1,218 @@
package public
import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/ente-io/museum/ente/base"
"github.com/lib/pq"
"github.com/spf13/viper"
"github.com/ente-io/museum/ente"
"github.com/ente-io/stacktrace"
)
// FileLinkRepository defines the methods for inserting, updating and
// retrieving entities related to public file
type FileLinkRepository struct {
DB *sql.DB
photoHost string
lockerHost string
}
// NewFileLinkRepo ..
func NewFileLinkRepo(db *sql.DB) *FileLinkRepository {
albumHost := viper.GetString("apps.public-albums")
if albumHost == "" {
albumHost = "https://albums.ente.io"
}
lockerHost := viper.GetString("apps.public-locker")
if lockerHost == "" {
lockerHost = "https://locker.ente.io"
}
return &FileLinkRepository{
DB: db,
photoHost: albumHost,
lockerHost: lockerHost,
}
}
func (pcr *FileLinkRepository) PhotoLink(token string) string {
return fmt.Sprintf("%s/?t=%s", pcr.photoHost, token)
}
func (pcr *FileLinkRepository) LockerFileLink(token string) string {
return fmt.Sprintf("%s/?t=%s", pcr.lockerHost, token)
}
func (pcr *FileLinkRepository) Insert(
ctx context.Context,
fileID int64,
ownerID int64,
token string,
app ente.App,
) (*string, error) {
id, err := base.NewID("pft")
if err != nil {
return nil, stacktrace.Propagate(err, "failed to generate new ID for public file token")
}
_, err = pcr.DB.ExecContext(ctx, `INSERT INTO public_file_tokens
(id, file_id, owner_id, access_token, app) VALUES ($1, $2, $3, $4, $5)`,
id, fileID, ownerID, token, string(app))
if err != nil {
if err.Error() == "pq: duplicate key value violates unique constraint \"public_active_file_link_unique_idx\"" {
return nil, ente.ErrActiveLinkAlreadyExists
}
return nil, stacktrace.Propagate(err, "failed to insert")
}
return id, nil
}
// GetActiveFileUrlToken will return ente.CollectionLinkRow for given collection ID
// Note: The token could be expired or deviceLimit is already reached
func (pcr *FileLinkRepository) GetActiveFileUrlToken(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) {
row := pcr.DB.QueryRowContext(ctx, `SELECT id, file_id, owner_id, access_token, valid_till, device_limit,
is_disabled, pw_hash, pw_nonce, mem_limit, ops_limit, enable_download FROM
public_file_tokens WHERE file_id = $1 and is_disabled = FALSE`,
fileID)
ret := ente.FileLinkRow{}
err := row.Scan(&ret.LinkID, &ret.FileID, ret.OwnerID, &ret.Token, &ret.ValidTill, &ret.DeviceLimit,
&ret.IsDisabled, &ret.PassHash, &ret.Nonce, &ret.MemLimit, &ret.OpsLimit, &ret.EnableDownload)
if err != nil {
return nil, stacktrace.Propagate(err, "")
}
return &ret, nil
}
func (pcr *FileLinkRepository) GetFileUrls(ctx context.Context, userID int64, sinceTime int64, limit int64, app ente.App) ([]*ente.FileLinkRow, error) {
if limit <= 0 {
limit = 500
}
query := `SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit,
created_at, updated_at FROM public_file_tokens
WHERE owner_id = $1 AND created_at > $2 AND app = $3 ORDER BY updated_at DESC LIMIT $4`
rows, err := pcr.DB.QueryContext(ctx, query, userID, sinceTime, string(app), limit)
if err != nil {
return nil, stacktrace.Propagate(err, "failed to get public file urls")
}
defer rows.Close()
var result []*ente.FileLinkRow
for rows.Next() {
var row ente.FileLinkRow
err = rows.Scan(&row.LinkID, &row.FileID, &row.OwnerID, &row.IsDisabled,
&row.ValidTill, &row.DeviceLimit, &row.EnableDownload,
&row.PassHash, &row.Nonce, &row.MemLimit,
&row.OpsLimit, &row.CreatedAt, &row.UpdatedAt)
if err != nil {
return nil, stacktrace.Propagate(err, "failed to scan public file url row")
}
result = append(result, &row)
}
return result, nil
}
func (pcr *FileLinkRepository) DisableLinkForFiles(ctx context.Context, fileIDs []int64) error {
if len(fileIDs) == 0 {
return nil
}
query := `UPDATE public_file_tokens SET is_disabled = TRUE WHERE file_id = ANY($1)`
_, err := pcr.DB.ExecContext(ctx, query, pq.Array(fileIDs))
if err != nil {
return stacktrace.Propagate(err, "failed to disable public file links")
}
return nil
}
// DisableLinksForUser will disable all public file links for the given user
func (pcr *FileLinkRepository) DisableLinksForUser(ctx context.Context, userID int64) error {
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET is_disabled = TRUE WHERE owner_id = $1`, userID)
if err != nil {
return stacktrace.Propagate(err, "failed to disable public file link")
}
return nil
}
func (pcr *FileLinkRepository) GetFileUrlRowByToken(ctx context.Context, accessToken string) (*ente.FileLinkRow, error) {
row := pcr.DB.QueryRowContext(ctx,
`SELECT id, file_id, owner_id, is_disabled, valid_till, device_limit, enable_download, pw_hash, pw_nonce, mem_limit, ops_limit
created_at, updated_at
from public_file_tokens
where access_token = $1
`, accessToken)
var result = ente.FileLinkRow{}
err := row.Scan(&result.LinkID, &result.FileID, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, ente.ErrNotFound
}
return nil, stacktrace.Propagate(err, "failed to get public file url summary by token")
}
return &result, nil
}
func (pcr *FileLinkRepository) GetFileUrlRowByFileID(ctx context.Context, fileID int64) (*ente.FileLinkRow, error) {
row := pcr.DB.QueryRowContext(ctx,
`SELECT id, file_id, access_token, owner_id, is_disabled, enable_download, valid_till, device_limit, pw_hash, pw_nonce, mem_limit, ops_limit,
created_at, updated_at
from public_file_tokens
where file_id = $1 and is_disabled = FALSE`, fileID)
var result = ente.FileLinkRow{}
err := row.Scan(&result.LinkID, &result.FileID, &result.Token, &result.OwnerID, &result.IsDisabled, &result.EnableDownload, &result.ValidTill, &result.DeviceLimit, &result.PassHash, &result.Nonce, &result.MemLimit, &result.OpsLimit, &result.CreatedAt, &result.UpdatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, ente.ErrNotFound
}
return nil, stacktrace.Propagate(err, "failed to get public file url summary by file ID")
}
return &result, nil
}
// UpdateLink will update the row for corresponding public file token
func (pcr *FileLinkRepository) UpdateLink(ctx context.Context, pct ente.FileLinkRow) error {
_, err := pcr.DB.ExecContext(ctx, `UPDATE public_file_tokens SET valid_till = $1, device_limit = $2,
pw_hash = $3, pw_nonce = $4, mem_limit = $5, ops_limit = $6, enable_download = $7
where id = $8`,
pct.ValidTill, pct.DeviceLimit, pct.PassHash, pct.Nonce, pct.MemLimit, pct.OpsLimit, pct.EnableDownload, pct.LinkID)
return stacktrace.Propagate(err, "failed to update public file token")
}
func (pcr *FileLinkRepository) GetUniqueAccessCount(ctx context.Context, linkId string) (int64, error) {
row := pcr.DB.QueryRowContext(ctx, `SELECT count(*) FROM public_file_tokens_access_history WHERE id = $1`, linkId)
var count int64 = 0
err := row.Scan(&count)
if err != nil {
return -1, stacktrace.Propagate(err, "")
}
return count, nil
}
func (pcr *FileLinkRepository) RecordAccessHistory(ctx context.Context, shareID string, ip string, ua string) error {
_, err := pcr.DB.ExecContext(ctx, `INSERT INTO public_file_tokens_access_history
(id, ip, user_agent) VALUES ($1, $2, $3)
ON CONFLICT ON CONSTRAINT unique_access_id_ip_ua DO NOTHING;`,
shareID, ip, ua)
return stacktrace.Propagate(err, "failed to record access history")
}
// AccessedInPast returns true if the given ip, ua agent combination has accessed the url in the past
func (pcr *FileLinkRepository) AccessedInPast(ctx context.Context, shareID string, ip string, ua string) (bool, error) {
row := pcr.DB.QueryRowContext(ctx, `select id from public_file_tokens_access_history where id =$1 and ip = $2 and user_agent = $3`,
shareID, ip, ua)
var tempID int64
err := row.Scan(&tempID)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return true, stacktrace.Propagate(err, "failed to record access history")
}
// CleanupAccessHistory public_file_tokens_access_history where public_collection_tokens is disabled and the last updated time is older than 30 days
func (pcr *FileLinkRepository) CleanupAccessHistory(ctx context.Context) error {
_, err := pcr.DB.ExecContext(ctx, `DELETE FROM public_file_tokens_access_history WHERE id IN (SELECT id FROM public_file_tokens WHERE is_disabled = TRUE AND updated_at < (now_utc_micro_seconds() - (24::BIGINT * 30 * 60 * 60 * 1000 * 1000)))`)
if err != nil {
return stacktrace.Propagate(err, "failed to clean up public file access history")
}
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/ente-io/museum/pkg/repo/public"
"strings"
"github.com/ente-io/museum/ente"
@@ -32,10 +33,11 @@ type FileWithUpdatedAt struct {
}
type TrashRepository struct {
DB *sql.DB
ObjectRepo *ObjectRepository
FileRepo *FileRepository
QueueRepo *QueueRepository
DB *sql.DB
ObjectRepo *ObjectRepository
FileRepo *FileRepository
QueueRepo *QueueRepository
FileLinkRepo *public.FileLinkRepository
}
func (t *TrashRepository) InsertItems(ctx context.Context, tx *sql.Tx, userID int64, items []ente.TrashItemRequest) error {
@@ -156,6 +158,13 @@ func (t *TrashRepository) TrashFiles(fileIDs []int64, userID int64, trash ente.T
return stacktrace.Propagate(err, "")
}
err = tx.Commit()
if err == nil {
removeLinkErr := t.FileLinkRepo.DisableLinkForFiles(ctx, fileIDs)
if removeLinkErr != nil {
return stacktrace.Propagate(removeLinkErr, "failed to disable file links for files being trashed")
}
}
return stacktrace.Propagate(err, "")
}

View File

@@ -17,8 +17,9 @@ import (
)
const (
PublicAccessKey = "X-Public-Access-ID"
CastContext = "X-Cast-Context"
PublicAccessKey = "X-Public-Access-ID"
FileLinkAccessKey = "X-Public-FileLink-Access-ID"
CastContext = "X-Cast-Context"
)
// GenerateRandomBytes returns securely generated random bytes.
@@ -120,6 +121,8 @@ func GetCastToken(c *gin.Context) string {
return token
}
// GetAccessTokenJWT fetches the JWT access token from the request header or query parameters.
// This token is issued by server on password verification of links that are protected by password.
func GetAccessTokenJWT(c *gin.Context) string {
token := c.GetHeader("X-Auth-Access-Token-JWT")
if token == "" {
@@ -132,6 +135,10 @@ func MustGetPublicAccessContext(c *gin.Context) ente.PublicAccessContext {
return c.MustGet(PublicAccessKey).(ente.PublicAccessContext)
}
func MustGetFileLinkAccessContext(c *gin.Context) *ente.FileLinkAccessContext {
return c.MustGet(FileLinkAccessKey).(*ente.FileLinkAccessContext)
}
func GetCastCtx(c *gin.Context) cast.AuthContext {
return c.MustGet(CastContext).(cast.AuthContext)
}