Compare commits

..

91 Commits

Author SHA1 Message Date
Neeraj Gupta
4e3448d97e Add release 4.4.4 to appdata.xml 2025-08-09 00:49:25 +05:30
Neeraj Gupta
6ac99e6e97 Bump version code 2025-08-09 00:48:32 +05:30
Neeraj Gupta
b57b012840 Bump version 2025-08-09 00:24:48 +05:30
Aman Raj Singh Mourya
8c2cb6dcad [auth] add AvistaZ network icons (#6789)
## Description
Add custom icons for AvistaZ.to network (Private Torrent tracker).
Including:
- [AvistaZ](https://avistaz.to/)
- [AnimeZ](https://animez.to/)
- [CinemaZ](https://cinemaz.to/)
- [ExoticaZ](https://exoticaz.to/)
- [PrivateHD](https://privatehd.to/)
## Tests
2025-08-08 17:33:28 +05:30
Aman Raj Singh Mourya
692f77c9bc [auth] add icon for capcom (#6790)
- added an icon for capcom
- added a json entry for capcom
2025-08-08 17:32:25 +05:30
Brook
dd2a5d6191 [auth] add icon for capcom 2025-08-08 11:37:06 +03:00
Hieu Pham
e66045dc74 [auth] add AvistaZ network icons
Add custom icons for AvistaZ.to network (Private Torrent tracker)
2025-08-08 15:08:08 +07:00
Neeraj
d9e860466c [auth] Auto hide dock icon macos (#6772)
## Description
After closing app or hidding the window on macos the dock icon still
appears even when the app is in tray which shouldn't happen this fixes
it
2025-08-07 20:54:30 +05:30
Manav Rathi
1633c478e3 [photos] [web] Handle XMP files interspersed with live photos (#6778) 2025-08-07 15:55:09 +05:30
Manav Rathi
5eb7cadc4d Fix multi-file scenarios 2025-08-07 15:49:50 +05:30
Manav Rathi
af5de77880 [photos] [web] Handle XMP files interspersed with live photos 2025-08-07 15:30:32 +05:30
Neeraj
58279b668b [mob][photos] Reduce logs on startup (#6775)
## Description

## Tests
2025-08-07 14:08:28 +05:30
Ashil
7537614fc2 [mob][photos] More gallery scroll improvements (#6776) 2025-08-07 14:00:10 +05:30
Ashil
0db2385dbd Merge branch 'main' into gallery_scroll_improvement 2025-08-07 13:59:59 +05:30
Neeraj
fbb5397217 [auth] Add monochrome icon style for macos tray (#6771)
## Description
This fixes this issue https://github.com/ente-io/ente/issues/5173
2025-08-07 13:34:38 +05:30
ashilkn
f4605f86a4 Bump up build number 2025-08-07 13:18:27 +05:30
ashilkn
1f1cad181f Reduce galleryThumbnailDiskLoadDeferDuration 2025-08-07 13:14:21 +05:30
Aman Raj Singh Mourya
7942c4d642 Added logos for meesman.nl, isc2.org, scouting.nl, zivver.com (#6770)
Fixed linting error in custom_icons.json (line 1733, superfluous comma)
and added the aforementioned logos.
2025-08-07 12:15:17 +05:30
Aman Raj Singh Mourya
625410852b [auth] add icon for Dresden University of Technology (#6774)
## Description

Add custom SVG icon for Dresden University of Technology.

Icon optimized with SVGOMG.
2025-08-07 12:10:54 +05:30
Neeraj Gupta
970ca59077 Clean up localSettings log 2025-08-07 11:13:18 +05:30
Neeraj Gupta
3ee021c9de Reduce heartbeat logs 2025-08-07 11:08:58 +05:30
ashilkn
0f8a8a7579 Perf: Skip recreation/modification of ImageProvider of Image used in thumbnail 2025-08-07 11:02:18 +05:30
ashilkn
33703072eb remove todo 2025-08-07 10:56:32 +05:30
Muhammad Talal Anwar
83395641ef chore: add TU Dresden icon 2025-08-07 04:49:40 +00:00
Ashil
8cbdd4cc98 Gallery perf tweaks (#6754) 2025-08-07 10:00:10 +05:30
eYdr1en
df5a7d6c19 auto hide dock icon macos 2025-08-06 15:24:57 +02:00
eYdr1en
95347022e8 add isTemplate for correct macos look on wallpaper 2025-08-06 15:20:44 +02:00
eYdr1en
4f224e7eba add monochrome icon style for macos tray 2025-08-06 15:10:42 +02:00
Ashil
1c2f8a74c4 Merge branch 'main' into gallery_perf_tweks 2025-08-06 14:46:33 +05:30
ashilkn
d86f9d2ffa refactor 2025-08-06 14:45:03 +05:30
Manav Rathi
ffdc21d15c [rust] Make CI fail on warnings (#6763)
https://doc.rust-lang.org/stable/clippy/continuous_integration/github_actions.html
2025-08-06 10:55:37 +05:30
peterv99
abe5548202 Added logos for meesman.nl, isc2.org, scouting.nl, zivver.com
Fixed linting error in custom_icons.json (line 1733, superfluous comma) and added the aforementioned logos.
2025-08-05 19:52:36 +02:00
ashilkn
769adb75c5 Different cache extents for different photoGridSizes 2025-08-05 20:31:10 +05:30
ashilkn
1648f62da6 Add repaint boundary over each gird item in gallery 2025-08-05 16:49:48 +05:30
Manav Rathi
97d66a3afa [rust] Make CI fail on warnings
https://doc.rust-lang.org/stable/clippy/continuous_integration/github_actions.html
2025-08-05 14:39:01 +05:30
Aman Raj Singh Mourya
99556dbbcd [auth] Fixed CONTRIBUTING.md broken link and added new icons (#6749)
## Description

Fixed the broken link to custom-icons.md in CONTRIBUTING.md.

Also added new icons for Twitter (before rebrand), Animal Crossing, Best
Buy, Chucklefish, Toyhouse, and Art Fight.

I realized right after uploading a Best Buy icon that the Best Buy icon
already exists in the custom icon repositories. It doesn't show up when
I search for it in the Ente app (iOS), though, and I checked and there
are no updates for my app. I guess it wasn't pushed to the mobile app
yet?

My icon is called `best_buy.svg`. The one that was already there is
`bestbuy.svg`. Should I remove mine?

Thanks in advance!
2025-08-05 12:10:12 +05:30
Neeraj
d7fdca78f7 Update pubspec.yaml 2025-08-05 10:20:49 +05:30
Neeraj
f7858a96ed [mob][photos] Log info about lock (#6745)
## Description

## Tests
2025-08-05 10:18:23 +05:30
ashilkn
63f24966ce Incrase cacheExtent of gallery 2025-08-05 06:53:54 +05:30
NylaTheWolf
047c2954f8 Actually fix broken link in CONTRIBUTING.md
It's easier to just put everything in one pull request.

The link to the adding-icons.md file was broken, so I fixed it.
2025-08-04 17:25:11 -04:00
NylaTheWolf
11786057e2 Moving broken link fix to another pull request 2025-08-04 17:10:34 -04:00
NylaTheWolf
12648ce726 Update custom-icons.json 2025-08-04 17:00:23 -04:00
NylaTheWolf
782618d26f Merge pull request #1 from NylaTheWolf/patch-1
Fix broken link to adding-icons.md in CONTRIBUTING.md
2025-08-04 16:34:50 -04:00
NylaTheWolf
824b071af4 Added more icons
Icons for Twitter (before rebrand), Art Fight, Toyhouse, Animal Crossing, Best Buy, and Chucklefish.
I wasn't able to find an official SVG of Toyhouse's icon or get it through inspect element, but I got it from here: https://logos.fandom.com/wiki/Toyhouse

Animal Crossing Leaf: Nintendo, Public domain, via Wikimedia Commons

Original Twitter logo: Martin Grasser, per source, Apache License 2.0 <http://www.apache.org/licenses/LICENSE-2.0>, via Wikimedia Commons

Best Buy: Best Buy, Public domain, via Wikimedia Commons

Chucklefish: https://en.wikipedia.org/wiki/File:Chucklefish.svg (Warns that this is not a free logo)
2025-08-04 16:28:45 -04:00
NylaTheWolf
f1f84af3a7 Add Smogon to custom-icons.json 2025-08-04 15:40:27 -04:00
NylaTheWolf
8d484528e7 Delete mobile/apps/auth/assets/custom-icons/icons/Smogon.svg 2025-08-04 15:32:50 -04:00
NylaTheWolf
e95aa55339 Added Smogon icon (filename fixed)
The previous file name was "Smogon.svg." This time it's all lowercase.
2025-08-04 15:32:06 -04:00
NylaTheWolf
0d139df652 Added Smogon icon 2025-08-04 15:30:14 -04:00
NylaTheWolf
568c5393a8 Fix broken link to adding-icons.md in CONTRIBUTING.md
The link to the adding-icons.md file was broken, so I fixed it.
2025-08-04 15:25:36 -04:00
Neeraj
8d8ce6487f [Server] Single file link (#6566)
## Description
Adds 4 authenticate API for
- Creating link for individual file
- Update Link
- Delete Link
- Fetch all links (based on header, the server will return particular
app's link)

For link preview
- API to get Info (pending discussion)
- API to get file attributes (pending discussion)
- APIs to get thumbnail and file
- API to verify password


Pending
- [x] Clean up on account deletion
- [x] Clean up on file deletion
- [x] Clean up history for disabled links

## Tests

Basic santiy check during client integration
2025-08-04 14:41:50 +05:30
Neeraj Gupta
658ba49186 [mob][photos] Log info about lock 2025-08-04 12:42:27 +05:30
Aman Raj Singh Mourya
75bc07f08f updated ubiquiti auth custom icon (#6728)
## Description
Updated Ubiquiti icon to new design, fix #6726
2025-08-03 00:37:40 +05:30
Rafael Ieda
b7ff0ca985 updated ubiquiti auth custom icon 2025-08-02 10:27:27 -03:00
Manav Rathi
a9cba0c7a6 [mob] Update broken relative links (#6722)
## Description
Update broken links in photos from the `Move auth/ to mobile/apps/auth`
in 99d7685be2

Updated the auth's contributing link to be consistent with the photos

## Tests
🔗
2025-08-02 11:30:43 +05:30
ian
a4b938b5d5 Update README.md 2025-08-02 00:07:37 +08:00
ian
0174d82829 Update README.md 2025-08-02 00:02:32 +08:00
ashilkn
1c14896fd6 bump up build number 2025-07-31 22:36:49 +05:30
Neeraj Gupta
fcc90c6725 Bump version 2025-07-25 15:05:16 +05:30
Neeraj Gupta
7037d67a45 Merge branch 'main' into single_file_link 2025-07-25 14:58:49 +05:30
Neeraj Gupta
04e3ad2b77 Fix query bug in delete 2025-07-23 23:15:26 +05:30
Neeraj Gupta
b64a69ebf0 Fix minor bugs 2025-07-23 17:33:28 +05:30
Neeraj Gupta
227ea4a371 Fix bugs 2025-07-23 17:07:01 +05:30
Neeraj Gupta
6b5131ec45 Merge remote-tracking branch 'origin/main' into single_file_link 2025-07-23 14:12:47 +05:30
Neeraj Gupta
4e8fa3babd Merge branch 'main' into single_file_link 2025-07-23 14:12:32 +05:30
Neeraj Gupta
e443838621 Use diff statuscode when accessToken for password is missing 2025-07-21 16:51:23 +05:30
Neeraj Gupta
d9710555ea Add endpoint to get pass-info 2025-07-21 16:48:28 +05:30
Neeraj Gupta
6bed9bd8a2 Send file info 2025-07-21 16:44:22 +05:30
Neeraj Gupta
c57d467965 Disable all links on account deletion 2025-07-21 16:28:37 +05:30
Neeraj Gupta
e8e7f81593 Clean up old link history 2025-07-21 16:25:27 +05:30
Neeraj Gupta
8b6d7e049a Remove link when files are trashed 2025-07-21 11:22:36 +05:30
Neeraj Gupta
e69276cf5f Rename 2025-07-18 16:00:15 +05:30
Neeraj Gupta
eb8737cb46 Add verify password endpoint 2025-07-18 15:58:40 +05:30
Neeraj Gupta
02b93b12fc Fix typo 2025-07-18 15:54:50 +05:30
Neeraj Gupta
2d0d914fd3 Hook APIs to get file or thumbnail from fileLink 2025-07-18 15:27:02 +05:30
Neeraj Gupta
dbb1ad66d3 Rename and minor fixes 2025-07-18 14:50:57 +05:30
Neeraj Gupta
3aa419b430 Add config for locker url 2025-07-18 13:23:55 +05:30
Neeraj Gupta
944bdfc7fa Rename 2025-07-18 13:09:12 +05:30
Neeraj Gupta
13420e4440 Endpoints for create,edit,delete and fetch links 2025-07-18 13:04:21 +05:30
Neeraj Gupta
51c00eefd4 Support for link password validation 2025-07-17 15:27:21 +05:30
Neeraj Gupta
8d108dc719 Rename 2025-07-17 14:39:20 +05:30
Neeraj Gupta
2e49f581c4 File link token middleware 2025-07-17 14:10:37 +05:30
Neeraj Gupta
c5d9b2408f Implement all repo method 2025-07-17 13:33:25 +05:30
Neeraj Gupta
46ba71a15a Fix queries 2025-07-16 17:12:33 +05:30
Neeraj Gupta
99f4d4ca4d Update schema 2025-07-16 10:56:12 +05:30
Neeraj Gupta
47f0722687 Merge branch 'main' into single_file_link 2025-07-15 13:01:29 +05:30
Neeraj Gupta
bbf4462c6c temp 2025-05-28 10:31:49 +05:30
Neeraj Gupta
cf5aabbde1 Merge branch 'main' into single_file_link 2025-05-27 10:28:28 +05:30
Neeraj Gupta
b9b239c207 move refactor 2025-05-12 15:18:13 +05:30
Neeraj Gupta
aa181b1f1f Merge branch 'main' into single_file_link 2025-05-11 10:31:13 +05:30
Neeraj Gupta
daec225ef8 Add DB Scheme for file URL 2025-05-07 16:57:59 +05:30
Neeraj Gupta
c94878e190 Model for single file sharing 2025-05-07 10:51:17 +05:30
84 changed files with 2272 additions and 1712 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,19 @@
"slug": "amtrak",
"hex": "003A5D"
},
{
"title": "Animal Crossing",
"slug:": "animal_crossing",
"altNames": [
"AnimalCrossing",
"Bell Tree Forums",
"BellTree Forums"
]
},
{
"title": "AnimeZ.to",
"slug": "animez"
},
{
"title": "Ankama",
"slug": "ankama"
@@ -81,6 +94,13 @@
"Docaposte AR24"
]
},
{
"title": "Art Fight",
"slug": "art_fight",
"altNames": [
"ArtFight"
]
},
{
"title": "Aruba",
"slug": "aruba",
@@ -106,6 +126,10 @@
{
"title": "availity"
},
{
"title": "AvistaZ.to",
"slug": "avistaz"
},
{
"title": "AzurHosts",
"slug": "azurhosts",
@@ -306,6 +330,14 @@
{
"title": "Capacities"
},
{
"title": "Capcom",
"slug": "capcom",
"hex": "0D4DA2",
"altNames": [
"Capcom Co., Ltd."
]
},
{
"title": "Carta",
"altNames": [
@@ -341,6 +373,13 @@
"slug": "cih",
"hex": "D14633"
},
{
"title": "Chucklefish"
},
{
"title": "CinemaZ.to",
"slug": "cinemaz"
},
{
"title": "Clipper",
"slug": "clippercard",
@@ -593,6 +632,10 @@
],
"hex": "17AB17"
},
{
"title": "ExoticaZ.to",
"slug": "exoticaz"
},
{
"title": "Experian",
"slug": "experian",
@@ -1312,6 +1355,10 @@
"title": "Privacy.com",
"slug": "privacy"
},
{
"title": "PrivateHD.to",
"slug": "privatehd"
},
{
"title": "Proton"
},
@@ -1505,6 +1552,9 @@
{
"title": "Skinport"
},
{
"title": "Smogon"
},
{
"title": "SMSPool",
"slug": "sms_pool_net",
@@ -1664,6 +1714,12 @@
{
"title": "TorGuard"
},
{
"title": "Toyhouse",
"altNames": [
"Toyhou.se"
]
},
{
"title": "Trading 212"
},
@@ -1685,6 +1741,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 +1764,12 @@
"Twitch tv"
]
},
{
"title": "Twitter",
"altNames": [
"X"
]
},
{
"title": "Ubiquiti",
"slug": "ubiquiti",
@@ -1764,7 +1835,7 @@
"title": "Warner Bros.",
"slug": "warner_bros",
"altNames": [
"Warner Brothers"
"Warner Brothers"
]
},
{
@@ -1777,7 +1848,7 @@
"title": "WEB.DE",
"slug": "web_de"
},
{
{
"title": "WeMod",
"slug": "wemod",
"altNames": [
@@ -1906,6 +1977,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: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 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

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="926.65619"
height="958.5625"
id="svg2"
inkscape:version="0.48.1 "
sodipodi:docname="Capcom icon.svg">
<metadata
id="metadata3015">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1835"
inkscape:window-height="1058"
id="namedview3013"
showgrid="false"
inkscape:zoom="0.32"
inkscape:cx="1623.239"
inkscape:cy="170.71783"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<defs
id="defs4" />
<g
transform="translate(2426,-55.799683)"
id="layer1">
<path
d="m -1552.0877,1007.6508 4.1189,-358.25737 -199.9062,0 c -15.7424,43.40396 -59.9402,72.71875 -113.25,72.71875 -28.0932,0 -58.13,-6.62565 -82.4688,-21.25 -31.7176,-19.0581 -55.4185,-41.69206 -74.9375,-81.90625 -15.3567,-37.21359 -27.109,-82.77963 -28.4687,-123.09375 4.6791,-79.72921 28.9019,-108.14789 73.25,-141.03125 9.8313,-7.28971 37.2996,-13.1189 45.2187,-13.15625 17.4221,-0.0822 31.9427,10.48615 39.8125,26.0625 11.4471,22.65651 -5.3079,50.1006 -21.875,69.625 l 161.625,111.34375 249.625,-261.1875 c -87.0859,-138.79748 -246.2201,-231.718747 -428.0937,-231.718747 -275.3534,0 -498.5625,212.989027 -498.5625,475.718747 0,262.72972 216.0662,461.41517 498.5625,482.84377 l 376.5933,-1.6875 c 0.2227,-314.9224 -12.7573,-290.91079 -1.2435,-5.0239 z"
id="path3840"
style="fill:#0c4da2;fill-opacity:1;stroke:none"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccssccssscccssccc" />
<path
d="m -1927.7708,145.18332 c -226.9823,1.42857 -412.3662,166.7661 -412.3662,383.05133 0,216.28523 204.6469,402.07109 452.3662,396.47808 l 240.7127,0.6783 0,-189.50724 -45.4634,0 c -14.2837,35.73115 -80.9968,70.68064 -129.3669,70.68064 -25.4902,0 -89.6606,-3.46506 -113.3988,-11.7792 -41.9106,-14.67892 -101.712,-35.75045 -153.7082,-123.14142 -13.9338,-30.6351 -53.1686,-105.28898 -44.4023,-214.19078 4.2456,-65.63495 45.9366,-147.56776 120.7486,-184.67165 32.6212,-16.17886 65.4607,-18.83348 123.1208,-13.60101 41.3957,3.75653 82.5276,32.86328 96.8886,67.79717 11.107,27.01833 7.9153,59.94169 -5.16,78.69532 -0.9328,1.33796 -0.9528,2.52293 -0.7595,3.80017 0.1471,0.97218 1.408,2.592 1.408,2.592 l 22.7845,17.79932 c 0,0 6.4866,4.50696 14.0636,4.30516 4.4004,-0.11719 12.2667,-4.89104 12.2667,-4.89104 l 130.1212,-136.55317 c -80.4452,-108.54703 -223.4056,-147.54198 -309.8556,-147.54198 z"
id="path3950"
style="fill:#ffcb08;fill-opacity:1;stroke:none"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 438 438" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Artboard1" x="0" y="0" width="437.5" height="437.5" style="fill:none;"/><g id="layer101"><path d="M80.861,420.364c-4.353,-1.171 -8.1,-4.433 -10.133,-8.799l-1.102,-2.397l0.27,-99.121c0.291,-94.079 0.36,-100.573 1.396,-103.303c0.216,-0.469 -1.105,-1.372 -5.198,-3.524c-25.794,-13.562 -44.713,-37.051 -52.206,-64.809c-2.864,-10.522 -3.359,-14.797 -3.306,-28.127c0.096,-13.244 0.362,-15.721 3.139,-26.22c4.859,-18.438 14.538,-34.891 28.434,-48.251c15.784,-15.189 35.348,-24.767 57.621,-28.224c6.712,-1.041 23.289,-0.974 30.121,0.164c17.719,2.848 33.842,9.706 47.898,20.401c5.197,3.951 12.219,10.687 16.429,15.746c1.786,2.143 3.359,3.944 3.53,4.03c0.17,0.043 1.457,-1.276 2.831,-2.98c1.374,-1.703 4.464,-5.023 6.823,-7.364c16,-15.872 36.506,-26.044 59.249,-29.328c6.883,-0.998 22.306,-0.936 29.095,0.117c24.21,3.771 45.357,14.964 61.863,32.804c14.591,15.824 23.698,35.557 26.684,57.999c0.705,5.216 0.645,20.34 -0.103,25.635c-3.387,24.211 -13.551,44.465 -30.708,61.272c-8.322,8.17 -17.577,14.713 -27.465,19.501c-1.841,0.89 -2.398,1.358 -2.187,1.871c1.355,3.125 1.433,5.048 1.331,30.426c-0.056,14.056 -0.017,25.549 0.111,25.55c0.128,0 18.029,-10.353 39.741,-22.998c21.712,-12.602 40.555,-23.464 41.839,-24.057c1.969,-0.932 2.952,-1.099 6.199,-1.086c3.29,0.013 4.229,0.188 6.276,1.178c3.241,1.509 6.433,4.598 7.874,7.594l1.186,2.398l-0.204,83.099c-0.171,63.745 -0.335,83.526 -0.726,84.977c-1.733,6.188 -8.375,11.117 -14.998,11.091c-4.742,-0.019 -4.742,-0.019 -46.94,-25.012l-40.62,-24.046l-0.32,26.574c-0.309,23.754 -0.407,26.83 -1.054,28.451c-1.899,4.649 -5.458,7.967 -10.164,9.444c-2.695,0.843 -3.763,0.839 -116.64,0.344c-88.824,-0.356 -114.33,-0.586 -115.866,-1.02Zm140.434,-223.059c-0.256,-0.215 -1.661,-1.331 -3.195,-2.448c-6.858,-5.197 -12.604,-10.732 -18.726,-18.148c-1.021,-1.243 -2,-2.187 -2.128,-2.145c-0.171,0.085 -1.759,1.873 -3.562,4.002c-4.25,5.067 -10.769,11.151 -16.426,15.444l-4.585,3.442l24.524,0.098c13.458,0.054 24.31,-0.073 24.098,-0.245Zm-94.961,-25.46c25.4,-5.495 44.191,-24.646 49.163,-50.005c1.571,-8.239 1.017,-19.307 -1.512,-27.991c-5.519,-19.291 -20.708,-35.245 -39.61,-41.602c-7.594,-2.551 -10.669,-3.033 -19.726,-3.069c-8.673,-0.035 -11.751,0.38 -18.767,2.573c-19.293,5.99 -35.166,22.119 -40.926,41.621c-2.598,8.663 -3.241,19.726 -1.736,27.978c2.552,13.511 8.999,25.415 18.788,34.683c9.024,8.495 19.726,13.836 32.534,16.194c4.355,0.787 17.43,0.583 21.792,-0.382Zm167.009,0.498c5.987,-1.343 8.683,-2.315 14.804,-5.324c17.081,-8.391 28.977,-23.639 33.369,-42.847c0.87,-3.756 1.006,-5.636 1.033,-12.344c0.026,-6.707 -0.094,-8.588 -0.934,-12.351c-5.454,-24.845 -23.837,-43.29 -48.596,-48.773c-5.165,-1.174 -18.751,-1.228 -23.925,-0.096c-25.999,5.536 -45.305,25.454 -49.553,51.114c-0.831,4.91 -0.869,14.31 -0.077,19.226c4.125,26.164 24.251,46.796 50.422,51.686c6.233,1.178 17.642,1.01 23.457,-0.291Z" style="fill:#fff;fill-rule:nonzero;"/></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 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 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 557 557" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Artboard1" x="0" y="0" width="556.25" height="556.25" style="fill:none;"/><g><path d="M45.455,412.973c-8.755,-1.966 -14.046,-4.401 -21.21,-9.692c-9.88,-7.304 -16.341,-17.418 -19.9,-31.231c-0.655,-2.481 -0.749,-13.157 -0.889,-90.601c-0.141,-62.086 -0.047,-89.102 0.328,-92.333c2.013,-17.746 12.969,-33.384 28.795,-41.251c8.896,-4.401 14.609,-5.478 29.545,-5.478l10.956,0l0,109.096l121.27,0l0,-109.189l12.97,0.187c12.267,0.187 13.297,0.281 17.699,1.451c18.682,4.964 32.963,19.432 37.832,38.348l1.498,5.759l0,180.266l-1.451,5.618c-3.886,14.984 -12.642,26.408 -25.893,33.712c-4.495,2.482 -8.849,4.074 -14.702,5.338c-3.933,0.843 -6.087,0.983 -16.247,0.983l-11.706,0l0,-107.223l-121.27,0l0,107.223l-11.799,-0.046c-9.879,-0.047 -12.501,-0.188 -15.826,-0.937Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/><path d="M272.625,413.956l0,-271.569l118.226,0.094l118.226,0.14l4.449,1.218c8.943,2.481 16.668,6.883 23.177,13.11c8.053,7.726 12.829,16.294 15.17,27.204c0.983,4.541 0.983,4.963 0.983,94.112l0,89.571l-1.123,4.917c-2.342,10.254 -6.275,17.558 -13.485,25.097c-7.679,8.006 -16.294,12.782 -27.157,15.123l-4.589,0.983l-233.877,0Zm215.148,-61.805l0,-147.958l-150.767,0l0,147.958l150.767,0Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.66288e-14,271.569,-271.569,1.66288e-14,269.5,142.387)"><stop offset="0" style="stop-color:#decc84;stop-opacity:1"/><stop offset="0.49" style="stop-color:#e1b821;stop-opacity:1"/><stop offset="1" style="stop-color:#e2b201;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.66288e-14,271.569,-271.569,1.66288e-14,272.625,142.387)"><stop offset="0" style="stop-color:#decc84;stop-opacity:1"/><stop offset="0.49" style="stop-color:#e1b821;stop-opacity:1"/><stop offset="1" style="stop-color:#e2b201;stop-opacity:1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.4 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

@@ -87,9 +87,9 @@ PODS:
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- SDWebImage (5.21.1):
- SDWebImage/Core (= 5.21.1)
- SDWebImage/Core (5.21.1)
- Sentry/HybridSDK (8.46.0)
- sentry_flutter (8.14.2):
- Flutter
@@ -258,7 +258,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImage: f29024626962457f3470184232766516dee8dfea
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad

View File

@@ -199,9 +199,11 @@ class _AppState extends State<App>
switch (menuItem.key) {
case 'hide_window':
windowManager.hide();
windowManager.setSkipTaskbar(true);
break;
case 'show_window':
windowManager.show();
windowManager.setSkipTaskbar(false);
break;
case 'exit_app':
windowManager.destroy();

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

@@ -18,6 +18,7 @@
</screenshot>
</screenshots>
<releases>
<release version="4.4.4" date="2025-08-09" />
<release version="4.4.3" date="2025-06-21" />
<release version="4.4.2" date="2025-06-21" />
<release version="4.4.0" date="2025-05-31" />

View File

@@ -157,33 +157,33 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS:
app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468
connectivity_plus: 3f6c9057f4cd64198dc826edfb0542892f825343
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_local_authentication: 2f9a2682f498abcc12d7e9729b5007a947170fdc
flutter_local_notifications: 453432cd6399a07d072885bc7828fb2307868856
flutter_secure_storage_macos: b2d62a774c23b060f0b99d0173b0b36abb4a8632
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
flutter_local_authentication: 85674893931e1c9cfa7c9e4f5973cb8c56b018b0
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
objective_c: ec13431e45ba099cb734eb2829a5c1cd37986cba
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
objective_c: e5f8194456e8fc943e034d1af00510a1bc29c067
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: a8a591e70e87ce97ce5d21b2594f69cea9e0312f
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
share_plus: 11c7b7fa7020465584eca3ff6392c5bc1e399d6e
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sodium_libs: b9459e5bfc1185349f43472e79fc5d8e526b2bda
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sodium_libs: d39bd76697736cb11ce4a0be73b9b4bc64466d6f
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
sqlite3_flutter_libs: 03311aede9d32fb2d24e32bebb8cd01c3b2e6239
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
PODFILE CHECKSUM: 6ff827273ace187339fc5d3684072a26ad85c298

View File

@@ -11,6 +11,7 @@ class AppDelegate: FlutterAppDelegate {
}
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
NSApp.setActivationPolicy(.accessory)
return false
}
}

View File

@@ -1,7 +1,7 @@
name: ente_auth
description: ente two-factor authenticator
version: 4.4.3+443
version: 4.4.4+448
publish_to: none
environment:

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

@@ -27,7 +27,7 @@ const subGalleryMultiplier = 10;
// used to identify which ente file are available in app cache
const String sharedMediaIdentifier = 'ente-shared-media://';
const galleryThumbnailDiskLoadDeferDuration = Duration(milliseconds: 500);
const galleryThumbnailDiskLoadDeferDuration = Duration(milliseconds: 80);
const galleryThumbnailServerLoadDeferDuration = Duration(milliseconds: 80);
// 256 bit key maps to 24 words

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

@@ -61,6 +61,7 @@ const kBGTaskTimeout = Duration(seconds: 28);
const kBGPushTimeout = Duration(seconds: 28);
const kFGTaskDeathTimeoutInMicroseconds = 5000000;
bool isProcessBg = true;
bool _stopHearBeat = false;
void main() async {
debugRepaintRainbowEnabled = false;
@@ -291,6 +292,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
EnteWakeLockService.instance.init(preferences);
logLocalSettings();
initComplete = true;
_stopHearBeat = true;
_logger.info("Initialization done $tlog");
} catch (e, s) {
_logger.severe("Error in init ", e, s);
@@ -299,25 +301,29 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
}
void logLocalSettings() {
_logger.info("Show memories: ${memoriesCacheService.showAnyMemories}");
_logger
.info("Smart memories enabled: ${localSettings.isSmartMemoriesEnabled}");
_logger.info("Ml is enabled: ${flagService.hasGrantedMLConsent}");
_logger.info(
"ML local indexing is enabled: ${localSettings.isMLLocalIndexingEnabled}",
);
_logger.info(
"Multipart upload is enabled: ${localSettings.userEnabledMultiplePart}",
);
_logger.info("Gallery grid size: ${localSettings.getPhotoGridSize()}");
_logger.info(
"Video streaming is enalbed: ${VideoPreviewService.instance.isVideoStreamingEnabled}",
);
final settings = {
'Show memories': memoriesCacheService.showAnyMemories,
'Smart memories enabled': localSettings.isSmartMemoriesEnabled,
'ML enabled': flagService.hasGrantedMLConsent,
'ML local indexing enabled': localSettings.isMLLocalIndexingEnabled,
'Multipart upload enabled': localSettings.userEnabledMultiplePart,
'Gallery grid size': localSettings.getPhotoGridSize(),
'Video streaming enabled':
VideoPreviewService.instance.isVideoStreamingEnabled,
};
final formattedSettings =
settings.entries.map((e) => '${e.key}: ${e.value}').join(', ');
_logger.info('Local settings - $formattedSettings');
}
void _heartBeatOnInit(int i) {
if (i <= 15) {
if (i <= 15 && !_stopHearBeat) {
Future.delayed(const Duration(seconds: 1), () {
if (_stopHearBeat) {
_logger.info("Stopping Heartbeat check at $i");
return;
}
_logger.info("init Heartbeat $i");
_heartBeatOnInit(i + 1);
});

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

@@ -151,13 +151,7 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
Widget? image;
if (_imageProvider != null) {
image = Image(
image: optimizedImageHeight != null || optimizedImageWidth != null
? ResizeImage(
_imageProvider!,
width: optimizedImageWidth,
height: optimizedImageHeight,
)
: _imageProvider!,
image: _imageProvider!,
fit: widget.fit,
);
}
@@ -251,7 +245,11 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
final cachedSmallThumbnail =
ThumbnailInMemoryLruCache.get(widget.file, thumbnailSmallSize);
if (cachedSmallThumbnail != null) {
_imageProvider = Image.memory(cachedSmallThumbnail).image;
_imageProvider = Image.memory(
cachedSmallThumbnail,
cacheHeight: optimizedImageHeight,
cacheWidth: optimizedImageWidth,
).image;
_hasLoadedThumbnail = true;
} else {
if (widget.diskLoadDeferDuration != null) {
@@ -296,7 +294,11 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
}
if (mounted) {
final imageProvider = Image.memory(thumbData).image;
final imageProvider = Image.memory(
thumbData,
cacheHeight: optimizedImageHeight,
cacheWidth: optimizedImageWidth,
).image;
_cacheAndRender(imageProvider);
}
ThumbnailInMemoryLruCache.put(
@@ -381,10 +383,15 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
_isLoadingRemoteThumbnail = true;
final cachedThumbnail = ThumbnailInMemoryLruCache.get(widget.file);
if (cachedThumbnail != null) {
_imageProvider = Image.memory(cachedThumbnail).image;
_imageProvider = Image.memory(
cachedThumbnail,
cacheHeight: optimizedImageHeight,
cacheWidth: optimizedImageWidth,
).image;
_hasLoadedThumbnail = true;
return;
}
if (widget.serverLoadDeferDuration != null) {
Future.delayed(widget.serverLoadDeferDuration!, () {
if (mounted) {
@@ -401,7 +408,11 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
try {
final thumbnail = await getThumbnailFromServer(widget.file);
if (mounted) {
final imageProvider = Image.memory(thumbnail).image;
final imageProvider = Image.memory(
thumbnail,
cacheHeight: optimizedImageHeight,
cacheWidth: optimizedImageWidth,
).image;
_cacheAndRender(imageProvider);
}
} catch (e) {

View File

@@ -42,12 +42,6 @@ class SectionedListSliver<T> extends StatelessWidget {
sectionLayouts: sectionLayouts,
delegate: SliverChildBuilderDelegate(
(context, index) {
//TODO:
// This could be optimized by using a combination of
//linear search and binary search depending on the index (use linear
//if index is small) or keep track on lastIndex of section and
//go to next section after the last index.
// Check if the optimization is required.
if (index >= childCount) return null;
final sectionLayout = sectionLayouts
.firstWhereOrNull((section) => section.hasChild(index));

View File

@@ -600,6 +600,7 @@ class GalleryState extends State<Gallery> {
? const NeverScrollableScrollPhysics()
: const ExponentialBouncingScrollPhysics(),
controller: _scrollController,
cacheExtent: galleryCacheExtent,
slivers: [
SliverToBoxAdapter(
child: SizeChangedLayoutNotifier(
@@ -637,6 +638,25 @@ class GalleryState extends State<Gallery> {
),
);
}
double get galleryCacheExtent {
final int photoGridSize = localSettings.getPhotoGridSize();
switch (photoGridSize) {
case 2:
case 3:
return 1000;
case 4:
return 850;
case 5:
return 600;
case 6:
return 300;
default:
throw StateError(
'Invalid photo grid size configuration: $photoGridSize',
);
}
}
}
class PinnedGroupHeader extends StatefulWidget {

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

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.2.0+1200
version: 1.2.0+1203
publish_to: none
environment:

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)
}

View File

@@ -1,85 +0,0 @@
import React from 'react';
import { NavbarBase } from 'ente-base/components/Navbar';
import { NormalNavbarContents, HiddenSectionNavbarContents } from './NavbarContents';
import type { EnteFile } from 'ente-media/file';
import type { SelectedState } from 'utils/file';
interface GalleryContainerProps {
// State props
filteredFiles: EnteFile[];
selected: SelectedState;
barMode: string;
isInSearchMode: boolean;
isFirstLoad: boolean;
activeCollectionID: number | undefined;
// Event handlers
onSidebar: () => void;
onUpload: () => void;
onSelectSearchOption: (option: unknown) => void;
onSelectPeople: () => void;
onSelectPerson: (personID: string) => void;
onClearSelection: () => void;
onChangeMode: (mode: string) => void;
// Children for flexible rendering
children: React.ReactNode;
}
/**
* Simplified gallery container component that handles the basic layout
*/
export const GalleryContainer: React.FC<GalleryContainerProps> = ({
selected,
barMode,
isInSearchMode,
activeCollectionID,
onSidebar,
onUpload,
onSelectSearchOption,
onSelectPeople,
onSelectPerson,
onChangeMode,
children,
}) => {
const showSelectionBar = selected.count > 0 && selected.collectionID === activeCollectionID;
return (
<>
{/* Navigation Bar */}
<NavbarBase
sx={[
{
mb: "12px",
px: "24px",
"@media (width < 720px)": { px: "4px" },
},
showSelectionBar && { borderColor: "accent.main" },
]}
>
{showSelectionBar ? (
<div>Selection Bar Placeholder</div>
) : barMode === "hidden-albums" ? (
<HiddenSectionNavbarContents
onBack={() => onChangeMode("albums")}
/>
) : (
<NormalNavbarContents
isInSearchMode={isInSearchMode}
onSidebar={onSidebar}
onUpload={onUpload}
onShowSearchInput={() => {
// TODO: Implement search mode toggle
}}
onSelectSearchOption={onSelectSearchOption}
onSelectPeople={onSelectPeople}
onSelectPerson={onSelectPerson}
/>
)}
</NavbarBase>
{/* Main Content Area */}
{children}
</>
);
};

View File

@@ -1,65 +0,0 @@
import React from 'react';
import { GalleryEmptyState, PeopleEmptyState } from 'ente-new/photos/components/gallery';
import { uploadManager } from 'services/upload-manager';
import { PseudoCollectionID } from 'ente-new/photos/services/collection-summary';
import type { EnteFile } from 'ente-media/file';
import type { GalleryBarMode } from 'ente-new/photos/components/gallery/reducer';
interface GalleryContentProps {
// Basic state
filteredFiles: EnteFile[];
isInSearchMode: boolean;
isFirstLoad: boolean;
barMode: GalleryBarMode;
activeCollectionID: number | undefined;
activePerson: unknown;
// Event handlers
onUpload: () => void;
// Children for flexible content
children?: React.ReactNode;
}
/**
* Simplified main content area that handles empty states and delegates to children
*/
export const GalleryContent: React.FC<GalleryContentProps> = ({
filteredFiles,
isInSearchMode,
isFirstLoad,
barMode,
activeCollectionID,
activePerson,
onUpload,
children,
}) => {
// Show empty states for specific conditions
const showGalleryEmptyState =
!isInSearchMode &&
!isFirstLoad &&
!filteredFiles.length &&
activeCollectionID === PseudoCollectionID.all;
const showPeopleEmptyState =
!isInSearchMode &&
!isFirstLoad &&
barMode === "people" &&
!activePerson;
if (showGalleryEmptyState) {
return (
<GalleryEmptyState
isUploadInProgress={uploadManager.isUploadInProgress()}
onUpload={onUpload}
/>
);
}
if (showPeopleEmptyState) {
return <PeopleEmptyState />;
}
// Render children (FileListWithViewer, GalleryBarAndListHeader, etc.)
return <>{children}</>;
};

View File

@@ -1,66 +0,0 @@
import React from 'react';
import { PlanSelector } from 'ente-new/photos/components/PlanSelector';
import { SingleInputDialog } from 'ente-base/components/SingleInputDialog';
import { WhatsNew } from 'ente-new/photos/components/WhatsNew';
import { t } from 'i18next';
interface GalleryModalsProps {
// Modal states - using simplified visibility props
planSelectorVisible: boolean;
whatsNewVisible: boolean;
albumNameInputVisible: boolean;
// Modal handlers
onClosePlanSelector: () => void;
onCloseWhatsNew: () => void;
onCloseAlbumNameInput: () => void;
onAlbumNameSubmit: (name: string) => Promise<void>;
setLoading: (loading: boolean) => void;
}
/**
* Simplified container for essential gallery modals
*/
export const GalleryModals: React.FC<GalleryModalsProps> = ({
planSelectorVisible,
whatsNewVisible,
albumNameInputVisible,
onClosePlanSelector,
onCloseWhatsNew,
onCloseAlbumNameInput,
onAlbumNameSubmit,
setLoading,
}) => {
return (
<>
{/* Plan Selector Modal */}
{planSelectorVisible && (
<PlanSelector
open={planSelectorVisible}
onClose={onClosePlanSelector}
setLoading={setLoading}
/>
)}
{/* What's New Dialog */}
{whatsNewVisible && (
<WhatsNew
open={whatsNewVisible}
onClose={onCloseWhatsNew}
/>
)}
{/* Album Name Input Dialog */}
{albumNameInputVisible && (
<SingleInputDialog
open={albumNameInputVisible}
onClose={onCloseAlbumNameInput}
title={t("new_album")}
label={t("album_name")}
submitButtonTitle={t("create")}
onSubmit={onAlbumNameSubmit}
/>
)}
</>
);
};

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
import { CenteredRow } from 'ente-base/components/containers';
import { TranslucentLoadingOverlay } from 'ente-base/components/loaders';
import { t } from 'i18next';
/**
* Message shown during first load to inform users about potential delays
*/
export const FirstLoadMessage: React.FC = () => (
<CenteredRow>
<Typography variant="small" sx={{ color: "text.muted" }}>
{t("initial_load_delay_warning")}
</Typography>
</CenteredRow>
);
/**
* Message shown when the app is offline
*/
export const OfflineMessage: React.FC = () => (
<Typography
variant="small"
sx={{ bgcolor: "background.paper", p: 2, mb: 1, textAlign: "center" }}
>
{t("offline_message")}
</Typography>
);
/**
* Blocking overlay shown during certain operations
*/
export const BlockingLoadOverlay: React.FC<{ show: boolean }> = ({ show }) =>
show ? <TranslucentLoadingOverlay /> : null;

View File

@@ -1,80 +0,0 @@
import React from 'react';
import { Stack, Typography, IconButton } from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import MenuIcon from '@mui/icons-material/Menu';
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
import { FocusVisibleButton } from 'ente-base/components/mui/FocusVisibleButton';
import { useIsSmallWidth } from 'ente-base/components/utils/hooks';
import type { ButtonishProps } from 'ente-base/components/mui';
import { SearchBar, type SearchBarProps } from 'ente-new/photos/components/SearchBar';
import { uploadManager } from 'services/upload-manager';
import { t } from 'i18next';
interface NormalNavbarContentsProps extends SearchBarProps {
onSidebar: () => void;
onUpload: () => void;
}
export const NormalNavbarContents: React.FC<NormalNavbarContentsProps> = ({
onSidebar,
onUpload,
...props
}) => (
<>
{!props.isInSearchMode && <SidebarButton onClick={onSidebar} />}
<SearchBar {...props} />
{!props.isInSearchMode && <UploadButton onClick={onUpload} />}
</>
);
const SidebarButton: React.FC<ButtonishProps> = ({ onClick }) => (
<IconButton {...{ onClick }}>
<MenuIcon />
</IconButton>
);
const UploadButton: React.FC<ButtonishProps> = ({ onClick }) => {
const disabled = uploadManager.isUploadInProgress();
const isSmallWidth = useIsSmallWidth();
const icon = <FileUploadOutlinedIcon />;
return (
<>
{isSmallWidth ? (
<IconButton {...{ onClick, disabled }}>{icon}</IconButton>
) : (
<FocusVisibleButton
color="secondary"
startIcon={icon}
{...{ onClick, disabled }}
>
{t("upload")}
</FocusVisibleButton>
)}
</>
);
};
interface HiddenSectionNavbarContentsProps {
onBack: () => void;
}
export const HiddenSectionNavbarContents: React.FC<
HiddenSectionNavbarContentsProps
> = ({ onBack }) => (
<Stack
direction="row"
sx={(theme) => ({
gap: "24px",
flex: 1,
alignItems: "center",
background: theme.vars.palette.background.default,
})}
>
<IconButton onClick={onBack}>
<ArrowBackIcon />
</IconButton>
<Typography sx={{ flex: 1 }}>{t("section_hidden")}</Typography>
</Stack>
);

View File

@@ -1,6 +0,0 @@
// Gallery presentational components
export { GalleryContainer } from './GalleryContainer';
export { GalleryContent } from './GalleryContent';
export { GalleryModals } from './GalleryModals';
export { NormalNavbarContents, HiddenSectionNavbarContents } from './NavbarContents';
export { FirstLoadMessage, OfflineMessage, BlockingLoadOverlay } from './MessageComponents';

View File

@@ -1,11 +0,0 @@
// Gallery hooks for managing different aspects of the gallery component
export { useGalleryData } from './useGalleryData';
export { useFileOperations } from './useFileOperations';
export { useSelection } from './useSelection';
export { useCollectionOperations } from './useCollectionOperations';
export { useModalManagement } from './useModalManagement';
export { useGalleryInitialization } from './useGalleryInitialization';
export { useGalleryNavigation } from './useGalleryNavigation';
// Re-export types for convenience
export type { RemotePullOpts } from './useGalleryData';

View File

@@ -1,167 +0,0 @@
import { useCallback, useState } from 'react';
import type { Collection } from 'ente-media/collection';
import type { EnteFile } from 'ente-media/file';
import type { SelectedState } from 'utils/file';
import type { CollectionOp } from 'ente-new/photos/components/SelectedFileOptions';
import type { CollectionSelectorAttributes } from 'ente-new/photos/components/CollectionSelector';
import { createAlbum } from 'ente-new/photos/services/collection';
import { performCollectionOp } from 'ente-new/photos/components/gallery/helpers';
import { getSelectedFiles } from 'utils/file';
import { usePhotosAppContext } from 'ente-new/photos/types/context';
import { useBaseContext } from 'ente-base/context';
import { notifyOthersFilesDialogAttributes } from 'ente-new/photos/components/utils/dialog-attributes';
interface UseCollectionOperationsProps {
user: { id: number } | null;
filteredFiles: EnteFile[];
selected: SelectedState;
clearSelection: () => void;
onRemotePull: () => Promise<void>;
}
/**
* Custom hook for managing collection operations
*/
export const useCollectionOperations = ({
user,
filteredFiles,
selected,
clearSelection,
onRemotePull,
}: UseCollectionOperationsProps) => {
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
const { onGenericError, showMiniDialog } = useBaseContext();
// Collection selector state
const [openCollectionSelector, setOpenCollectionSelector] = useState(false);
const [collectionSelectorAttributes, setCollectionSelectorAttributes] =
useState<CollectionSelectorAttributes | undefined>();
// Album creation state
const [postCreateAlbumOp, setPostCreateAlbumOp] = useState<CollectionOp | undefined>();
/**
* Create a handler for collection operations (add, move)
*/
const createOnSelectForCollectionOp = useCallback(
(op: CollectionOp) => (selectedCollection: Collection) => {
void (async () => {
showLoadingBar();
try {
setOpenCollectionSelector(false);
const selectedFiles = getSelectedFiles(selected, filteredFiles);
const userFiles = selectedFiles.filter(
(f) => f.ownerID === user!.id,
);
const sourceCollectionID = selected.collectionID;
if (userFiles.length > 0) {
await performCollectionOp(
op,
selectedCollection,
userFiles,
sourceCollectionID,
);
}
// Notify if some files couldn't be processed
if (userFiles.length !== selectedFiles.length) {
showMiniDialog(notifyOthersFilesDialogAttributes());
}
clearSelection();
await onRemotePull();
} catch (e) {
onGenericError(e);
} finally {
hideLoadingBar();
}
})();
},
[
showLoadingBar,
hideLoadingBar,
selected,
filteredFiles,
user,
showMiniDialog,
clearSelection,
onRemotePull,
onGenericError,
],
);
/**
* Create a handler for collection operations that need album creation
*/
const createOnCreateForCollectionOp = useCallback(
(op: CollectionOp) => {
setPostCreateAlbumOp(op);
return () => {
// This will be handled by the album name input dialog
// The actual creation happens in handleAlbumNameSubmit
};
},
[],
);
/**
* Handle album name submission after creation
*/
const handleAlbumNameSubmit = useCallback(
async (name: string) => {
if (!postCreateAlbumOp) return;
try {
const collection = await createAlbum(name);
// Execute the deferred operation
createOnSelectForCollectionOp(postCreateAlbumOp)(collection);
setPostCreateAlbumOp(undefined);
} catch (e) {
onGenericError(e);
setPostCreateAlbumOp(undefined);
}
},
[postCreateAlbumOp, createOnSelectForCollectionOp, onGenericError],
);
/**
* Open collection selector with specific attributes
*/
const handleOpenCollectionSelector = useCallback(
(attributes: CollectionSelectorAttributes) => {
setCollectionSelectorAttributes(attributes);
setOpenCollectionSelector(true);
},
[],
);
/**
* Close collection selector
*/
const handleCloseCollectionSelector = useCallback(
() => {
setOpenCollectionSelector(false);
setCollectionSelectorAttributes(undefined);
},
[],
);
return {
// State
openCollectionSelector,
collectionSelectorAttributes,
postCreateAlbumOp,
// Handlers
createOnSelectForCollectionOp,
createOnCreateForCollectionOp,
handleAlbumNameSubmit,
handleOpenCollectionSelector,
handleCloseCollectionSelector,
// Setters for external control
setOpenCollectionSelector,
setCollectionSelectorAttributes,
};
};

View File

@@ -1,183 +0,0 @@
import { useCallback } from 'react';
import type { EnteFile } from 'ente-media/file';
import type { ItemVisibility } from 'ente-media/file-metadata';
import type { Collection } from 'ente-media/collection';
import {
addToFavoritesCollection,
removeFromFavoritesCollection,
removeFromCollection
} from 'ente-new/photos/services/collection';
import { updateFilesVisibility } from 'ente-new/photos/services/file';
import { getSelectedFiles, type SelectedState } from 'utils/file';
import type { FileOp } from 'ente-new/photos/components/SelectedFileOptions';
import { usePhotosAppContext } from 'ente-new/photos/types/context';
import { useBaseContext } from 'ente-base/context';
import { notifyOthersFilesDialogAttributes } from 'ente-new/photos/components/utils/dialog-attributes';
interface UseFileOperationsProps {
user: { id: number };
filteredFiles: EnteFile[];
selected: SelectedState;
clearSelection: () => void;
onRemotePull: () => Promise<void>;
dispatch: (action: { type: string; [key: string]: unknown }) => void;
favoriteFileIDs: Set<number>;
}
/**
* Custom hook for handling file operations
*/
export const useFileOperations = ({
user,
filteredFiles,
selected,
clearSelection,
onRemotePull,
dispatch,
favoriteFileIDs,
}: UseFileOperationsProps) => {
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
const { onGenericError, showMiniDialog } = useBaseContext();
/**
* Toggle favorite status of a file
*/
const handleToggleFavorite = useCallback(
async (file: EnteFile) => {
const fileID = file.id;
const isFavorite = favoriteFileIDs.has(fileID);
dispatch({ type: "addPendingFavoriteUpdate", fileID });
try {
const action = isFavorite
? removeFromFavoritesCollection
: addToFavoritesCollection;
await action([file]);
dispatch({
type: "unsyncedFavoriteUpdate",
fileID,
isFavorite: !isFavorite,
});
} finally {
dispatch({ type: "removePendingFavoriteUpdate", fileID });
}
},
[favoriteFileIDs, dispatch],
);
/**
* Update file visibility
*/
const handleFileVisibilityUpdate = useCallback(
async (file: EnteFile, visibility: ItemVisibility) => {
const fileID = file.id;
dispatch({ type: "addPendingVisibilityUpdate", fileID });
try {
await updateFilesVisibility([file], visibility);
dispatch({
type: "unsyncedPrivateMagicMetadataUpdate",
fileID,
privateMagicMetadata: {
...file.magicMetadata,
count: file.magicMetadata?.count ?? 0,
version: (file.magicMetadata?.version ?? 0) + 1,
data: { ...file.magicMetadata?.data, visibility },
},
});
} finally {
dispatch({ type: "removePendingVisibilityUpdate", fileID });
}
},
[dispatch],
);
/**
* Remove files from a collection
*/
const handleRemoveFilesFromCollection = useCallback(
async (collection: Collection) => {
showLoadingBar();
let notifyOthersFiles = false;
try {
const selectedFiles = getSelectedFiles(selected, filteredFiles);
const processedCount = await removeFromCollection(
collection,
selectedFiles,
);
notifyOthersFiles = processedCount !== selectedFiles.length;
clearSelection();
await onRemotePull();
} catch (e) {
onGenericError(e);
} finally {
hideLoadingBar();
}
if (notifyOthersFiles) {
showMiniDialog(notifyOthersFilesDialogAttributes());
}
},
[
showLoadingBar,
hideLoadingBar,
selected,
filteredFiles,
clearSelection,
onRemotePull,
onGenericError,
showMiniDialog,
],
);
/**
* Create a file operation handler
*/
const createFileOpHandler = useCallback(
(op: FileOp) => () => {
void (async () => {
showLoadingBar();
try {
const selectedFiles = getSelectedFiles(selected, filteredFiles);
const toProcessFiles = selectedFiles.filter(
(file) => file.ownerID === user.id,
);
if (toProcessFiles.length > 0) {
// TODO: Implement proper file operations with correct typing
// await performFileOp(op, toProcessFiles, ...callbacks);
console.log('File operation:', op, toProcessFiles.length, 'files');
}
if (toProcessFiles.length !== selectedFiles.length) {
showMiniDialog(notifyOthersFilesDialogAttributes());
}
clearSelection();
await onRemotePull();
} catch (e) {
onGenericError(e);
} finally {
hideLoadingBar();
}
})();
},
[
showLoadingBar,
hideLoadingBar,
selected,
filteredFiles,
user,
dispatch,
showMiniDialog,
clearSelection,
onRemotePull,
onGenericError,
],
);
return {
handleToggleFavorite,
handleFileVisibilityUpdate,
handleRemoveFilesFromCollection,
createFileOpHandler,
};
};

View File

@@ -1,132 +0,0 @@
import { useCallback, useRef, useState } from 'react';
import { useRouter } from 'next/router';
import { useGalleryReducer } from 'ente-new/photos/components/gallery/reducer';
import { PromiseQueue } from 'ente-utils/promise';
import { usePhotosAppContext } from 'ente-new/photos/types/context';
import { useBaseContext } from 'ente-base/context';
import {
savedCollections,
savedCollectionFiles,
savedTrashItems
} from 'ente-new/photos/services/photos-fdb';
import { pullFiles, prePullFiles, postPullFiles } from 'ente-new/photos/services/pull';
import { ensureLocalUser } from 'ente-accounts/services/user';
import { savedUserDetailsOrTriggerPull } from 'ente-new/photos/services/user-details';
import { masterKeyFromSession, clearSessionStorage } from 'ente-base/session';
import { isSessionInvalid } from 'ente-accounts/services/session';
import log from 'ente-base/log';
import exportService from 'ente-new/photos/services/export';
export interface RemotePullOpts {
silent?: boolean;
}
/**
* Custom hook for managing gallery data fetching and state
*/
export const useGalleryData = () => {
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
const { onGenericError } = useBaseContext();
const router = useRouter();
const [state, dispatch] = useGalleryReducer();
const [isFirstLoad, setIsFirstLoad] = useState(false);
// Queues for serializing remote operations
const remoteFilesPullQueue = useRef(new PromiseQueue<void>());
const remotePullQueue = useRef(new PromiseQueue<void>());
/**
* Pull latest collections, collection files and trash items from remote
*/
const remoteFilesPull = useCallback(
() =>
remoteFilesPullQueue.current.add(() =>
pullFiles({
onSetCollections: (collections) =>
dispatch({ type: "setCollections", collections }),
onSetCollectionFiles: (collectionFiles) =>
dispatch({
type: "setCollectionFiles",
collectionFiles,
}),
onSetTrashedItems: (trashItems) =>
dispatch({ type: "setTrashItems", trashItems }),
onDidUpdateCollectionFiles: () =>
exportService.onLocalFilesUpdated(),
}),
),
[],
);
/**
* Perform a full remote pull with error handling
*/
const remotePull = useCallback(
async (opts?: RemotePullOpts) =>
remotePullQueue.current.add(async () => {
const { silent } = opts ?? {};
// Pre-flight checks
if (!navigator.onLine) return;
if (await isSessionInvalid()) {
// Handle session expiry
return;
}
if (!(await masterKeyFromSession())) {
clearSessionStorage();
void router.push("/credentials");
return;
}
try {
if (!silent) showLoadingBar();
await prePullFiles();
await remoteFilesPull();
await postPullFiles();
} catch (e) {
log.error("Remote pull failed", e);
} finally {
dispatch({ type: "clearUnsyncedState" });
if (!silent) hideLoadingBar();
}
}),
[showLoadingBar, hideLoadingBar, router, remoteFilesPull],
);
/**
* Initialize the gallery on mount
*/
const initializeGallery = useCallback(async () => {
try {
dispatch({ type: "showAll" });
const user = ensureLocalUser();
const userDetails = await savedUserDetailsOrTriggerPull();
dispatch({
type: "mount",
user,
familyData: userDetails?.familyData,
collections: await savedCollections(),
collectionFiles: await savedCollectionFiles(),
trashItems: await savedTrashItems(),
});
await remotePull();
setIsFirstLoad(false);
} catch (error) {
onGenericError(error);
}
}, [dispatch, remotePull, onGenericError]);
return {
state,
dispatch,
isFirstLoad,
setIsFirstLoad,
remoteFilesPull,
remotePull,
initializeGallery,
};
};

View File

@@ -1,124 +0,0 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { haveMasterKeyInSession } from 'ente-base/session';
import { savedAuthToken } from 'ente-base/token';
import { stashRedirect } from 'ente-accounts/services/redirect';
import { validateKey } from 'ente-new/photos/components/gallery/helpers';
import {
getAndClearIsFirstLogin,
getAndClearJustSignedUp
} from 'ente-accounts/services/accounts-db';
import { shouldShowWhatsNew } from 'ente-new/photos/services/changelog';
import { initSettings } from 'ente-new/photos/services/settings';
import { useBaseContext } from 'ente-base/context';
interface UseGalleryInitializationProps {
onInitializeGallery: () => Promise<void>;
onShowPlanSelector: () => void;
onShowWhatsNew: () => void;
}
/**
* Custom hook for handling gallery initialization and authentication checks
*/
export const useGalleryInitialization = ({
onInitializeGallery,
onShowPlanSelector,
onShowWhatsNew,
}: UseGalleryInitializationProps) => {
const { logout } = useBaseContext();
const router = useRouter();
const [isFirstLoad, setIsFirstLoad] = useState(false);
const [blockingLoad, setBlockingLoad] = useState(false);
/**
* Preload all three variants of a responsive image
*/
const preloadImage = (imgBasePath: string) => {
const srcset: string[] = [];
for (let i = 1; i <= 3; i++) srcset.push(`${imgBasePath}/${i}x.png ${i}x`);
new Image().srcset = srcset.join(",");
};
/**
* Set up keyboard shortcuts for select all
*/
const setupSelectAllKeyBoardShortcutHandler = () => {
// This will be handled by the useSelection hook
// Keeping this function for compatibility
return () => {
// Cleanup function - currently handled by useSelection hook
};
};
useEffect(() => {
const electron = globalThis.electron;
let syncIntervalID: ReturnType<typeof setInterval> | undefined;
void (async () => {
// Check authentication
if (!haveMasterKeyInSession() || !(await savedAuthToken())) {
stashRedirect("/gallery");
void router.push("/");
return;
}
// Validate credentials
if (!(await validateKey())) {
logout();
return;
}
// One-time initialization
preloadImage("/images/subscription-card-background");
initSettings();
setupSelectAllKeyBoardShortcutHandler();
// Check if this is the user's first login
setIsFirstLoad(getAndClearIsFirstLogin());
// Show plan selector for new users
if (getAndClearJustSignedUp()) {
onShowPlanSelector();
}
// Initialize gallery data
await onInitializeGallery();
// Clear first load state
setIsFirstLoad(false);
// Start periodic sync
syncIntervalID = setInterval(
() => {
// This should trigger a silent remote pull
// Will be handled by the data management hook
},
5 * 60 * 1000 /* 5 minutes */,
);
// Handle electron-specific features
if (electron) {
electron.onMainWindowFocus(() => {
// Trigger silent remote pull on focus
});
if (await shouldShowWhatsNew(electron)) {
onShowWhatsNew();
}
}
})();
return () => {
clearInterval(syncIntervalID);
if (electron) {
electron.onMainWindowFocus(undefined);
}
};
}, [router, logout, onInitializeGallery, onShowPlanSelector, onShowWhatsNew]);
return {
isFirstLoad,
blockingLoad,
setBlockingLoad,
};
};

View File

@@ -1,168 +0,0 @@
import { useCallback, useRef } from 'react';
import { useRouter } from 'next/router';
import { PseudoCollectionID } from 'ente-new/photos/services/collection-summary';
import type { SearchOption } from 'ente-new/photos/services/search/types';
interface UseGalleryNavigationProps {
dispatch: (action: { type: string; [key: string]: unknown }) => void;
barMode: string;
activeCollectionID: number | undefined;
}
/**
* Custom hook for managing gallery navigation and view changes
*/
export const useGalleryNavigation = ({
dispatch,
barMode,
activeCollectionID,
}: UseGalleryNavigationProps) => {
const router = useRouter();
/**
* Grace period tracking for hidden section authentication
*/
const lastAuthenticationForHiddenTimestamp = useRef<number>(0);
/**
* Handle collection summary selection with optional authentication
*/
const showCollectionSummary = useCallback(
async (
collectionSummaryID: number | undefined,
isHiddenCollectionSummary: boolean | undefined,
authenticateUser?: () => Promise<void>,
) => {
const lastAuthAt = lastAuthenticationForHiddenTimestamp.current;
if (
isHiddenCollectionSummary &&
barMode !== "hidden-albums" &&
Date.now() - lastAuthAt > 5 * 60 * 1e3 /* 5 minutes */ &&
authenticateUser
) {
await authenticateUser();
lastAuthenticationForHiddenTimestamp.current = Date.now();
}
// Trigger a pull of the latest data when opening trash
if (collectionSummaryID === PseudoCollectionID.trash) {
// This should trigger a remote files pull
// Will be handled by the calling component
}
dispatch({ type: "showCollectionSummary", collectionSummaryID });
},
[dispatch, barMode],
);
/**
* Handle search option selection
*/
const handleSelectSearchOption = useCallback(
(searchOption: SearchOption | undefined) => {
if (searchOption) {
const type = searchOption.suggestion.type;
if (type === "collection") {
dispatch({
type: "showCollectionSummary",
collectionSummaryID: searchOption.suggestion.collectionID,
});
} else if (type === "person") {
dispatch({
type: "showPerson",
personID: searchOption.suggestion.person.id,
});
} else {
dispatch({
type: "enterSearchMode",
searchSuggestion: searchOption.suggestion,
});
}
} else {
dispatch({ type: "exitSearch" });
}
},
[dispatch],
);
/**
* Handle gallery bar mode changes
*/
const handleChangeBarMode = useCallback(
(mode: string) => {
if (mode === "people") {
dispatch({ type: "showPeople" });
} else {
dispatch({ type: "showAlbums" });
}
},
[dispatch],
);
/**
* Handle collection selection
*/
const handleSelectCollection = useCallback(
(collectionID: number) =>
dispatch({
type: "showCollectionSummary",
collectionSummaryID: collectionID,
}),
[dispatch],
);
/**
* Handle person selection
*/
const handleSelectPerson = useCallback(
(personID: string) => dispatch({ type: "showPerson", personID }),
[dispatch],
);
/**
* Handle entering search mode
*/
const handleEnterSearchMode = useCallback(
() => dispatch({ type: "enterSearchMode" }),
[dispatch],
);
/**
* Handle showing people view
*/
const handleShowPeople = useCallback(
() => dispatch({ type: "showPeople" }),
[dispatch],
);
/**
* Update browser URL based on active collection
*/
const updateBrowserURL = useCallback(() => {
if (typeof activeCollectionID === "undefined" || !router.isReady) {
return;
}
let collectionURL = "";
if (activeCollectionID !== PseudoCollectionID.all) {
collectionURL = `?collection=${activeCollectionID}`;
}
const href = `/gallery${collectionURL}`;
void router.push(href, undefined, { shallow: true });
}, [activeCollectionID, router]);
return {
// Navigation handlers
showCollectionSummary,
handleSelectSearchOption,
handleChangeBarMode,
handleSelectCollection,
handleSelectPerson,
handleEnterSearchMode,
handleShowPeople,
updateBrowserURL,
// State
lastAuthenticationForHiddenTimestamp,
};
};

View File

@@ -1,121 +0,0 @@
import { useCallback, useRef, useState } from 'react';
import type { EnteFile } from 'ente-media/file';
import { useModalVisibility } from 'ente-base/components/utils/modal';
import type { UploadTypeSelectorIntent } from 'ente-gallery/components/Upload';
/**
* Custom hook for managing all modal states and visibility
*/
export const useModalManagement = () => {
// File drag and drop state
const [dragAndDropFiles, setDragAndDropFiles] = useState<File[]>([]);
const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false);
// Upload modal state
const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false);
const [uploadTypeSelectorIntent, setUploadTypeSelectorIntent] =
useState<UploadTypeSelectorIntent>("upload");
// File viewer state
const [isFileViewerOpen, setIsFileViewerOpen] = useState(false);
// Fix creation time dialog state
const [fixCreationTimeFiles, setFixCreationTimeFiles] = useState<EnteFile[]>([]);
// Authentication callback for hidden section access
const onAuthenticateCallback = useRef<(() => void) | undefined>(undefined);
// Modal visibility hooks
const { show: showSidebar, props: sidebarVisibilityProps } = useModalVisibility();
const { show: showPlanSelector, props: planSelectorVisibilityProps } = useModalVisibility();
const { show: showWhatsNew, props: whatsNewVisibilityProps } = useModalVisibility();
const { show: showFixCreationTime, props: fixCreationTimeVisibilityProps } = useModalVisibility();
const { show: showExport, props: exportVisibilityProps } = useModalVisibility();
const { show: showAuthenticateUser, props: authenticateUserVisibilityProps } = useModalVisibility();
const { show: showAlbumNameInput, props: albumNameInputVisibilityProps } = useModalVisibility();
/**
* Authenticate user for hidden section access
*/
const authenticateUser = useCallback(
() =>
new Promise<void>((resolve) => {
onAuthenticateCallback.current = resolve;
showAuthenticateUser();
}),
[showAuthenticateUser],
);
/**
* Open upload type selector
*/
const openUploader = useCallback((intent?: UploadTypeSelectorIntent) => {
setUploadTypeSelectorView(true);
setUploadTypeSelectorIntent(intent ?? "upload");
}, []);
/**
* Close upload type selector
*/
const closeUploadTypeSelector = useCallback(() => {
setUploadTypeSelectorView(false);
}, []);
/**
* Check if any modal is currently open
*/
const isAnyModalOpen =
uploadTypeSelectorView ||
sidebarVisibilityProps.open ||
planSelectorVisibilityProps.open ||
fixCreationTimeVisibilityProps.open ||
exportVisibilityProps.open ||
authenticateUserVisibilityProps.open ||
albumNameInputVisibilityProps.open ||
isFileViewerOpen;
return {
// State
dragAndDropFiles,
shouldDisableDropzone,
uploadTypeSelectorView,
uploadTypeSelectorIntent,
isFileViewerOpen,
fixCreationTimeFiles,
isAnyModalOpen,
// Setters
setDragAndDropFiles,
setShouldDisableDropzone,
setUploadTypeSelectorView,
setUploadTypeSelectorIntent,
setIsFileViewerOpen,
setFixCreationTimeFiles,
// Modal visibility props
sidebarVisibilityProps,
planSelectorVisibilityProps,
whatsNewVisibilityProps,
fixCreationTimeVisibilityProps,
exportVisibilityProps,
authenticateUserVisibilityProps,
albumNameInputVisibilityProps,
// Modal show functions
showSidebar,
showPlanSelector,
showWhatsNew,
showFixCreationTime,
showExport,
showAuthenticateUser,
showAlbumNameInput,
// Actions
authenticateUser,
openUploader,
closeUploadTypeSelector,
// Authentication callback
onAuthenticateCallback,
};
};

View File

@@ -1,123 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { SelectedState } from 'utils/file';
import type { EnteFile } from 'ente-media/file';
import { PseudoCollectionID } from 'ente-new/photos/services/collection-summary';
interface UseSelectionProps {
user: { id: number } | null;
filteredFiles: EnteFile[];
activeCollectionID: number | undefined;
barMode: string;
activePersonID?: string;
isAnyModalOpen: boolean;
}
/**
* Custom hook for managing file selection state and keyboard shortcuts
*/
export const useSelection = ({
user,
filteredFiles,
activeCollectionID,
barMode,
activePersonID,
isAnyModalOpen,
}: UseSelectionProps) => {
const [selected, setSelected] = useState<SelectedState>({
ownCount: 0,
count: 0,
collectionID: 0,
context: { mode: "albums", collectionID: PseudoCollectionID.all },
});
const selectAll = useCallback((e: KeyboardEvent) => {
// Don't intercept Ctrl/Cmd + a if the user is typing in a text field
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement
) {
return;
}
e.preventDefault();
// Don't select all if conditions aren't met
if (
!user ||
!filteredFiles.length ||
isAnyModalOpen
) {
return;
}
// Create a selection with everything based on the current context
const newSelected = {
ownCount: 0,
count: 0,
collectionID: activeCollectionID,
context:
barMode === "people" && activePersonID
? { mode: "people" as const, personID: activePersonID }
: {
mode: barMode as "albums" | "hidden-albums",
collectionID: activeCollectionID!,
},
};
filteredFiles.forEach((item) => {
if (item.ownerID === user.id) {
newSelected.ownCount++;
}
newSelected.count++;
// @ts-expect-error Selection code needs type fixing
newSelected[item.id] = true;
});
setSelected(newSelected);
}, [user, filteredFiles, activeCollectionID, barMode, activePersonID, isAnyModalOpen]);
const clearSelection = useCallback(() => {
if (!selected.count) {
return;
}
setSelected({
ownCount: 0,
count: 0,
collectionID: 0,
context: undefined,
});
}, [selected.count]);
const keyboardShortcutHandlerRef = useRef({ selectAll, clearSelection });
useEffect(() => {
keyboardShortcutHandlerRef.current = { selectAll, clearSelection };
}, [selectAll, clearSelection]);
// Set up keyboard shortcuts
useEffect(() => {
const handleKeyUp = (e: KeyboardEvent) => {
switch (e.key) {
case "Escape":
keyboardShortcutHandlerRef.current.clearSelection();
break;
case "a":
if (e.ctrlKey || e.metaKey) {
keyboardShortcutHandlerRef.current.selectAll(e);
}
break;
}
};
document.addEventListener("keydown", handleKeyUp);
return () => {
document.removeEventListener("keydown", handleKeyUp);
};
}, []);
return {
selected,
setSelected,
clearSelection,
selectAll,
};
};

View File

@@ -705,56 +705,54 @@ const splitMetadataAndMediaItems = (
* single live photo when appropriate.
*/
const clusterLivePhotos = async (
items: UploadItemWithCollectionIDAndName[],
_items: UploadItemWithCollectionIDAndName[],
parsedMetadataJSONMap: Map<string, ParsedMetadataJSON>,
) => {
const result: ClusteredUploadItem[] = [];
type ItemAsset = PotentialLivePhotoAsset & {
localID: number;
isLivePhoto?: boolean;
};
const items: ItemAsset[] = _items.map((item) => ({
localID: item.localID,
isLivePhoto: item.isLivePhoto,
fileName: item.fileName,
fileType: potentialFileTypeFromExtension(item.fileName) ?? -1,
collectionID: item.collectionID,
uploadItem: item.uploadItem!,
pathPrefix: item.pathPrefix,
}));
items
.sort((f, g) =>
nameAndExtension(f.fileName)[0].localeCompare(
.sort((f, g) => {
const cmp = nameAndExtension(f.fileName)[0].localeCompare(
nameAndExtension(g.fileName)[0],
),
)
);
return cmp == 0 ? f.fileType - g.fileType : cmp;
})
.sort((f, g) => f.collectionID - g.collectionID);
let index = 0;
while (index < items.length - 1) {
const f = items[index]!;
const g = items[index + 1]!;
const fFileType = potentialFileTypeFromExtension(f.fileName)!;
const gFileType = potentialFileTypeFromExtension(g.fileName)!;
const fa: PotentialLivePhotoAsset = {
fileName: f.fileName,
fileType: fFileType,
collectionID: f.collectionID,
uploadItem: f.uploadItem!,
pathPrefix: f.pathPrefix,
};
const ga: PotentialLivePhotoAsset = {
fileName: g.fileName,
fileType: gFileType,
collectionID: g.collectionID,
uploadItem: g.uploadItem!,
pathPrefix: g.pathPrefix,
};
const fa = items[index]!;
const ga = items[index + 1]!;
if (await areLivePhotoAssets(fa, ga, parsedMetadataJSONMap)) {
const [image, video] =
fFileType == FileType.image ? [f, g] : [g, f];
fa.fileType == FileType.image ? [fa, ga] : [ga, fa];
result.push({
localID: f.localID,
collectionID: f.collectionID,
localID: fa.localID,
collectionID: fa.collectionID,
fileName: image.fileName,
isLivePhoto: true,
pathPrefix: image.pathPrefix,
livePhotoAssets: {
image: image.uploadItem!,
video: video.uploadItem!,
image: image.uploadItem,
video: video.uploadItem,
},
});
index += 2;
} else {
// They may already be a live photo (we might be retrying a
// previously failed upload).
result.push({ ...f, isLivePhoto: f.isLivePhoto ?? false });
result.push({ ...fa, isLivePhoto: fa.isLivePhoto ?? false });
index += 1;
}
}

View File

@@ -327,7 +327,7 @@ interface EncryptedFilePieces {
export interface PotentialLivePhotoAsset {
fileName: string;
fileType: number /* FileType */;
fileType: number /* FileType | -1 */;
collectionID: number;
uploadItem: UploadItem;
pathPrefix: UploadPathPrefix | undefined;

View File

@@ -1227,7 +1227,7 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
"@types/react-dom@^19.1.6":
"@types/react-dom@^19.1.1", "@types/react-dom@^19.1.6":
version "19.1.6"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.6.tgz#4af629da0e9f9c0f506fc4d1caa610399c595d64"
integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==
@@ -1244,7 +1244,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^19.1.8":
"@types/react@*", "@types/react@^19.1.0", "@types/react@^19.1.8":
version "19.1.8"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.8.tgz#ff8395f2afb764597265ced15f8dddb0720ae1c3"
integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==