Compare commits
282 Commits
fdroid-v0.
...
streaming-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0982e01d7 | ||
|
|
dd52ee7763 | ||
|
|
387e4ae826 | ||
|
|
098ff4e664 | ||
|
|
00a04f18e4 | ||
|
|
b8304f0ec5 | ||
|
|
979fa5e6da | ||
|
|
91f356ceda | ||
|
|
a019aaf5fc | ||
|
|
a9df48ea5d | ||
|
|
be6ce6d639 | ||
|
|
9d421e79a7 | ||
|
|
d1d8144fd1 | ||
|
|
7302f1d4ab | ||
|
|
39788341db | ||
|
|
6c86fe0d53 | ||
|
|
30ed06cfda | ||
|
|
21788c28cf | ||
|
|
2969b5c9a5 | ||
|
|
aa74948f4a | ||
|
|
1bdbfe0580 | ||
|
|
cacf4212c7 | ||
|
|
8f540f23dc | ||
|
|
17d76e50da | ||
|
|
db26923d68 | ||
|
|
4670be9bba | ||
|
|
f8c2f4b9dd | ||
|
|
396065e80c | ||
|
|
5a755d851a | ||
|
|
286a968f65 | ||
|
|
efff97bc71 | ||
|
|
2a73de848c | ||
|
|
c7c8fd65b6 | ||
|
|
8d7eef99ad | ||
|
|
1605b44c6e | ||
|
|
578a92d4bc | ||
|
|
bf3ed6f478 | ||
|
|
92a9698df5 | ||
|
|
342ac3258a | ||
|
|
e4427d7605 | ||
|
|
6f729c01e1 | ||
|
|
0d7c319903 | ||
|
|
6d552f5190 | ||
|
|
06450a0ce0 | ||
|
|
72d6789739 | ||
|
|
3d2d0cc345 | ||
|
|
884246d2ab | ||
|
|
cf25cc40e4 | ||
|
|
7138510e48 | ||
|
|
15e7e0ae9d | ||
|
|
9dcced260f | ||
|
|
2d5dc734aa | ||
|
|
1d93d44180 | ||
|
|
0aeb9f0c82 | ||
|
|
183bbdd145 | ||
|
|
8d701d4fd5 | ||
|
|
c6f6041d24 | ||
|
|
f49ece10e6 | ||
|
|
d0f206741f | ||
|
|
87ff5c5c0b | ||
|
|
b931dac18b | ||
|
|
2b52616ba5 | ||
|
|
e66ee5bcb1 | ||
|
|
f18bcc71d3 | ||
|
|
43a7cb1223 | ||
|
|
ceb25651f2 | ||
|
|
8a8934eacd | ||
|
|
20fea517ce | ||
|
|
0d32bd55dd | ||
|
|
20bbdb131d | ||
|
|
1980cb035e | ||
|
|
bd00c27dc6 | ||
|
|
e8fa86e2ad | ||
|
|
baa72202b2 | ||
|
|
46658a26f3 | ||
|
|
6653b36764 | ||
|
|
c17d0d0087 | ||
|
|
b823a8d6a1 | ||
|
|
e06b20a566 | ||
|
|
8218bfba04 | ||
|
|
8df5831944 | ||
|
|
6e774d6758 | ||
|
|
981c74d3f1 | ||
|
|
18ee3b19f7 | ||
|
|
aa27191ddc | ||
|
|
0883fe1d05 | ||
|
|
17e59de59c | ||
|
|
bdb30d64f0 | ||
|
|
57881f34c3 | ||
|
|
6ef3c01030 | ||
|
|
d4ddc0f919 | ||
|
|
4736ec7e0a | ||
|
|
0840c66a34 | ||
|
|
eb2f6aec68 | ||
|
|
45074f85d9 | ||
|
|
c46c27d21d | ||
|
|
3ff8d04d7b | ||
|
|
437eb246b0 | ||
|
|
5e383f3844 | ||
|
|
9bce8dc878 | ||
|
|
a447d615e0 | ||
|
|
239e6a3158 | ||
|
|
5a72d62555 | ||
|
|
7aa8f6f00f | ||
|
|
5b168021f4 | ||
|
|
a407b1baad | ||
|
|
3589cc5bbf | ||
|
|
0cef0656f3 | ||
|
|
3b3ba721a2 | ||
|
|
d899be6eac | ||
|
|
17c713d3de | ||
|
|
0e9153f4ab | ||
|
|
5484a95bf4 | ||
|
|
2a1c1a30e9 | ||
|
|
f902b7e75c | ||
|
|
ac9f4e3181 | ||
|
|
b68b1a97b5 | ||
|
|
b8de2bf736 | ||
|
|
d35975b26e | ||
|
|
c2ca87d3af | ||
|
|
a41c359ae4 | ||
|
|
e00cdee92b | ||
|
|
a4ade14794 | ||
|
|
b1ce7b6edb | ||
|
|
a5efee1ae3 | ||
|
|
33b56a2257 | ||
|
|
9abdfd2555 | ||
|
|
81ead3e4ce | ||
|
|
95d218b3a1 | ||
|
|
74db8767a2 | ||
|
|
1200dbb6a9 | ||
|
|
8a90eba39a | ||
|
|
562ead3202 | ||
|
|
2c92411596 | ||
|
|
2fd5c703c9 | ||
|
|
15d58e3446 | ||
|
|
677a473d7d | ||
|
|
fb0128369a | ||
|
|
bc4aa85eb1 | ||
|
|
b1d2de712b | ||
|
|
526b5da40f | ||
|
|
1c6efd4985 | ||
|
|
defd88050d | ||
|
|
9e12f35650 | ||
|
|
a7f31119fe | ||
|
|
40959cae09 | ||
|
|
f32874fb05 | ||
|
|
69f9bf35ac | ||
|
|
8204ac3070 | ||
|
|
115c2c7fb3 | ||
|
|
60b7ed52b8 | ||
|
|
e6f72ea1c3 | ||
|
|
967d8c0f3b | ||
|
|
b729b8f0ea | ||
|
|
e3323890df | ||
|
|
fe4b0ded71 | ||
|
|
566364191d | ||
|
|
c1dccf438b | ||
|
|
71e419ac20 | ||
|
|
de8fb95477 | ||
|
|
84c00d0d31 | ||
|
|
d9ed2b4c10 | ||
|
|
db308fa199 | ||
|
|
244599ba67 | ||
|
|
ee8ce50649 | ||
|
|
5f5632aac5 | ||
|
|
5f736aaa10 | ||
|
|
f003b4f8ac | ||
|
|
b933a89336 | ||
|
|
016a476895 | ||
|
|
94c4e1ff0d | ||
|
|
d7ee9615b7 | ||
|
|
bf89a0ca9e | ||
|
|
9f1b4fc23c | ||
|
|
f3feb4cdda | ||
|
|
0b7b4b72f3 | ||
|
|
0380a30705 | ||
|
|
ff72dae408 | ||
|
|
be7cbc2ba0 | ||
|
|
1b0d481b45 | ||
|
|
4ee6ef408e | ||
|
|
9eb887e511 | ||
|
|
5dd5f7e9c5 | ||
|
|
cd7183a9ad | ||
|
|
17b213e380 | ||
|
|
79712182af | ||
|
|
97362ddbf2 | ||
|
|
7621041ce0 | ||
|
|
d58c2a3d49 | ||
|
|
2d4e532186 | ||
|
|
3484f81546 | ||
|
|
54da181256 | ||
|
|
021b6ec9b4 | ||
|
|
b4e01d5ab5 | ||
|
|
78334d2e22 | ||
|
|
9496baaffc | ||
|
|
1feb43d3d4 | ||
|
|
5d6cc892f3 | ||
|
|
27070bbe4c | ||
|
|
556a0d1814 | ||
|
|
5c1d7a0315 | ||
|
|
5b1130ab24 | ||
|
|
96937041f1 | ||
|
|
6f0deba3ed | ||
|
|
ca31a422fa | ||
|
|
8c68af7772 | ||
|
|
42ac508fe7 | ||
|
|
2e52efb15f | ||
|
|
825a9df9fa | ||
|
|
f30e05389b | ||
|
|
e952aa80a5 | ||
|
|
f3d9595953 | ||
|
|
a57232c34b | ||
|
|
5c16ce3459 | ||
|
|
44c64c06a7 | ||
|
|
3bbfa71824 | ||
|
|
970da9f29c | ||
|
|
97bdc9362a | ||
|
|
4881f08790 | ||
|
|
d322f5e1bc | ||
|
|
b87b68e9d4 | ||
|
|
782688c1f7 | ||
|
|
38a35696a3 | ||
|
|
cea9fa84a1 | ||
|
|
bf4807da5b | ||
|
|
dc3f074588 | ||
|
|
282ecf763b | ||
|
|
218c652ed1 | ||
|
|
5b17711b55 | ||
|
|
b3d8e2e865 | ||
|
|
85bf3eebcb | ||
|
|
ceb3046a02 | ||
|
|
d3ebb3a50c | ||
|
|
4a9bc84375 | ||
|
|
2282db7800 | ||
|
|
5abd8b8f04 | ||
|
|
45f1549079 | ||
|
|
01aa679698 | ||
|
|
8da160b834 | ||
|
|
2947ca2e3c | ||
|
|
54d63c9969 | ||
|
|
77be0a18d4 | ||
|
|
627e170304 | ||
|
|
59e26779b9 | ||
|
|
dda46c0639 | ||
|
|
d0e9972547 | ||
|
|
1358087ee7 | ||
|
|
5a0d2ba922 | ||
|
|
d607d8a851 | ||
|
|
8d7950afea | ||
|
|
10ee5989f2 | ||
|
|
7509abd1a9 | ||
|
|
b0966e0cca | ||
|
|
d99d08e8ae | ||
|
|
0a19b8259a | ||
|
|
caf601b49b | ||
|
|
b453ffef85 | ||
|
|
12c472ef01 | ||
|
|
726c6dc8e6 | ||
|
|
5d0a15e9e5 | ||
|
|
8559dd8364 | ||
|
|
b6b724f64f | ||
|
|
c20dcdae76 | ||
|
|
5522121cf6 | ||
|
|
0a9e706b50 | ||
|
|
602881ee26 | ||
|
|
10079d4cb0 | ||
|
|
d363f37592 | ||
|
|
8922d7e663 | ||
|
|
d6a626fe0d | ||
|
|
f11803fd1f | ||
|
|
e3833044e9 | ||
|
|
edfd86628a | ||
|
|
99e5bc5050 | ||
|
|
252ae8169d | ||
|
|
3478720cb3 | ||
|
|
fea6d58bd4 | ||
|
|
e54027c5dd | ||
|
|
36c06d5501 | ||
|
|
701b7b8f37 | ||
|
|
1396ca57db | ||
|
|
5a639a9c60 |
9
.github/workflows/auth-internal-release.yml
vendored
9
.github/workflows/auth-internal-release.yml
vendored
@@ -54,3 +54,12 @@ jobs:
|
||||
packageName: io.ente.auth
|
||||
releaseFiles: auth/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
|
||||
track: internal
|
||||
|
||||
- name: Notify Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }}
|
||||
nodetail: true
|
||||
title: "🏆 Internal release available for Auth"
|
||||
description: "[Download](https://play.google.com/store/apps/details?id=io.ente.auth)"
|
||||
color: 0x800080
|
||||
|
||||
11
.github/workflows/mobile-internal-release.yml
vendored
11
.github/workflows/mobile-internal-release.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
- name: Build PlayStore AAB
|
||||
run: |
|
||||
flutter build appbundle --release --flavor playstore
|
||||
flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore
|
||||
env:
|
||||
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }}
|
||||
@@ -54,3 +54,12 @@ jobs:
|
||||
packageName: io.ente.photos
|
||||
releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
|
||||
track: internal
|
||||
|
||||
- name: Notify Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }}
|
||||
nodetail: true
|
||||
title: "🏆 Internal release available for Photos"
|
||||
description: "[Download](https://play.google.com/store/apps/details?id=io.ente.photos)"
|
||||
color: 0x00ff00
|
||||
|
||||
2
.github/workflows/mobile-release.yml
vendored
2
.github/workflows/mobile-release.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
- name: Build independent APK
|
||||
run: |
|
||||
flutter build apk --release --flavor independent
|
||||
flutter build apk --dart-define=cronetHttpNoPlay=true --release --flavor independent
|
||||
mv build/app/outputs/flutter-apk/app-independent-release.apk build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk
|
||||
env:
|
||||
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
|
||||
|
||||
25
.github/workflows/server-publish.yml
vendored
25
.github/workflows/server-publish.yml
vendored
@@ -1,27 +1,24 @@
|
||||
name: "Publish ghcr (server)"
|
||||
|
||||
on:
|
||||
# Run manually, providing it the commit.
|
||||
#
|
||||
# To obtain the commit from the currently deployed museum, do:
|
||||
# curl -s https://api.ente.io/ping | jq -r '.id'
|
||||
#
|
||||
# See server/docs/publish.md for more details.
|
||||
# Run automatically on 15th of every month, at 05:00 UTC.
|
||||
schedule:
|
||||
- cron: '0 5 15 * *'
|
||||
# Run manually if needed to publish out of schedule.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit:
|
||||
description: "Commit to publish the image from"
|
||||
type: string
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Determine commit from prod museum
|
||||
run: |
|
||||
echo "museum_commit=$(curl -s https://api.ente.io/ping | jq -r .id)" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.commit }}
|
||||
ref: ${{ env.museum_commit }}
|
||||
|
||||
- name: Build and push
|
||||
uses: mr-smithers-excellent/docker-build-push@v6
|
||||
@@ -34,8 +31,8 @@ jobs:
|
||||
enableBuildKit: true
|
||||
multiPlatform: true
|
||||
platform: linux/amd64,linux/arm64
|
||||
buildArgs: GIT_COMMIT=${{ inputs.commit }}
|
||||
tags: ${{ inputs.commit }}, latest
|
||||
buildArgs: GIT_COMMIT=${{ env.museum_commit }}
|
||||
tags: ${{ env.museum_commit }}, latest
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -35,9 +35,18 @@
|
||||
{
|
||||
"title": "Amazon"
|
||||
},
|
||||
{
|
||||
"title": "Ankama",
|
||||
"slug": "ankama"
|
||||
},
|
||||
{
|
||||
"title": "Anycoin Direct",
|
||||
"slug": "anycoindirect"
|
||||
},
|
||||
{
|
||||
"title": "Aruba",
|
||||
"slug": "aruba",
|
||||
"hex": "ef8a33"
|
||||
},
|
||||
{
|
||||
"title": "AscendEX"
|
||||
@@ -352,6 +361,14 @@
|
||||
{
|
||||
"title": "Estateguru"
|
||||
},
|
||||
{
|
||||
"title": "EVEOnline",
|
||||
"slug": "eve_online",
|
||||
"altNames": [
|
||||
"EVE Online"
|
||||
],
|
||||
"hex": "858585"
|
||||
},
|
||||
{
|
||||
"title": "Fastmail"
|
||||
},
|
||||
@@ -760,6 +777,11 @@
|
||||
"altNames": [
|
||||
"欧易"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "OnShape",
|
||||
"slug": "onshape",
|
||||
"hex": "7abb5e"
|
||||
},
|
||||
{
|
||||
"title": "Parqet",
|
||||
@@ -857,6 +879,11 @@
|
||||
{
|
||||
"title": "RealMe",
|
||||
"slug": "realme"
|
||||
},
|
||||
{
|
||||
"title": "RealVNC",
|
||||
"slug": "realvnc",
|
||||
"hex": "488aec"
|
||||
},
|
||||
{
|
||||
"title": "Registro br",
|
||||
@@ -901,6 +928,10 @@
|
||||
{
|
||||
"title": "Samsung"
|
||||
},
|
||||
{
|
||||
"title": "Seafile",
|
||||
"slug": "seafile"
|
||||
},
|
||||
{
|
||||
"title": "Sendgrid"
|
||||
},
|
||||
|
||||
5
auth/assets/custom-icons/icons/ankama.svg
Normal file
5
auth/assets/custom-icons/icons/ankama.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 720" width="720" height="720">
|
||||
<title>ankama</title>
|
||||
<path class="s0" d="m572.3 253.3c-0.3-1.4-0.5-3-1.1-4.4-15.1-46-59.7-76.2-107.9-71.5-31.2 3-55.6 18.9-73.4 44.7-3.8 5.7-7.1 12-10.1 18.4-9.4 19.5-19.5 38.7-24.7 59.7-0.5 1.6-1.1 3.5-1.9 4.9-2.5 4.6-5.5 6.3-10.1 4.1-11.7-5.7-23.8-5.2-36.1-4.4-20.3 1.1-37.9-5.7-52.9-19.5-36.1-33.1-35.3-91 1.9-123 26.8-23.3 43.3-52.1 49.3-86.8 3-16.5 3.3-33.4 0-49.9-0.5-2.7-0.8-5.5-1.4-8.7 7.4-1.4 14.3 0 20.8 2.2 42.8 12.4 69.3 40.6 81.6 82.7 0.5 2.2 1.1 4.4 1.6 6.8 0.8 3 2.7 4.9 5.7 5.5 3.3 0.5 6.3-0.5 7.9-3.5 3-6.3 5.7-12.7 8.2-19.2 3.3-9.7 3.8-10.5 14.3-10.6 58.3-0.8 111.7 15.4 159.4 49 60.5 42.9 98.4 101.4 115.7 173.4 0.8 3.5 0.8 7.6 0.5 11.6-4.6 68-26.8 129.6-68.8 183.5-49 63.1-112.4 104.4-190.7 121.4-72.6 15.8-141.7 4.9-207.1-30.1-1.4-0.8-2.7-1.6-3.8-2.5-0.3 0-0.5-0.8-1.6-2.2 8.7 3 17 5.5 24.7 8.6 13.9 5.7 28.5 8.7 43.6 8.6 14.3 0 28.2 0.8 42.5 0 63.9-3.8 119.5-27.4 166.2-70.7 23.6-21.9 32.8-50.7 30.7-82.4-1.9-32.3-16.5-58.6-42.2-78.3-18.9-14.6-32-33.1-38-56.4-4.1-15.8-4.6-32-0.8-48.2 5.7-24.7 27.7-41.7 52.3-40.6 17 0.8 29.6 9 39.5 22.5 1.4 1.9 2.7 4.1 4.1 6.3 0.5 0 0.8-0.5 1.4-0.8l0.8 0.3v-0.5zm-263.5-55.1c-14.7 0-26.3 12.4-26.3 27.7 0 15.3 11.6 27.4 26.6 27.4 15 0 26-12.4 26-27.1 0-14.7-12-27.9-26.3-27.9v-0.2z"/>
|
||||
<path class="s0" d="m168.2 314.5c7.4 1.6 14.7 3.3 22.5 4.9 10.9 2.2 14.3 6.8 12 18.1-1.6 8.2-3 16.2-4.9 24.1-0.8 3.5 0 5.7 2.5 8.2 22.8 23.6 50.1 38.4 83.8 43.6-2.2-1.4-3-2.2-4.1-2.7-19.5-10.1-27.7-25.2-25.2-46.6 1.1-9.8 0-19.2-6-27.4-3.8-5.5-9-9.7-13.9-14.3-1.6-1.6-3.8-2.5-6-4.1 5.2-3.3 10.5-3.3 15.4-3 7.4 0.3 14.7 1.4 22.2 3 11.6 2.5 21.7 8.2 30.4 16.2 6.5 6 12.4 12.4 18.7 18.4 13.9 13.6 25.2 12.8 37.2-2.7 7.6-9.8 12.4-21.4 15.8-33.1 3.8-12.7 8.2-24.9 15.4-36.1 7.4-11.6 16.5-21.7 27.4-30.1 9.7-7.4 19.7-6.8 30.7-3.3v9.7c-1.1 27.1 5.2 52.6 19.5 75.9 1.1 1.9 2.2 4.1 3.5 6 4.1 6.3 3.3 12-0.5 18.1-5.5 8.7-13.5 13.2-23.8 13.5h-6.3c-12.7 0-23.3 8.2-26.8 20.6-3 10.9 1.9 23.3 11.7 29.6 10.9 6.8 24.1 5.7 33.4-3 10.1-9.8 16.5-21.7 18.9-35.7 0.3-1.9 0.8-4.1 1.1-6 17.3-0.3 39.8 16.6 48.2 37.2 10.1 24.1 6.3 46.6-10.1 68.8-0.8-5.2-1.4-9.4-2.2-12.8-2.2-9.7-8.6-15.7-18.1-17.7-6.8-1.6-9.8 0-13.6 5.7-1.9 3-3.5 6.3-5.2 9.7-2.5 4.9-4.6 9.8-7.1 14.6-13.6 27.1-34.9 45.8-63.2 55.6-27.1 9.7-55.2 13.2-83.5 10.1-16.5-1.6-29.3-10.5-39.8-23.6 1.9-1.4 3.3-2.7 4.9-3.8 7.4-5.5 12.8-12.7 16.6-20.8 1.1-2.2 1.6-4.9 1.9-7.4 0.5-5.2-2.7-9.4-7.6-9.8-4.6-0.5-8.7 2.5-10.1 7.6-0.8 3.3-1.1 6.5-2.5 9.7-3.8 7.1-9.7 12.8-16.5 17-7.4 4.4-14.6 3.8-20-1.4-5.5-5.5-6-11.7-1.9-19.7 0.3-0.5 0.5-1.4 1.4-3-2.5 1.1-4.1 1.4-5.5 2.2-17.7 10.6-25.8 31.5-19.2 51.5 18.7 56.7-6 119.5-54 151-3.3 2.2-7.1 4.1-11.6 6.8 0-3-0.5-5.2-0.5-7.4-2.5-57.8-30.1-98.9-82.4-122.8-32.8-14.7-53.7-39.5-62.1-74-12.4-50.1 13.9-102.2 61.2-122.8 1.9-0.8 4.1-1.4 7.6-2.7-15.1 28.8-18.7 57.2-9.4 86.8 5.5 17.3 14.7 32 29 45.5 0.3-12.4-0.8-23.6 7.6-33.4 2.7 6.3 5.5 12 8.2 17.7 4.9 10.9 12.7 19.5 23.3 24.9 12 6.3 24.1 6.8 36.5 1.1 8.6-3.8 15.8-9.4 22.2-16.2 8.6-9 8.6-24.9 0.3-35.7-5.5-7.1-8.6-14.7-7.4-23.6 1.9-15.4 15.1-26.8 32-28.2 4.6-0.3 9.4 0 13.6 0.5 4.1 0.8 8.2 2.5 12.7 3.3-3.5-3.8-7.9-6.5-12.7-8.2-8.2-2.7-16.6-3.3-25.2-1.6-14.7 3.3-24.9 16.2-26.3 33.4-0.5 6.3 0 12.8 1.1 19.2 3 16.5-0.8 30.4-13.2 42.8-1.9-1.6-3.8-2.7-5.7-4.4-43.9-37.6-69.6-84.6-75.6-142.2-10.1-95.9 42.5-184.9 130.9-223 28.5-12.4 46-33.8 53.4-63.9 0.3-1.4 0.8-2.7 1.9-3.8 1.9 10.1 1.4 20.3-1.1 30.1-6 24.1-19.5 42.8-41.4 55.3-21.4 12.4-38.4 29-51.5 50.1-2.5 4.1-2.7 7.1 0 10.9 2.7 3.5 5.2 7.6 7.6 11.6 4.4 7.4 3 13.9-4.1 18.9-6.3 4.4-13.2 8.2-19.5 12.7-1.6 1.1-3.5 3.3-3.8 5.2-1.6 18.7-0.5 36.9 5.5 55.3l1.4-0.3-0.2-0.2zm192.8 132c14.3 0 25.8-11.3 25.5-25.2 0-13.6-12-25.8-25.5-25.8-13.5 0-25.2 11.3-25.5 25.5 0 14.3 11.3 25.5 25.2 25.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
4
auth/assets/custom-icons/icons/aruba.svg
Normal file
4
auth/assets/custom-icons/icons/aruba.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#FF8300" fill-rule="evenodd" d="M12.1099561,17.3015551 C9.03598293,17.3015551 6.50849391,14.8423766 6.50849391,11.836714 C6.50849391,8.83105139 9.03598293,6.37187289 12.1099561,6.37187289 C15.1839292,6.37187289 17.7114182,8.83105139 17.7114182,11.836714 C17.7114182,14.8423766 15.1839292,17.3015551 12.1099561,17.3015551 L12.1099561,17.3015551 Z M12.1099561,2 C6.50849391,2 2,6.4401834 2,11.836714 C2,17.3015551 6.50849391,21.673428 12.1099561,21.673428 C14.4325135,21.673428 16.5501395,20.9220123 18.2579023,19.6241126 C19.28256,21.3318754 22.2199121,21.673428 22.2199121,21.673428 L22.2199121,11.836714 C22.2199121,6.4401834 17.7114182,2 12.1099561,2 L12.1099561,2 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 901 B |
3
auth/assets/custom-icons/icons/eve_online.svg
Normal file
3
auth/assets/custom-icons/icons/eve_online.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="198.4" height="198.4" xml:space="preserve">
|
||||
<path transform="translate(0, 60)" d="M 0,0 0,13.88 10.97,13.88 10.97,10.31 60.69,10.31 60.69,0 0,0 z M 65.84,0 99.22,58.09 132.6,0 120.7,0 C 120.7,0 100.5,34.91 99.22,37.16 97.92,34.91 77.75,0 77.75,0 L 65.84,0 z M 137.8,0 137.8,13.88 148.7,13.88 148.7,10.31 198.4,10.31 198.4,0 137.8,0 z M 0,19.12 0,29.47 60.69,29.47 60.69,19.12 0,19.12 z M 137.8,19.12 137.8,29.47 198.4,29.47 198.4,19.12 137.8,19.12 z M 0,34.66 0,48.59 60.69,48.59 60.69,38.25 10.97,38.25 10.97,34.66 0,34.66 z M 137.8,34.66 137.8,48.59 198.4,48.59 198.4,38.25 148.7,38.25 148.7,34.66 137.8,34.66 z M 42.19,69.72 C 41.32,69.72 40.71,69.89 40.41,70.19 40.1,70.49 39.97,71.03 39.97,71.84 L 39.97,76.56 C 39.97,77.38 40.1,77.93 40.41,78.22 40.71,78.52 41.32,78.66 42.19,78.66 L 48.72,78.66 C 49.59,78.66 50.19,78.52 50.5,78.22 50.8,77.93 50.97,77.38 50.97,76.56 L 50.97,71.84 C 50.97,71.03 50.8,70.49 50.5,70.19 50.19,69.89 49.59,69.72 48.72,69.72 L 42.19,69.72 z M 64.37,69.72 64.37,78.66 66.25,78.66 66.25,73.84 C 66.25,73.66 66.23,73.43 66.22,73.19 66.2,72.94 66.18,72.69 66.16,72.41 66.26,72.53 66.38,72.67 66.5,72.78 66.62,72.89 66.75,73.01 66.91,73.16 L 73.47,78.66 74.88,78.66 74.88,69.72 73.03,69.72 73.03,74.41 C 73.03,74.52 73.05,74.7 73.06,74.91 73.07,75.11 73.09,75.47 73.12,75.97 72.99,75.81 72.82,75.66 72.66,75.5 72.49,75.35 72.31,75.18 72.09,75 L 65.81,69.72 64.37,69.72 z M 88.53,69.72 88.53,78.66 97.31,78.66 97.31,77 90.59,77 90.59,69.72 88.53,69.72 z M 109.4,69.72 109.4,78.66 111.5,78.66 111.5,69.72 109.4,69.72 z M 125.1,69.72 125.1,78.66 127,78.66 127,73.84 C 127,73.66 127,73.43 126.9,73.19 126.9,72.94 126.9,72.69 126.9,72.41 127,72.53 127.1,72.67 127.2,72.78 127.3,72.89 127.5,73.01 127.6,73.16 L 134.2,78.66 135.6,78.66 135.6,69.72 133.8,69.72 133.8,74.41 C 133.8,74.52 133.8,74.7 133.8,74.91 133.8,75.11 133.8,75.47 133.8,75.97 133.7,75.81 133.6,75.66 133.4,75.5 133.2,75.35 133,75.18 132.8,75 L 126.5,69.72 125.1,69.72 z M 149.3,69.72 149.3,78.66 158.5,78.66 158.5,77 151.3,77 151.3,74.78 155.4,74.78 155.4,73.25 151.3,73.25 151.3,71.25 158.4,71.25 158.4,69.72 149.3,69.72 z M 42.03,71.31 48.87,71.31 48.87,77 42.03,77 42.03,71.31 z" /></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
auth/assets/custom-icons/icons/onshape.svg
Normal file
1
auth/assets/custom-icons/icons/onshape.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 33 KiB |
1
auth/assets/custom-icons/icons/realvnc.svg
Normal file
1
auth/assets/custom-icons/icons/realvnc.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
13
auth/assets/custom-icons/icons/seafile.svg
Normal file
13
auth/assets/custom-icons/icons/seafile.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 120" width="180" height="120">
|
||||
<title>seafile</title>
|
||||
<defs>
|
||||
<linearGradient id="g1" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0,114.369,-177.525,0,89.989,2.834)">
|
||||
<stop offset="0" stop-color="#fad956"/>
|
||||
<stop offset="1" stop-color="#ffa10f"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style>
|
||||
.s0 { fill: url(#g1) }
|
||||
</style>
|
||||
<path class="s0" d="m1.2 52.8c0-3 2.4-5.4 5.4-5.4 1.4 0 2.7 0.6 3.6 1.5q0-0.7 0-1.4c0-9.9 8-17.9 17.9-17.9 2.5 0 4.9 0.5 7.1 1.5q0-0.8 0-1.5c0-14.8 12-26.8 26.8-26.8 14.7 0 26.6 11.9 26.8 26.6-4.8 4.2-8.7 9.6-11.2 15.7-4.8-3-10.4-4.8-16.5-4.8-12.4 0-23.2 7.1-28.3 17.8h-19.1-7.1c-3 0-5.4-2.4-5.4-5.3zm141.2-16c-6.6-6.7-15.8-10.8-25.9-10.8-18.5 0-33.8 13.7-36.3 31.5-4.5-6.1-11.8-10-20-10-13.8 0-25 11.2-25 25 0 4 0.9 7.8 2.6 11.2-8.7 1.7-15.1 8.5-15.1 16.5 0 9.4 8.8 17 19.7 17 4.7 0 9.1-1.5 12.6-4l40.2-39.5c4.4-4.1 10.3-6.6 16.8-6.6 13.6 0 24.7 10.9 25.1 24.4q0 0-0.1-0.1c0.2 4-1.8 8.1-5.7 10.3-5.3 3.1-12 1.4-15-3.7-2.9-5.1-1-11.7 4.4-14.8q1.9-1.1 3.9-1.4-1.8-0.4-3.6-0.4c-9.9 0-17.9 8-17.9 17.9 0 9.9 8 17.9 17.9 17.9q0.6 0 1.3-0.1l0.5-0.1h35.1v0.2c10.7-0.5 20.9-10.4 20.9-22.5 0-12.3-10.6-22.4-22.9-22.4q-0.1 0-0.1 0c-2 3.6-4.4 5.7-7.1 7.9 2.8-5.2 4.5-11.2 4.5-17.6-0.1-10.1-4.2-19.2-10.8-25.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
Submodule auth/flutter updated: 68415ad1d9...2663184aa7
@@ -88,6 +88,8 @@
|
||||
"useRecoveryKey": "Usa la clau de recuperació",
|
||||
"incorrectPasswordTitle": "Contrasenya incorrecta",
|
||||
"welcomeBack": "Benvingut de nou!",
|
||||
"emailAlreadyRegistered": "El correu electrònic ja està registrat.",
|
||||
"emailNotRegistered": "El correu electrònic no està registrat.",
|
||||
"madeWithLoveAtPrefix": "fet amb ❤️ a ",
|
||||
"supportDevs": "Subscriu-te a <bold-green>ente</bold-green> per donar-nos suport",
|
||||
"supportDiscount": "Usa el codi de descompte \"AUTH\" per obtenir un 10% de descompte el primer any",
|
||||
@@ -502,5 +504,13 @@
|
||||
"deselectAll": "Desselecciona-ho tot",
|
||||
"selectAll": "Seleccionar-ho tot",
|
||||
"deleteDuplicates": "Elimina duplicats",
|
||||
"plainHTML": "HTML pla"
|
||||
"plainHTML": "HTML pla",
|
||||
"tellUsWhatYouThink": "Digueu-nos què us sembla",
|
||||
"dropReview": "Deixa una ressenya a l'App/Play Store",
|
||||
"supportEnte": "Donar suport a <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Dona'ns una estrella a Github",
|
||||
"free5GB": "5 GB gratuïts a <bold-green>ente</bold-green> Photos",
|
||||
"loginWithAuthAccount": "Inicieu sessió amb el vostre compte Auth",
|
||||
"freeStorageOffer": "10% de descompte a <bold-green>ente</bold-green> photos",
|
||||
"freeStorageOfferDescription": "Utilitzeu el codi \"AUTH\" per obtenir un 10% de descompte el primer any"
|
||||
}
|
||||
@@ -88,6 +88,8 @@
|
||||
"useRecoveryKey": "Wiederherstellungsschlüssel verwenden",
|
||||
"incorrectPasswordTitle": "Falsches Passwort",
|
||||
"welcomeBack": "Willkommen zurück!",
|
||||
"emailAlreadyRegistered": "E-Mail ist bereits registriert.",
|
||||
"emailNotRegistered": "E-Mail-Adresse nicht registriert.",
|
||||
"madeWithLoveAtPrefix": "gemacht mit ❤️ bei ",
|
||||
"supportDevs": "Bei <bold-green>ente</bold-green> registrieren, um das Projekt zu unterstützen",
|
||||
"supportDiscount": "Benutzen Sie den Rabattcode \"AUTH\" für 10 % Rabatt im ersten Jahr",
|
||||
@@ -255,6 +257,8 @@
|
||||
"areYouSureYouWantToLogout": "Sind sie sicher, dass sie sich ausloggen möchten?",
|
||||
"yesLogout": "Ja ausloggen",
|
||||
"exit": "Schließen",
|
||||
"theme": "Theme",
|
||||
"systemTheme": "System",
|
||||
"verifyingRecoveryKey": "Verifiziere Wiederherstellungsschlüssel...",
|
||||
"recoveryKeyVerified": "Wiederherstellungsschlüssel verifiziert",
|
||||
"recoveryKeySuccessBody": "Großartig! Ihr Wiederherstellungsschlüssel ist gültig. Vielen Dank für die Verifizierung.\n\nBitte denken sie daran, dass sie ihren Wiederherstellungsschlüssel sicher aufbewahren.",
|
||||
@@ -325,6 +329,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Benutzerdefiniert",
|
||||
"activeSessions": "Aktive Sitzungen",
|
||||
"somethingWentWrongPleaseTryAgain": "Ein Fehler ist aufgetreten, bitte versuche es erneut",
|
||||
"thisWillLogYouOutOfThisDevice": "Dadurch wirst du von diesem Gerät abgemeldet!",
|
||||
@@ -478,5 +483,9 @@
|
||||
"setNewPin": "Neue PIN festlegen",
|
||||
"importFailureDescNew": "Die ausgewählte Datei konnte nicht verarbeitet werden.",
|
||||
"appLockNotEnabled": "App-Sperre nicht aktiviert",
|
||||
"appLockNotEnabledDescription": "Bitte aktivieren Sie die App-Sperre über Security > App-Sperre"
|
||||
"appLockNotEnabledDescription": "Bitte aktivieren Sie die App-Sperre über Security > App-Sperre",
|
||||
"duplicateCodes": "Doppelte Codes",
|
||||
"noDuplicates": "✨ Keine Duplikate",
|
||||
"deselectAll": "Alle abwählen",
|
||||
"selectAll": "Alles auswählen"
|
||||
}
|
||||
@@ -504,5 +504,12 @@
|
||||
"deselectAll": "Deseleccionar todo",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"deleteDuplicates": "Eliminar duplicados",
|
||||
"plainHTML": "HTML plano"
|
||||
"plainHTML": "HTML plano",
|
||||
"tellUsWhatYouThink": "Cuéntanos cuál es su opinión",
|
||||
"dropReview": "Danos una reseña en la App/Play Store",
|
||||
"supportEnte": "Apoya a <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Danos una estrella en GitHub",
|
||||
"free5GB": "5 GB gratis en <bold-green>ente</bold-green> Fotos",
|
||||
"freeStorageOffer": "10% de descuento en <bold-green>ente</bold-green> fotos",
|
||||
"freeStorageOfferDescription": "Usa el cupón \"AUTH\" para obtener un 10% de descuento en el primer año"
|
||||
}
|
||||
@@ -504,5 +504,13 @@
|
||||
"deselectAll": "Összes kijelölés megszüntetése",
|
||||
"selectAll": "Összes kijelölése",
|
||||
"deleteDuplicates": "Ismétlődések törlése",
|
||||
"plainHTML": "Sima HTML kód"
|
||||
"plainHTML": "Sima HTML kód",
|
||||
"tellUsWhatYouThink": "Mondja el mit gondol",
|
||||
"dropReview": "Írjon véleményt az App/Play Store-ban",
|
||||
"supportEnte": "Támogassa <bold-green>ente <bold-green>",
|
||||
"giveUsAStarOnGithub": "Adj nekünk egy csillagot a Githubon",
|
||||
"free5GB": "5GB ingyen <bold-green>ente <bold-green> Photos",
|
||||
"loginWithAuthAccount": "Jelentkezzen be Auth fiókjával",
|
||||
"freeStorageOffer": "10% kedvezmény on <bold-green>ente<bold-green> photos",
|
||||
"freeStorageOfferDescription": "Használja az \"AUTH\" kódot, hogy 10% kedvezményt kapjon az első évben"
|
||||
}
|
||||
@@ -499,7 +499,18 @@
|
||||
"appLockOfflineModeWarning": "バックアップなしで進むことを選択しました。アプリロックを忘れると、データにアクセスできなくなります。",
|
||||
"duplicateCodes": "重複コード",
|
||||
"noDuplicates": "✨ 重複なし",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "削除できる重複コードはありません",
|
||||
"deduplicateCodes": "重複コード",
|
||||
"deselectAll": "すべての選択を解除",
|
||||
"selectAll": "すべて選択",
|
||||
"deleteDuplicates": "重複を削除",
|
||||
"plainHTML": "Plain HTML",
|
||||
"tellUsWhatYouThink": "ご意見をお聞かせください",
|
||||
"loginWithAuthAccount": "認証アカウントでログイン"
|
||||
"dropReview": "App/Playストアにレビューを投稿する",
|
||||
"supportEnte": "<bold-green>ente</bold-green>をサポートする",
|
||||
"giveUsAStarOnGithub": "Githubで星をつける",
|
||||
"free5GB": "<bold-green>ente</bold-green>フォトで5GB無料",
|
||||
"loginWithAuthAccount": "認証アカウントでログイン",
|
||||
"freeStorageOffer": "<bold-green>ente</bold-green>の写真が10%オフ",
|
||||
"freeStorageOfferDescription": "クーポンコード \"AUTH\" の使用で初年度が10%オフになります"
|
||||
}
|
||||
@@ -1 +1,28 @@
|
||||
{}
|
||||
{
|
||||
"blog": "ബ്ലോഗ്",
|
||||
"verifyPassword": "പാസ്വേഡ് സ്ഥിരീകരിക്കുക",
|
||||
"recreatePassword": "പാസ്വേഡ് പുനഃസൃഷ്ടിക്കുക",
|
||||
"incorrectPasswordTitle": "തെറ്റായ പാസ്വേഡ്",
|
||||
"welcomeBack": "വീണ്ടും സ്വാഗതം!",
|
||||
"emailAlreadyRegistered": "ഇമെയിൽ ഇതിനകം രജിസ്റ്റർ ചെയ്തിട്ടുണ്ട്.",
|
||||
"emailNotRegistered": "ഇമെയിൽ രജിസ്റ്റർ ചെയ്തിട്ടില്ല.",
|
||||
"changeEmail": "ഇമെയിൽ മാറ്റുക",
|
||||
"changePassword": "പാസ്സ്വേർഡ് മാറ്റുക",
|
||||
"ok": "ശരി",
|
||||
"cancel": "റദ്ദാക്കുക",
|
||||
"yes": "അതെ",
|
||||
"no": "അല്ല",
|
||||
"email": "ഇമെയിൽ",
|
||||
"somethingWentWrongMessage": "എന്തോ കുഴപ്പമുണ്ടായി, ദയവായി വീണ്ടും ശ്രമിക്കുക",
|
||||
"inFamilyPlanMessage": "നിങ്ങൾ ഒരു ഫാമിലി പ്ലാനിലാണ്!",
|
||||
"scan": "സ്കാൻ ചെയ്യുക",
|
||||
"scanACode": "കോഡ് സ്കാൻ ചെയ്യുക",
|
||||
"verify": "പരിശോധിക്കുക",
|
||||
"verifyEmail": "ഇമെയിൽ സ്ഥിരീകരിക്കുക",
|
||||
"enterCodeHint": "നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പിൽ നിന്നുള്ള 6 അക്ക കോഡ് നൽകുക",
|
||||
"twoFactorAuthTitle": "ടു-ഫാക്ടർ ആധികാരികത",
|
||||
"createNewAccount": "പുതിയ അക്കൗണ്ട് സൃഷ്ടിക്കുക",
|
||||
"confirmPassword": "പാസ്വേഡ് സ്ഥിരീകരിക്കുക",
|
||||
"language": "ഭാഷ",
|
||||
"security": "സുരക്ഷ"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class WindowListenerService {
|
||||
static const double minWindowHeight = 320.0;
|
||||
static const double minWindowHeight = 600.0;
|
||||
static const double minWindowWidth = 800.0;
|
||||
static const double maxWindowHeight = 8192.0;
|
||||
static const double maxWindowWidth = 8192.0;
|
||||
|
||||
48
cli/main.go
48
cli/main.go
@@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var AppVersion = "0.2.2"
|
||||
var AppVersion = "0.2.3"
|
||||
|
||||
func main() {
|
||||
cliConfigDir, err := GetCLIConfigDir()
|
||||
@@ -50,18 +50,21 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Define a set of commands that do not require KeyHolder initialisation.
|
||||
skipKeyHolderCommands := map[string]struct{}{"version": {}, "docs": {}, "help": {}}
|
||||
// Define a set of commands that do not require KeyHolder or cli initialisation.
|
||||
skipInitCommands := map[string]struct{}{"version": {}, "docs": {}, "help": {}}
|
||||
|
||||
var keyHolder *secrets.KeyHolder
|
||||
|
||||
// Only initialise KeyHolder if the command isn't in the skip list.
|
||||
shouldInit := len(os.Args) > 1
|
||||
if len(os.Args) > 1 {
|
||||
if _, skip := skipKeyHolderCommands[os.Args[1]]; !skip {
|
||||
keyHolder = secrets.NewKeyHolder(secrets.GetOrCreateClISecret())
|
||||
if _, skip := skipInitCommands[os.Args[1]]; skip {
|
||||
shouldInit = false
|
||||
}
|
||||
}
|
||||
|
||||
if shouldInit {
|
||||
keyHolder = secrets.NewKeyHolder(secrets.GetOrCreateClISecret())
|
||||
}
|
||||
ctrl := pkg.ClICtrl{
|
||||
Client: api.NewClient(api.Params{
|
||||
Debug: viper.GetBool("log.http"),
|
||||
@@ -71,16 +74,10 @@ func main() {
|
||||
KeyHolder: keyHolder,
|
||||
}
|
||||
|
||||
err = ctrl.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if len(os.Args) == 1 {
|
||||
// If no arguments are passed, show help
|
||||
os.Args = append(os.Args, "help")
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(os.Args) == 2 && os.Args[1] == "docs" {
|
||||
log.Println("Generating docs")
|
||||
err = cmd.GenerateDocs()
|
||||
@@ -89,9 +86,16 @@ func main() {
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(os.Args) == 1 {
|
||||
// If no arguments are passed, show help
|
||||
os.Args = append(os.Args, "help")
|
||||
if shouldInit {
|
||||
err = ctrl.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if os.Args[1] == "version" && viper.GetString("endpoint.api") != constants.EnteApiUrl {
|
||||
log.Printf("Custom endpoint: %s\n", viper.GetString("endpoint.api"))
|
||||
@@ -120,10 +124,10 @@ func initConfig(cliConfigDir string) {
|
||||
func GetCLIConfigDir() (string, error) {
|
||||
var configDir = os.Getenv("ENTE_CLI_CONFIG_DIR")
|
||||
|
||||
if configDir == "" {
|
||||
// for backward compatibility, check for ENTE_CLI_CONFIG_PATH
|
||||
configDir = os.Getenv("ENTE_CLI_CONFIG_PATH")
|
||||
}
|
||||
if configDir == "" {
|
||||
// for backward compatibility, check for ENTE_CLI_CONFIG_PATH
|
||||
configDir = os.Getenv("ENTE_CLI_CONFIG_PATH")
|
||||
}
|
||||
|
||||
if configDir != "" {
|
||||
// remove trailing slash (for all OS)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.7.9 (Unreleased)
|
||||
## v1.7.10 (Unreleased)
|
||||
|
||||
- .
|
||||
|
||||
## v1.7.9
|
||||
|
||||
- Light mode.
|
||||
- Faster and more stable thumbnail generation.
|
||||
- Support `.supplemental-metadata` JSON files in Google Takeout.
|
||||
- .
|
||||
|
||||
## v1.7.8
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
</branding>
|
||||
|
||||
<releases>
|
||||
<release version="1.7.8" date="2025-01-13">
|
||||
<url type="details">https://github.com/ente-io/photos-desktop/releases/tag/v1.7.8</url>
|
||||
<release version="1.7" date="2025-01-13">
|
||||
<url type="details">https://github.com/ente-io/photos-desktop/releases</url>
|
||||
</release>
|
||||
</releases>
|
||||
</component>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ente",
|
||||
"version": "1.7.9-beta",
|
||||
"version": "1.7.10-beta",
|
||||
"private": true,
|
||||
"description": "Desktop client for Ente Photos",
|
||||
"repository": "github:ente-io/photos-desktop",
|
||||
|
||||
@@ -362,8 +362,18 @@ const createMainWindow = () => {
|
||||
// do it (Step 2) unconditionally (i.e., on macOS too).
|
||||
//
|
||||
// https://www.electronjs.org/docs/latest/tutorial/custom-title-bar#create-a-custom-title-bar
|
||||
//
|
||||
// Note that by default on Windows, the color of the WCO title bar
|
||||
// overlay (three buttons - minimize, maximize, close - on the top
|
||||
// right) is static, and unlike Linux, doesn't adapt to the theme /
|
||||
// content. Explicitly choosing a dark background, while it won't work
|
||||
// always (if the user's theme is light), is better than picking a light
|
||||
// background since the main image viewer is always dark.
|
||||
titleBarStyle: "hidden",
|
||||
titleBarOverlay: true,
|
||||
titleBarOverlay:
|
||||
process.platform == "win32"
|
||||
? { color: "black", symbolColor: "#cdcdcd" }
|
||||
: true,
|
||||
// The color to show in the window until the web content gets loaded.
|
||||
// https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property
|
||||
//
|
||||
|
||||
@@ -138,6 +138,10 @@ export const sidebar = [
|
||||
text: "Machine Learning",
|
||||
link: "/photos/faq/machine-learning",
|
||||
},
|
||||
{
|
||||
text: "Video Streaming",
|
||||
link: "/photos/faq/video-streaming",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
63
docs/docs/photos/faq/video-streaming.md
Normal file
63
docs/docs/photos/faq/video-streaming.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Video Streaming FAQ
|
||||
description:
|
||||
Frequently asked questions about Ente's Video Streaming feature
|
||||
---
|
||||
|
||||
# Video Streaming
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Video streaming is available in beta on mobile apps starting v0.9.98.
|
||||
|
||||
### How to enable video streaming?
|
||||
|
||||
- Open Settings -> General -> Advanced
|
||||
- Switch on the toggle for `Video streaming`
|
||||
|
||||
### What happens when I enable video streaming?
|
||||
|
||||
Enabling video streaming will start processing videos captured in the last 30
|
||||
days, generating streams for each. Both local and remote videos will be
|
||||
processed, so this may consume bandwidth for downloading of remote files and
|
||||
uploading of the generated streams.
|
||||
|
||||
### How can I view video streams?
|
||||
|
||||
Settings -> Backup > Backup status will show details regarding the processing
|
||||
status for videos. Processed videos will have a green play button next to them.
|
||||
You can open these videos by tapping on them.
|
||||
|
||||
Processed videos will show a `Play stream` button, clicking which will load and
|
||||
play the stream.
|
||||
|
||||
Clicking on the `Info` icon within the original video will show details
|
||||
about the generated stream.
|
||||
|
||||
### What is a stream?
|
||||
|
||||
Stream is an encrypted HLS file with an `.m3u8` playlist that helps play a video
|
||||
with support for seeking **without** downloading the full file.
|
||||
|
||||
Currently it converts videos into `720p` with `2mbps` bitrate in `H.264` format.
|
||||
The generated stream is single blob (encrypted with AES) while the playlist file
|
||||
(`.m3u8`) is another blob (encrypted using XChaCha20).
|
||||
|
||||
We cannot read the contents, duration or the number of chunks within the
|
||||
generated stream.
|
||||
|
||||
### Will streams consume space in my storage?
|
||||
|
||||
While this feature is in beta, we will not count the storage consumed by your
|
||||
streams against your storage quota. This may change in the future. If it does,
|
||||
we will provide an option to opt-in to one of the following:
|
||||
1. Original videos only
|
||||
2. Compressed streams only
|
||||
3. Both
|
||||
|
||||
### Something doesn't seem right, what to do?
|
||||
|
||||
As video streaming is still in beta, some things might not work correctly.
|
||||
Please create a thread within the `#feedback` channel on
|
||||
[Discord](https://discord.com/channels/948937918347608085/1121126215995113552)
|
||||
or reach out to [support@ente.io](mailto:support@ente.io).
|
||||
@@ -43,6 +43,10 @@ need to disable this "Optimize battery usage" mode in the system settings for
|
||||
Ente if you wish for Ente to automatically back up your photos in the
|
||||
background.
|
||||
|
||||
On Android versions 15 and later, if an app is in private space and the private
|
||||
space is locked, Android doesn’t allow the app to run any background processes.
|
||||
As a result, background sync will not work.
|
||||
|
||||
### Desktop
|
||||
|
||||
In addition to our mobile apps, the background sync also works on our desktop
|
||||
|
||||
@@ -20,23 +20,25 @@ the logs just make the process a bit faster and easier.
|
||||
- Select for the option to _Report a Bug_.
|
||||
- Tap on _Report a bug_.
|
||||
|
||||
## Desktop and Web
|
||||
|
||||
- Open settings (click on the three horizontal lines button located at the top
|
||||
left corner of the screen).
|
||||
- Click on the _Help_ option towards the bottom of settings.
|
||||
- Click on _View logs_. This will show you the location of the logs on your
|
||||
system (desktop), or download them from the browser onto your computer (web).
|
||||
- Go back to settings.
|
||||
- Click on _Support_. This will open your email client where you can attach the
|
||||
logs in the email and describe the issue.
|
||||
|
||||
## Desktop
|
||||
|
||||
- Click on _Help_ menu at the top of your screen, and select the _View logs_
|
||||
option.
|
||||
- Open settings (click on the three horizontal lines button located at the top
|
||||
left corner of the screen).
|
||||
- Click on _Support_. This will open your email client where you can attach the
|
||||
logs in the email and describe the issue.
|
||||
On the desktop app, you can also directly view the logs on your computer at the
|
||||
following locations:
|
||||
|
||||
## Web
|
||||
|
||||
- Open settings (click on the three horizontal lines button located at the top
|
||||
left corner of the screen).
|
||||
- Click on _Debug Logs_ towards the bottom of settings.
|
||||
- Click on _Download logs_
|
||||
- Click on _Support_. This will open your email client where you can attach the
|
||||
logs in the email and describe the issue.
|
||||
- macOS: `~/Library/Logs/ente/ente.log`
|
||||
- Linux: `~/.config/ente/logs/ente.log`
|
||||
- Windows: `%USERPROFILE%\AppData\Roaming\ente\logs\ente.log`
|
||||
|
||||
## Send email manually
|
||||
|
||||
|
||||
2
mobile/android/app/proguard-rules.pro
vendored
2
mobile/android/app/proguard-rules.pro
vendored
@@ -2,3 +2,5 @@
|
||||
# To ensure that stack traces is unambiguous
|
||||
# https://developer.android.com/studio/build/shrink-code#decode-stack-trace
|
||||
-keepattributes LineNumberTable,SourceFile
|
||||
|
||||
-keep class org.chromium.net.** { *; }
|
||||
|
||||
@@ -6,6 +6,9 @@ PODS:
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- cupertino_http (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- dart_ui_isolate (0.0.1):
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
@@ -158,6 +161,8 @@ PODS:
|
||||
- nanopb/encode (3.30910.0)
|
||||
- native_video_player (1.0.0):
|
||||
- Flutter
|
||||
- objective_c (0.0.1):
|
||||
- Flutter
|
||||
- onnxruntime (0.0.1):
|
||||
- Flutter
|
||||
- onnxruntime-objc (= 1.18.0)
|
||||
@@ -247,6 +252,7 @@ DEPENDENCIES:
|
||||
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
|
||||
- battery_info (from `.symlinks/plugins/battery_info/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
|
||||
- dart_ui_isolate (from `.symlinks/plugins/dart_ui_isolate/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- ffmpeg_kit_flutter_full_gpl (from `.symlinks/plugins/ffmpeg_kit_flutter_full_gpl/ios`)
|
||||
@@ -278,6 +284,7 @@ DEPENDENCIES:
|
||||
- motionphoto (from `.symlinks/plugins/motionphoto/ios`)
|
||||
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
|
||||
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
||||
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||
- onnxruntime (from `.symlinks/plugins/onnxruntime/ios`)
|
||||
- open_mail_app (from `.symlinks/plugins/open_mail_app/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
@@ -331,6 +338,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/battery_info/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
cupertino_http:
|
||||
:path: ".symlinks/plugins/cupertino_http/darwin"
|
||||
dart_ui_isolate:
|
||||
:path: ".symlinks/plugins/dart_ui_isolate/ios"
|
||||
device_info_plus:
|
||||
@@ -393,6 +402,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/move_to_background/ios"
|
||||
native_video_player:
|
||||
:path: ".symlinks/plugins/native_video_player/ios"
|
||||
objective_c:
|
||||
:path: ".symlinks/plugins/objective_c/ios"
|
||||
onnxruntime:
|
||||
:path: ".symlinks/plugins/onnxruntime/ios"
|
||||
open_mail_app:
|
||||
@@ -439,82 +450,84 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2
|
||||
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57
|
||||
battery_info: a06b00c06a39bc94c92beebf600f1810cb6c8c87
|
||||
connectivity_plus: 3f6c9057f4cd64198dc826edfb0542892f825343
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b
|
||||
ffmpeg_kit_flutter_full_gpl: 8d15c14c0c3aba616fac04fe44b3d27d02e3c330
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
ffmpeg_kit_flutter_full_gpl: ce18b888487c05c46ed252cd2e7956812f2e3bd1
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af
|
||||
firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38
|
||||
firebase_core: 085320ddfaacb80d1a96eac3a87857afcc150db1
|
||||
firebase_messaging: d398edc15fe825f832836e74f6ac61e8cd2f3ad3
|
||||
FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da
|
||||
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
|
||||
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
|
||||
FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
flutter_email_sender: cd533cdc7ea5eda6fabb2c7f78521c71207778a4
|
||||
flutter_image_compress: 4b058288a81f76e5e80340af37c709abafff34c4
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: 35ddbc7228eafcb3969dcc5f1fbbe27c1145a4f0
|
||||
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
|
||||
flutter_sodium: 152647449ba89a157fd48d7e293dcd6d29c6ab0e
|
||||
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_purchase_storekit: 8c3b0b3eb1b0f04efbff401c3de6266d4258d433
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
in_app_purchase_storekit: e126ef1b89e4a9fdf07e28f005f82632b4609437
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203
|
||||
media_extension: 6d30dc1431ebaa63f43c397c37917b1a0a597a4c
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91
|
||||
motionphoto: d4a432b8c8f22fb3ad966258597c0103c9c5ff16
|
||||
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
|
||||
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
|
||||
media_extension: a1fec16ee9c8241a6aef9613578ebf097d6c5e64
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_native_event_loop: 5fba1a849a6c87a34985f1e178a0de5bd444a0cf
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
|
||||
motionphoto: 584b43031ead3060225cdff08fa49818879801d2
|
||||
move_to_background: 155f7bfbd34d43ad847cb630d2d2d87c17199710
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c
|
||||
onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997
|
||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
|
||||
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
|
||||
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
|
||||
open_mail_app: 794172f6a22cd16319d3ddaf45e945b2f74952b0
|
||||
open_mail_app: 06d5a4162866388a92b1df3deb96e56be20cf45c
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: df9c334dc9feadcbd3266e5cb49c8443405e1c9f
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
receive_sharing_intent: f6a12b7e8f7ed745f61c982de8a65de88db44a44
|
||||
screen_brightness_ios: 5ed898fa50fa82a26171c086ca5e28228f932576
|
||||
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57
|
||||
sentry_flutter: 0eb93e5279eb41e2392212afe1ccd2fecb4f8cbe
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||
sentry_flutter: 0a211008f52553ba5dd81ceb71f48d78f0f1f6ab
|
||||
share_plus: 011d6fb4f9d2576b83179a3a5c5e323202cdabcf
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 44bb54cc302bff1fbe5752293aba1820b157cf1c
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
|
||||
system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
|
||||
sqlite3_flutter_libs: 9379996d65aa23dcda7585a5b58766cebe0aa042
|
||||
system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
|
||||
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
|
||||
ua_client_hints: 46bb5817a868f9e397c0ba7e3f2f5c5d90c35156
|
||||
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||
video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
ua_client_hints: 0b48eae1134283f5b131ee0871fa878377f07a01
|
||||
uni_links: ed8c961e47ed9ce42b6d91e1de8049e38a4b3152
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
|
||||
volume_controller: ca1cde542ee70fad77d388f82e9616488110942b
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
|
||||
PODFILE CHECKSUM: 20e086e6008977d43a3d40260f3f9bffcac748dd
|
||||
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/cupertino_http/cupertino_http.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/dart_ui_isolate/dart_ui_isolate.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
|
||||
@@ -321,6 +322,7 @@
|
||||
"${BUILT_PRODUCTS_DIR}/move_to_background/move_to_background.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/native_video_player/native_video_player.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/objective_c/objective_c.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/open_mail_app/open_mail_app.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
|
||||
@@ -387,6 +389,7 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cupertino_http.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/dart_ui_isolate.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
|
||||
@@ -416,6 +419,7 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/move_to_background.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/native_video_player.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/objective_c.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_mail_app.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:photos/core/error-reporting/tunneled_transport.dart';
|
||||
import "package:photos/core/errors.dart";
|
||||
import 'package:photos/models/typedefs.dart';
|
||||
import "package:photos/utils/device_info.dart";
|
||||
import "package:photos/utils/ram_check_util.dart";
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -205,6 +206,12 @@ class SuperLogging {
|
||||
}),
|
||||
);
|
||||
|
||||
unawaited(
|
||||
checkDeviceTotalRAM().then((ram) {
|
||||
if (ram != null) $.info("Device RAM: ${ram}MB");
|
||||
}),
|
||||
);
|
||||
|
||||
if (appConfig.body == null) return;
|
||||
|
||||
if (enable && sentryIsEnabled) {
|
||||
@@ -236,7 +243,7 @@ class SuperLogging {
|
||||
}
|
||||
|
||||
static _shouldSkipSentry(Object error) {
|
||||
if (error is DioError) {
|
||||
if (error is DioException) {
|
||||
return true;
|
||||
}
|
||||
final bool result = error is StorageLimitExceededError ||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:native_dio_adapter/native_dio_adapter.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
@@ -8,18 +9,17 @@ import 'package:photos/core/network/ente_interceptor.dart';
|
||||
import "package:photos/events/endpoint_updated_event.dart";
|
||||
import "package:ua_client_hints/ua_client_hints.dart";
|
||||
|
||||
int kConnectTimeout = 15000;
|
||||
|
||||
class NetworkClient {
|
||||
late Dio _dio;
|
||||
late Dio _enteDio;
|
||||
static const kConnectTimeout = 15;
|
||||
|
||||
Future<void> init(PackageInfo packageInfo) async {
|
||||
final String ua = await userAgent();
|
||||
final endpoint = Configuration.instance.getHttpEndpoint();
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: kConnectTimeout,
|
||||
connectTimeout: const Duration(seconds: kConnectTimeout),
|
||||
headers: {
|
||||
HttpHeaders.userAgentHeader: ua,
|
||||
'X-Client-Version': packageInfo.version,
|
||||
@@ -30,7 +30,7 @@ class NetworkClient {
|
||||
_enteDio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: endpoint,
|
||||
connectTimeout: kConnectTimeout,
|
||||
connectTimeout: const Duration(seconds: kConnectTimeout),
|
||||
headers: {
|
||||
HttpHeaders.userAgentHeader: ua,
|
||||
'X-Client-Version': packageInfo.version,
|
||||
@@ -38,6 +38,10 @@ class NetworkClient {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
_dio.httpClientAdapter = NativeAdapter();
|
||||
_enteDio.httpClientAdapter = NativeAdapter();
|
||||
|
||||
_setupInterceptors(endpoint);
|
||||
|
||||
Bus.instance.on<EndpointUpdatedEvent>().listen((event) {
|
||||
|
||||
@@ -251,20 +251,20 @@ class CollectionsDB {
|
||||
Map<String, dynamic> _getRowForCollection(Collection collection) {
|
||||
final row = <String, dynamic>{};
|
||||
row[columnID] = collection.id;
|
||||
row[columnOwner] = collection.owner!.toJson();
|
||||
row[columnOwner] = collection.owner.toJson();
|
||||
row[columnEncryptedKey] = collection.encryptedKey;
|
||||
row[columnKeyDecryptionNonce] = collection.keyDecryptionNonce;
|
||||
row[columnName] = collection.name;
|
||||
row[columnEncryptedName] = collection.encryptedName;
|
||||
row[columnNameDecryptionNonce] = collection.nameDecryptionNonce;
|
||||
row[columnType] = Collection.typeToString(collection.type);
|
||||
row[columnType] = typeToString(collection.type);
|
||||
row[columnEncryptedPath] = collection.attributes.encryptedPath;
|
||||
row[columnPathDecryptionNonce] = collection.attributes.pathDecryptionNonce;
|
||||
row[columnVersion] = collection.attributes.version;
|
||||
row[columnSharees] =
|
||||
json.encode(collection.sharees?.map((x) => x?.toMap()).toList());
|
||||
json.encode(collection.sharees.map((x) => x.toMap()).toList());
|
||||
row[columnPublicURLs] =
|
||||
json.encode(collection.publicURLs?.map((x) => x?.toMap()).toList());
|
||||
json.encode(collection.publicURLs.map((x) => x.toMap()).toList());
|
||||
row[columnUpdationTime] = collection.updationTime;
|
||||
if (collection.isDeleted) {
|
||||
row[columnIsDeleted] = _sqlBoolTrue;
|
||||
@@ -290,7 +290,7 @@ class CollectionsDB {
|
||||
row[columnName],
|
||||
row[columnEncryptedName],
|
||||
row[columnNameDecryptionNonce],
|
||||
Collection.typeFromString(row[columnType]),
|
||||
typeFromString(row[columnType]),
|
||||
CollectionAttributes(
|
||||
encryptedPath: row[columnEncryptedPath],
|
||||
pathDecryptionNonce: row[columnPathDecryptionNonce],
|
||||
|
||||
@@ -1733,6 +1733,7 @@ class FilesDB {
|
||||
Future<List<EnteFile>> getAllFilesAfterDate({
|
||||
required FileType fileType,
|
||||
required DateTime beginDate,
|
||||
required int userID,
|
||||
}) async {
|
||||
final db = await instance.sqliteAsyncDB;
|
||||
final results = await db.getAll(
|
||||
@@ -1741,6 +1742,7 @@ class FilesDB {
|
||||
WHERE $columnFileType = ?
|
||||
AND $columnCreationTime > ?
|
||||
AND $columnUploadedFileID != -1
|
||||
AND $columnOwnerID = $userID
|
||||
ORDER BY $columnCreationTime DESC
|
||||
''',
|
||||
[getInt(fileType), beginDate.microsecondsSinceEpoch],
|
||||
|
||||
@@ -322,22 +322,20 @@ class _AddContactPage extends State<AddContactPage> {
|
||||
final int ownerID = Configuration.instance.getUserID()!;
|
||||
existingEmails.add(Configuration.instance.getEmail()!);
|
||||
for (final c in CollectionsService.instance.getActiveCollections()) {
|
||||
if (c.owner?.id == ownerID) {
|
||||
for (final User? u in c.sharees ?? []) {
|
||||
if (u != null &&
|
||||
u.id != null &&
|
||||
if (c.owner.id == ownerID) {
|
||||
for (final User u in c.sharees) {
|
||||
if (u.id != null &&
|
||||
u.email.isNotEmpty &&
|
||||
!existingEmails.contains(u.email)) {
|
||||
existingEmails.add(u.email);
|
||||
suggestedUsers.add(u);
|
||||
}
|
||||
}
|
||||
} else if (c.owner != null &&
|
||||
c.owner!.id != null &&
|
||||
c.owner!.email.isNotEmpty &&
|
||||
!existingEmails.contains(c.owner!.email)) {
|
||||
existingEmails.add(c.owner!.email);
|
||||
suggestedUsers.add(c.owner!);
|
||||
} else if (c.owner.id != null &&
|
||||
c.owner.email.isNotEmpty &&
|
||||
!existingEmails.contains(c.owner.email)) {
|
||||
existingEmails.add(c.owner.email);
|
||||
suggestedUsers.add(c.owner);
|
||||
}
|
||||
}
|
||||
final cachedUserDetails = UserService.instance.getCachedUserDetails();
|
||||
|
||||
@@ -12,7 +12,7 @@ class CastGateway {
|
||||
);
|
||||
return response.data["publicKey"];
|
||||
} catch (e) {
|
||||
if (e is DioError && e.response != null) {
|
||||
if (e is DioException && e.response != null) {
|
||||
if (e.response!.statusCode == 404) {
|
||||
return null;
|
||||
} else if (e.response!.statusCode == 403) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class EntityGateway {
|
||||
},
|
||||
);
|
||||
return EntityKey.fromMap(response.data);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null && (e.response!.statusCode ?? 0) == 404) {
|
||||
throw EntityKeyNotFound();
|
||||
} else {
|
||||
|
||||
2
mobile/lib/generated/intl/messages_en.dart
generated
2
mobile/lib/generated/intl/messages_en.dart
generated
@@ -1063,6 +1063,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Selected items will be removed from this album"),
|
||||
"join": MessageLookupByLibrary.simpleMessage("Join"),
|
||||
"joinAlbum": MessageLookupByLibrary.simpleMessage("Join album"),
|
||||
"joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage(
|
||||
"Joining an album will make your email visible to its participants."),
|
||||
"joinAlbumSubtext":
|
||||
MessageLookupByLibrary.simpleMessage("to view and add your photos"),
|
||||
"joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage(
|
||||
|
||||
10
mobile/lib/generated/l10n.dart
generated
10
mobile/lib/generated/l10n.dart
generated
@@ -11220,6 +11220,16 @@ class S {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Joining an album will make your email visible to its participants.`
|
||||
String get joinAlbumConfirmationDialogBody {
|
||||
return Intl.message(
|
||||
'Joining an album will make your email visible to its participants.',
|
||||
name: 'joinAlbumConfirmationDialogBody',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
||||
@@ -1673,5 +1673,6 @@
|
||||
"ineligible": "Ineligible",
|
||||
"failed": "Failed",
|
||||
"playStream": "Play stream",
|
||||
"playOriginal": "Play original"
|
||||
"playOriginal": "Play original",
|
||||
"joinAlbumConfirmationDialogBody" : "Joining an album will make your email visible to its participants."
|
||||
}
|
||||
@@ -45,7 +45,7 @@ class CreateRequest {
|
||||
map['keyDecryptionNonce'] = keyDecryptionNonce;
|
||||
map['encryptedName'] = encryptedName;
|
||||
map['nameDecryptionNonce'] = nameDecryptionNonce;
|
||||
map['type'] = Collection.typeToString(type);
|
||||
map['type'] = typeToString(type);
|
||||
if (attributes != null) {
|
||||
map['attributes'] = attributes!.toMap();
|
||||
}
|
||||
|
||||
51
mobile/lib/models/base_location.dart
Normal file
51
mobile/lib/models/base_location.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
|
||||
class BaseLocation {
|
||||
final List<EnteFile> files;
|
||||
int? firstCreationTime;
|
||||
int? lastCreationTime;
|
||||
final Location location;
|
||||
final bool isCurrentBase;
|
||||
|
||||
BaseLocation(
|
||||
this.files,
|
||||
this.location,
|
||||
this.isCurrentBase, {
|
||||
this.firstCreationTime,
|
||||
this.lastCreationTime,
|
||||
});
|
||||
|
||||
int averageCreationTime() {
|
||||
if (firstCreationTime != null && lastCreationTime != null) {
|
||||
return (firstCreationTime! + lastCreationTime!) ~/ 2;
|
||||
}
|
||||
final List<int> creationTimes = files
|
||||
.where((file) => file.creationTime != null)
|
||||
.map((file) => file.creationTime!)
|
||||
.toList();
|
||||
if (creationTimes.length < 2) {
|
||||
return creationTimes.isEmpty ? 0 : creationTimes.first;
|
||||
}
|
||||
creationTimes.sort();
|
||||
firstCreationTime ??= creationTimes.first;
|
||||
lastCreationTime ??= creationTimes.last;
|
||||
return (firstCreationTime! + lastCreationTime!) ~/ 2;
|
||||
}
|
||||
|
||||
BaseLocation copyWith({
|
||||
List<EnteFile>? files,
|
||||
int? firstCreationTime,
|
||||
int? lastCreationTime,
|
||||
Location? location,
|
||||
bool? isCurrentBase,
|
||||
}) {
|
||||
return BaseLocation(
|
||||
files ?? this.files,
|
||||
location ?? this.location,
|
||||
isCurrentBase ?? this.isCurrentBase,
|
||||
firstCreationTime: firstCreationTime ?? this.firstCreationTime,
|
||||
lastCreationTime: lastCreationTime ?? this.lastCreationTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import "package:photos/models/metadata/common_keys.dart";
|
||||
|
||||
class Collection {
|
||||
final int id;
|
||||
final User? owner;
|
||||
final User owner;
|
||||
final String encryptedKey;
|
||||
final String? keyDecryptionNonce;
|
||||
@Deprecated("Use collectionName instead")
|
||||
@@ -20,8 +20,8 @@ class Collection {
|
||||
final String? nameDecryptionNonce;
|
||||
final CollectionType type;
|
||||
final CollectionAttributes attributes;
|
||||
final List<User?>? sharees;
|
||||
final List<PublicURL?>? publicURLs;
|
||||
final List<User> sharees;
|
||||
final List<PublicURL> publicURLs;
|
||||
final int updationTime;
|
||||
final bool isDeleted;
|
||||
|
||||
@@ -95,12 +95,12 @@ class Collection {
|
||||
|
||||
// hasLink returns true if there's any link attached to the collection
|
||||
// including expired links
|
||||
bool get hasLink => publicURLs != null && publicURLs!.isNotEmpty;
|
||||
bool get hasLink => publicURLs.isNotEmpty;
|
||||
|
||||
bool get hasCover => (pubMagicMetadata.coverID ?? 0) > 0;
|
||||
|
||||
// hasSharees returns true if the collection is shared with other ente users
|
||||
bool get hasSharees => sharees != null && sharees!.isNotEmpty;
|
||||
bool get hasSharees => sharees.isNotEmpty;
|
||||
|
||||
bool get isPinned => (magicMetadata.order ?? 0) != 0;
|
||||
|
||||
@@ -121,52 +121,43 @@ class Collection {
|
||||
}
|
||||
|
||||
List<User> getSharees() {
|
||||
final List<User> result = [];
|
||||
if (sharees == null) {
|
||||
return result;
|
||||
}
|
||||
for (final User? u in sharees!) {
|
||||
if (u != null) {
|
||||
result.add(u);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return sharees;
|
||||
}
|
||||
|
||||
bool isOwner(int userID) {
|
||||
return (owner?.id ?? 0) == userID;
|
||||
return (owner.id ?? -100) == userID;
|
||||
}
|
||||
|
||||
bool isDownloadEnabledForPublicLink() {
|
||||
if (publicURLs == null || publicURLs!.isEmpty) {
|
||||
if (publicURLs.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return publicURLs?.first?.enableDownload ?? true;
|
||||
return publicURLs.first.enableDownload;
|
||||
}
|
||||
|
||||
bool isCollectEnabledForPublicLink() {
|
||||
if (publicURLs == null || publicURLs!.isEmpty) {
|
||||
if (publicURLs.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return publicURLs?.first?.enableCollect ?? false;
|
||||
return publicURLs.first.enableCollect;
|
||||
}
|
||||
|
||||
bool get isJoinEnabled {
|
||||
if (publicURLs == null || publicURLs!.isEmpty) {
|
||||
if (publicURLs.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return publicURLs?.first?.enableJoin ?? false;
|
||||
return publicURLs.first.enableJoin;
|
||||
}
|
||||
|
||||
CollectionParticipantRole getRole(int userID) {
|
||||
if (isOwner(userID)) {
|
||||
return CollectionParticipantRole.owner;
|
||||
}
|
||||
if (sharees == null) {
|
||||
if (sharees.isEmpty) {
|
||||
return CollectionParticipantRole.unknown;
|
||||
}
|
||||
for (final User? u in sharees!) {
|
||||
if (u != null && u.id == userID) {
|
||||
for (final User u in sharees) {
|
||||
if (u.id == userID) {
|
||||
if (u.isViewer) {
|
||||
return CollectionParticipantRole.viewer;
|
||||
} else if (u.isCollaborator) {
|
||||
@@ -185,40 +176,8 @@ class Collection {
|
||||
}
|
||||
|
||||
void updateSharees(List<User> newSharees) {
|
||||
sharees?.clear();
|
||||
sharees?.addAll(newSharees);
|
||||
}
|
||||
|
||||
static CollectionType typeFromString(String type) {
|
||||
switch (type) {
|
||||
case "folder":
|
||||
return CollectionType.folder;
|
||||
case "favorites":
|
||||
return CollectionType.favorites;
|
||||
case "uncategorized":
|
||||
return CollectionType.uncategorized;
|
||||
case "album":
|
||||
return CollectionType.album;
|
||||
case "unknown":
|
||||
return CollectionType.unknown;
|
||||
}
|
||||
debugPrint("unexpected collection type $type");
|
||||
return CollectionType.unknown;
|
||||
}
|
||||
|
||||
static String typeToString(CollectionType type) {
|
||||
switch (type) {
|
||||
case CollectionType.folder:
|
||||
return "folder";
|
||||
case CollectionType.favorites:
|
||||
return "favorites";
|
||||
case CollectionType.album:
|
||||
return "album";
|
||||
case CollectionType.uncategorized:
|
||||
return "uncategorized";
|
||||
case CollectionType.unknown:
|
||||
return "unknown";
|
||||
}
|
||||
sharees.clear();
|
||||
sharees.addAll(newSharees);
|
||||
}
|
||||
|
||||
Collection copyWith({
|
||||
@@ -303,6 +262,38 @@ enum CollectionType {
|
||||
unknown,
|
||||
}
|
||||
|
||||
CollectionType typeFromString(String type) {
|
||||
switch (type) {
|
||||
case "folder":
|
||||
return CollectionType.folder;
|
||||
case "favorites":
|
||||
return CollectionType.favorites;
|
||||
case "uncategorized":
|
||||
return CollectionType.uncategorized;
|
||||
case "album":
|
||||
return CollectionType.album;
|
||||
case "unknown":
|
||||
return CollectionType.unknown;
|
||||
}
|
||||
debugPrint("unexpected collection type $type");
|
||||
return CollectionType.unknown;
|
||||
}
|
||||
|
||||
String typeToString(CollectionType type) {
|
||||
switch (type) {
|
||||
case CollectionType.folder:
|
||||
return "folder";
|
||||
case CollectionType.favorites:
|
||||
return "favorites";
|
||||
case CollectionType.album:
|
||||
return "album";
|
||||
case CollectionType.uncategorized:
|
||||
return "uncategorized";
|
||||
case CollectionType.unknown:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
extension CollectionTypeExtn on CollectionType {
|
||||
bool get canDelete =>
|
||||
this != CollectionType.favorites && this != CollectionType.uncategorized;
|
||||
|
||||
@@ -160,6 +160,7 @@ class EnteFile {
|
||||
|
||||
Future<Map<String, dynamic>> getMetadataForUpload(
|
||||
MediaUploadData mediaUploadData,
|
||||
ParsedExifDateTime? exifTime,
|
||||
) async {
|
||||
final asset = await getAsset;
|
||||
// asset can be null for files shared to app
|
||||
@@ -170,36 +171,24 @@ class EnteFile {
|
||||
}
|
||||
}
|
||||
bool hasExifTime = false;
|
||||
if ((fileType == FileType.image || fileType == FileType.video) &&
|
||||
mediaUploadData.sourceFile != null) {
|
||||
final exifData = await getExifFromSourceFile(mediaUploadData.sourceFile!);
|
||||
if (exifData != null) {
|
||||
if (fileType == FileType.image) {
|
||||
final exifTime = await getCreationTimeFromEXIF(null, exifData);
|
||||
if (exifTime != null) {
|
||||
hasExifTime = true;
|
||||
creationTime = exifTime.microsecondsSinceEpoch;
|
||||
}
|
||||
mediaUploadData.isPanorama = checkPanoramaFromEXIF(null, exifData);
|
||||
|
||||
if (mediaUploadData.isPanorama != true) {
|
||||
try {
|
||||
final xmpData = await getXmp(mediaUploadData.sourceFile!);
|
||||
mediaUploadData.isPanorama = checkPanoramaFromXMP(xmpData);
|
||||
} catch (_) {}
|
||||
|
||||
mediaUploadData.isPanorama ??= false;
|
||||
}
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
//Fix for missing location data in lower android versions.
|
||||
final Location? exifLocation = locationFromExif(exifData);
|
||||
if (Location.isValidLocation(exifLocation)) {
|
||||
location = exifLocation;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (exifTime != null && exifTime.time != null) {
|
||||
hasExifTime = true;
|
||||
creationTime = exifTime.time!.microsecondsSinceEpoch;
|
||||
}
|
||||
if (mediaUploadData.exifData != null) {
|
||||
mediaUploadData.isPanorama =
|
||||
checkPanoramaFromEXIF(null, mediaUploadData.exifData);
|
||||
}
|
||||
if (mediaUploadData.isPanorama != true &&
|
||||
fileType == FileType.image &&
|
||||
mediaUploadData.sourceFile != null) {
|
||||
try {
|
||||
final xmpData = await getXmp(mediaUploadData.sourceFile!);
|
||||
mediaUploadData.isPanorama = checkPanoramaFromXMP(xmpData);
|
||||
} catch (_) {}
|
||||
mediaUploadData.isPanorama ??= false;
|
||||
}
|
||||
|
||||
// Try to get the timestamp from fileName. In case of iOS, file names are
|
||||
// generic IMG_XXXX, so only parse it on Android devices
|
||||
if (!hasExifTime && Platform.isAndroid && title != null) {
|
||||
|
||||
@@ -14,6 +14,8 @@ const latKey = "lat";
|
||||
const longKey = "long";
|
||||
const motionVideoIndexKey = "mvi";
|
||||
const noThumbKey = "noThumb";
|
||||
const dateTimeKey = 'dateTime';
|
||||
const offsetTimeKey = 'offsetTime';
|
||||
|
||||
class MagicMetadata {
|
||||
// 0 -> visible
|
||||
@@ -46,6 +48,11 @@ class PubMagicMetadata {
|
||||
double? lat;
|
||||
double? long;
|
||||
|
||||
// ISO 8601 datetime without timezone. This contains the date and time of the photo in the original tz
|
||||
// where the photo was taken.
|
||||
String? dateTime;
|
||||
String? offsetTime;
|
||||
|
||||
// Motion Video Index. Positive value (>0) indicates that the file is a motion
|
||||
// photo
|
||||
int? mvi;
|
||||
@@ -74,6 +81,8 @@ class PubMagicMetadata {
|
||||
this.mvi,
|
||||
this.noThumb,
|
||||
this.mediaType,
|
||||
this.dateTime,
|
||||
this.offsetTime,
|
||||
});
|
||||
|
||||
factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
|
||||
@@ -96,6 +105,8 @@ class PubMagicMetadata {
|
||||
mvi: map[motionVideoIndexKey],
|
||||
noThumb: map[noThumbKey],
|
||||
mediaType: map[mediaTypeKey],
|
||||
dateTime: map[dateTimeKey],
|
||||
offsetTime: map[offsetTimeKey],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
32
mobile/lib/models/trip_memory.dart
Normal file
32
mobile/lib/models/trip_memory.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
|
||||
class TripMemory {
|
||||
final List<EnteFile> files;
|
||||
final Location location;
|
||||
final int firstCreationTime;
|
||||
final int lastCreationTime;
|
||||
|
||||
TripMemory(
|
||||
this.files,
|
||||
this.location,
|
||||
this.firstCreationTime,
|
||||
this.lastCreationTime,
|
||||
);
|
||||
|
||||
int get averageCreationTime => (firstCreationTime + lastCreationTime) ~/ 2;
|
||||
|
||||
TripMemory copyWith({
|
||||
List<EnteFile>? files,
|
||||
Location? location,
|
||||
int? firstCreationTime,
|
||||
int? lastCreationTime,
|
||||
}) {
|
||||
return TripMemory(
|
||||
files ?? this.files,
|
||||
location ?? this.location,
|
||||
firstCreationTime ?? this.firstCreationTime,
|
||||
lastCreationTime ?? this.lastCreationTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ class MultiPartUploader {
|
||||
// upload individual parts and get their etags
|
||||
try {
|
||||
etags = await _uploadParts(multipartInfo, encryptedFile);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
_logger.severe(
|
||||
"Multipart upload not found for key ${multipartInfo.urls.objectKey}",
|
||||
@@ -157,7 +157,7 @@ class MultiPartUploader {
|
||||
etags,
|
||||
multipartInfo.urls.completeURL,
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
_logger.severe(
|
||||
"Multipart upload not found for key ${multipartInfo.urls.objectKey}",
|
||||
|
||||
@@ -92,7 +92,7 @@ class BillingService {
|
||||
},
|
||||
);
|
||||
return Subscription.fromMap(response.data["subscription"]);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null && e.response!.statusCode == 409) {
|
||||
throw SubscriptionAlreadyClaimedError();
|
||||
} else {
|
||||
@@ -109,7 +109,7 @@ class BillingService {
|
||||
final response = await _enteDio.get("/billing/subscription");
|
||||
final subscription = Subscription.fromMap(response.data["subscription"]);
|
||||
return subscription;
|
||||
} on DioError catch (e, s) {
|
||||
} on DioException catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
rethrow;
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class BillingService {
|
||||
await _enteDio.post("/billing/stripe/cancel-subscription");
|
||||
final subscription = Subscription.fromMap(response.data["subscription"]);
|
||||
return subscription;
|
||||
} on DioError catch (e, s) {
|
||||
} on DioException catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
rethrow;
|
||||
}
|
||||
@@ -133,7 +133,7 @@ class BillingService {
|
||||
await _enteDio.post("/billing/stripe/activate-subscription");
|
||||
final subscription = Subscription.fromMap(response.data["subscription"]);
|
||||
return subscription;
|
||||
} on DioError catch (e, s) {
|
||||
} on DioException catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
rethrow;
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class BillingService {
|
||||
},
|
||||
);
|
||||
return response.data["url"];
|
||||
} on DioError catch (e, s) {
|
||||
} on DioException catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ class CollectionsService {
|
||||
}
|
||||
}
|
||||
// remove reference for incoming collections when unshared/deleted
|
||||
if (collection.isDeleted && ownerID != collection.owner?.id) {
|
||||
if (collection.isDeleted && ownerID != collection.owner.id) {
|
||||
await _db.deleteCollection(collection.id);
|
||||
} else {
|
||||
// keep entry for deletedCollection as collectionKey may be used during
|
||||
@@ -394,7 +394,7 @@ class CollectionsService {
|
||||
final List<Collection> collections =
|
||||
getCollectionsForUI(includedShared: true);
|
||||
for (final c in collections) {
|
||||
if (c.owner!.id == Configuration.instance.getUserID()) {
|
||||
if (c.owner.id == Configuration.instance.getUserID()) {
|
||||
if (c.hasSharees || c.hasLink && !c.isQuickLinkCollection()) {
|
||||
outgoing.add(c);
|
||||
} else if (c.isQuickLinkCollection()) {
|
||||
@@ -472,8 +472,8 @@ class CollectionsService {
|
||||
if (collectionID != null) {
|
||||
final Collection? collection = getCollectionByID(collectionID);
|
||||
if (collection != null) {
|
||||
if (collection.owner?.id == userID) {
|
||||
_cachedUserIdToUser[userID] = collection.owner!;
|
||||
if (collection.owner.id == userID) {
|
||||
_cachedUserIdToUser[userID] = collection.owner;
|
||||
} else {
|
||||
final matchingUser = collection.getSharees().firstWhereOrNull(
|
||||
(u) => u.id == userID,
|
||||
@@ -553,7 +553,7 @@ class CollectionsService {
|
||||
unawaited(_db.insert([_collectionIDToCollections[collectionID]!]));
|
||||
RemoteSyncService.instance.sync(silently: true).ignore();
|
||||
return sharees;
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 402) {
|
||||
throw SharingNotPermittedForFreeAccountsError();
|
||||
}
|
||||
@@ -641,7 +641,7 @@ class CollectionsService {
|
||||
} else {
|
||||
await _handleCollectionDeletion(collection);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null) {
|
||||
debugPrint("Error " + e.response!.toString());
|
||||
}
|
||||
@@ -698,7 +698,7 @@ class CollectionsService {
|
||||
);
|
||||
final encryptedKey = CryptoUtil.base642bin(collection.encryptedKey);
|
||||
Uint8List? collectionKey;
|
||||
if (collection.owner?.id == _config.getUserID()) {
|
||||
if (collection.owner.id == _config.getUserID()) {
|
||||
// If the collection is owned by the user, decrypt with the master key
|
||||
if (_config.getKey() == null) {
|
||||
// Possible during AppStore account migration, where SecureStorage
|
||||
@@ -767,7 +767,7 @@ class CollectionsService {
|
||||
) async {
|
||||
final int ownerID = Configuration.instance.getUserID()!;
|
||||
try {
|
||||
if (collection.owner?.id != ownerID) {
|
||||
if (collection.owner.id != ownerID) {
|
||||
throw AssertionError("cannot modify albums not owned by you");
|
||||
}
|
||||
// read the existing magic metadata and apply new updates to existing data
|
||||
@@ -798,7 +798,7 @@ class CollectionsService {
|
||||
);
|
||||
await _enteDio.put(
|
||||
"/collections/magic-metadata",
|
||||
data: params,
|
||||
data: params.toJson(),
|
||||
);
|
||||
// update the local information so that it's reflected on UI
|
||||
collection.mMdEncodedJson = jsonEncode(jsonToUpdate);
|
||||
@@ -808,7 +808,7 @@ class CollectionsService {
|
||||
|
||||
// trigger sync to fetch the latest collection state from server
|
||||
sync().ignore();
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null && e.response?.statusCode == 409) {
|
||||
_logger.severe('collection magic data out of sync');
|
||||
sync().ignore();
|
||||
@@ -826,7 +826,7 @@ class CollectionsService {
|
||||
) async {
|
||||
final int ownerID = Configuration.instance.getUserID()!;
|
||||
try {
|
||||
if (collection.owner?.id != ownerID) {
|
||||
if (collection.owner.id != ownerID) {
|
||||
throw AssertionError("cannot modify albums not owned by you");
|
||||
}
|
||||
// read the existing magic metadata and apply new updates to existing data
|
||||
@@ -857,7 +857,7 @@ class CollectionsService {
|
||||
);
|
||||
await _enteDio.put(
|
||||
"/collections/public-magic-metadata",
|
||||
data: params,
|
||||
data: params.toJson(),
|
||||
);
|
||||
// update the local information so that it's reflected on UI
|
||||
collection.mMdPubEncodedJson = jsonEncode(jsonToUpdate);
|
||||
@@ -867,7 +867,7 @@ class CollectionsService {
|
||||
_cacheLocalPathAndCollection(collection);
|
||||
// trigger sync to fetch the latest collection state from server
|
||||
sync().ignore();
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null && e.response?.statusCode == 409) {
|
||||
_logger.severe('collection magic data out of sync');
|
||||
sync().ignore();
|
||||
@@ -885,7 +885,7 @@ class CollectionsService {
|
||||
) async {
|
||||
final int ownerID = Configuration.instance.getUserID()!;
|
||||
try {
|
||||
if (collection.owner?.id == ownerID) {
|
||||
if (collection.owner.id == ownerID) {
|
||||
throw AssertionError("cannot modify sharee settings for albums owned "
|
||||
"by you");
|
||||
}
|
||||
@@ -917,7 +917,7 @@ class CollectionsService {
|
||||
);
|
||||
await _enteDio.put(
|
||||
"/collections/sharee-magic-metadata",
|
||||
data: params,
|
||||
data: params.toJson(),
|
||||
);
|
||||
// update the local information so that it's reflected on UI
|
||||
collection.sharedMmdJson = jsonEncode(jsonToUpdate);
|
||||
@@ -927,7 +927,7 @@ class CollectionsService {
|
||||
_cacheLocalPathAndCollection(collection);
|
||||
// trigger sync to fetch the latest collection state from server
|
||||
sync().ignore();
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null && e.response?.statusCode == 409) {
|
||||
_logger.severe('collection magic data out of sync');
|
||||
sync().ignore();
|
||||
@@ -952,13 +952,13 @@ class CollectionsService {
|
||||
"enableJoin": true,
|
||||
},
|
||||
);
|
||||
collection.publicURLs?.add(PublicURL.fromMap(response.data["result"]));
|
||||
collection.publicURLs.add(PublicURL.fromMap(response.data["result"]));
|
||||
await _db.insert(List.from([collection]));
|
||||
_collectionIDToCollections[collection.id] = collection;
|
||||
Bus.instance.fire(
|
||||
CollectionUpdatedEvent(collection.id, <EnteFile>[], "shareUrL"),
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 402) {
|
||||
throw SharingNotPermittedForFreeAccountsError();
|
||||
}
|
||||
@@ -980,14 +980,14 @@ class CollectionsService {
|
||||
data: json.encode(prop),
|
||||
);
|
||||
// remove existing url information
|
||||
collection.publicURLs?.clear();
|
||||
collection.publicURLs?.add(PublicURL.fromMap(response.data["result"]));
|
||||
collection.publicURLs.clear();
|
||||
collection.publicURLs.add(PublicURL.fromMap(response.data["result"]));
|
||||
await _db.insert(List.from([collection]));
|
||||
_collectionIDToCollections[collection.id] = collection;
|
||||
Bus.instance.fire(
|
||||
CollectionUpdatedEvent(collection.id, <EnteFile>[], "updateUrl"),
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 402) {
|
||||
throw SharingNotPermittedForFreeAccountsError();
|
||||
}
|
||||
@@ -1003,7 +1003,7 @@ class CollectionsService {
|
||||
await _enteDio.delete(
|
||||
"/collections/share-url/" + collection.id.toString(),
|
||||
);
|
||||
collection.publicURLs?.clear();
|
||||
collection.publicURLs.clear();
|
||||
await _db.insert(List.from([collection]));
|
||||
_collectionIDToCollections[collection.id] = collection;
|
||||
Bus.instance.fire(
|
||||
@@ -1013,7 +1013,7 @@ class CollectionsService {
|
||||
"disableShareUrl",
|
||||
),
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
_logger.info(e);
|
||||
rethrow;
|
||||
}
|
||||
@@ -1038,7 +1038,7 @@ class CollectionsService {
|
||||
return collections;
|
||||
} catch (e, s) {
|
||||
_logger.warning(e, s);
|
||||
if (e is DioError && e.response?.statusCode == 401) {
|
||||
if (e is DioException && e.response?.statusCode == 401) {
|
||||
throw UnauthorizedError();
|
||||
}
|
||||
rethrow;
|
||||
@@ -1091,7 +1091,7 @@ class CollectionsService {
|
||||
} catch (e, s) {
|
||||
_logger.warning(e, s);
|
||||
_logger.severe("Failed to fetch public collection");
|
||||
if (e is DioError && e.response?.statusCode == 410) {
|
||||
if (e is DioException && e.response?.statusCode == 410) {
|
||||
await showInfoDialog(
|
||||
context,
|
||||
title: S.of(context).linkExpired,
|
||||
@@ -1100,7 +1100,7 @@ class CollectionsService {
|
||||
throw UnauthorizedError();
|
||||
}
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
if (e is DioError && e.response?.statusCode == 401) {
|
||||
if (e is DioException && e.response?.statusCode == 401) {
|
||||
throw UnauthorizedError();
|
||||
}
|
||||
rethrow;
|
||||
@@ -1300,7 +1300,7 @@ class CollectionsService {
|
||||
_cacheLocalPathAndCollection(collection);
|
||||
return collection;
|
||||
} catch (e) {
|
||||
if (e is DioError && e.response?.statusCode == 401) {
|
||||
if (e is DioException && e.response?.statusCode == 401) {
|
||||
throw UnauthorizedError();
|
||||
}
|
||||
_logger.severe('failed to fetch collection: $collectionID', e);
|
||||
|
||||
@@ -230,7 +230,7 @@ class FavoritesService {
|
||||
if (_cachedFavoritesCollectionID == null) {
|
||||
final collections = _collectionsService.getActiveCollections();
|
||||
for (final collection in collections) {
|
||||
if (collection.owner!.id == _config.getUserID() &&
|
||||
if (collection.owner.id == _config.getUserID() &&
|
||||
collection.type == CollectionType.favorites) {
|
||||
_cachedFavoritesCollectionID = collection.id;
|
||||
return collection;
|
||||
|
||||
@@ -117,7 +117,7 @@ class FileMagicService {
|
||||
// should be eventually synced after remote sync has completed
|
||||
await _filesDB.insertMultiple(files);
|
||||
RemoteSyncService.instance.sync(silently: true).ignore();
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null && e.response!.statusCode == 409) {
|
||||
RemoteSyncService.instance.sync(silently: true).ignore();
|
||||
}
|
||||
@@ -185,7 +185,7 @@ class FileMagicService {
|
||||
// update the state of the selected file. Same file in other collection
|
||||
// should be eventually synced after remote sync has completed
|
||||
RemoteSyncService.instance.sync(silently: true).ignore();
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null && e.response!.statusCode == 409) {
|
||||
RemoteSyncService.instance.sync(silently: true).ignore();
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ extension HiddenService on CollectionsService {
|
||||
final int userID = config.getUserID()!;
|
||||
final allDefaultHidden = collectionIDToCollections.values
|
||||
.where(
|
||||
(element) => element.isDefaultHidden() && element.owner!.id == userID,
|
||||
(element) => element.isDefaultHidden() && element.owner.id == userID,
|
||||
)
|
||||
.toList();
|
||||
|
||||
@@ -101,7 +101,7 @@ extension HiddenService on CollectionsService {
|
||||
collectionIDToCollections.values.firstWhereOrNull(
|
||||
(element) =>
|
||||
element.type == CollectionType.uncategorized &&
|
||||
element.owner!.id == userID,
|
||||
element.owner.id == userID,
|
||||
);
|
||||
if (matchedCollection != null) {
|
||||
cachedUncategorizedCollection = matchedCollection;
|
||||
@@ -166,7 +166,9 @@ extension HiddenService on CollectionsService {
|
||||
await dialog.hide();
|
||||
} on AssertionError catch (e) {
|
||||
await dialog.hide();
|
||||
unawaited(showErrorDialog(context, S.of(context).oops, e.message as String));
|
||||
unawaited(
|
||||
showErrorDialog(context, S.of(context).oops, e.message as String),
|
||||
);
|
||||
return false;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Could not hide", e, s);
|
||||
|
||||
@@ -193,6 +193,28 @@ class MLIndexingIsolate extends SuperIsolate {
|
||||
}
|
||||
}
|
||||
|
||||
/// WARNING: This method is only for debugging purposes. It should not be used in production.
|
||||
Future<void> debugLoadSingleModel(MLModels model) {
|
||||
return _initModelLock.synchronized(() async {
|
||||
final modelInstance = model.model;
|
||||
if (modelInstance.isInitialized) {
|
||||
_logger.info("Model ${model.name} already loaded");
|
||||
return;
|
||||
}
|
||||
final modelName = modelInstance.modelName;
|
||||
final modelPath = await modelInstance.downloadModelSafe();
|
||||
if (modelPath == null) {
|
||||
_logger.severe("Could not download model, no wifi");
|
||||
return;
|
||||
}
|
||||
final address = await runInIsolate(IsolateOperation.loadModel, {
|
||||
"modelName": modelName,
|
||||
"modelPath": modelPath,
|
||||
}) as int;
|
||||
modelInstance.storeSessionAddress(address);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cleanupLocalIndexingModels({bool delete = false}) async {
|
||||
if (!areModelsDownloaded) return;
|
||||
await _releaseModels();
|
||||
|
||||
@@ -135,22 +135,6 @@ class PreviewVideoStore {
|
||||
}
|
||||
}
|
||||
|
||||
final fileSize = file.lengthSync();
|
||||
FFProbeProps? props;
|
||||
|
||||
if (fileSize <= 10 * 1024 * 1024) {
|
||||
props = await getVideoPropsAsync(file);
|
||||
final videoData = List.from(props?.propData?["streams"] ?? [])
|
||||
.firstWhereOrNull((e) => e["type"] == "video");
|
||||
|
||||
final codec = videoData["codec_name"]?.toString().toLowerCase();
|
||||
final codecIsH264 = codec?.contains("h264") ?? false;
|
||||
if (codecIsH264) {
|
||||
_items.removeWhere((key, value) => value.file == enteFile);
|
||||
Bus.instance.fire(PreviewUpdatedEvent(_items));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (uploadingFileId >= 0) {
|
||||
_items[enteFile.uploadedFileID!] = PreviewItem(
|
||||
status: PreviewItemStatus.inQueue,
|
||||
@@ -174,7 +158,8 @@ class PreviewVideoStore {
|
||||
);
|
||||
Bus.instance.fire(PreviewUpdatedEvent(_items));
|
||||
|
||||
props ??= await getVideoPropsAsync(file);
|
||||
final props = await getVideoPropsAsync(file);
|
||||
final fileSize = enteFile.fileSize ?? file.lengthSync();
|
||||
|
||||
final videoData = List.from(props?.propData?["streams"] ?? [])
|
||||
.firstWhereOrNull((e) => e["type"] == "video");
|
||||
@@ -207,13 +192,11 @@ class PreviewVideoStore {
|
||||
final codecIsH264 = codec?.contains("h264") ?? false;
|
||||
|
||||
if (bitrate != null && bitrate <= 4000 * 1000 && codecIsH264) {
|
||||
// create playlist without compression, as is
|
||||
session = await FFmpegKit.execute(
|
||||
'-i "${file.path}" '
|
||||
'-metadata:s:v:0 rotate=0 ' // Adjust metadata if needed
|
||||
'-c:v copy ' // Copy the original video codec
|
||||
'-c:a copy ' // Copy the original audio codec
|
||||
'-f hls -hls_time 10 -hls_flags single_file '
|
||||
'-metadata:s:v:0 rotate=0 '
|
||||
'-c:v copy -c:a copy '
|
||||
'-f hls -hls_time 2 -hls_flags single_file '
|
||||
'-hls_list_size 0 -hls_key_info_file ${keyinfo.path} '
|
||||
'$prefix/output.m3u8',
|
||||
);
|
||||
@@ -221,16 +204,14 @@ class PreviewVideoStore {
|
||||
codec != null &&
|
||||
bitrate <= 2000 * 1000 &&
|
||||
!codecIsH264) {
|
||||
// compress video with crf=21, h264 no change in resolution or frame rate,
|
||||
// just change color scheme
|
||||
session = await FFmpegKit.execute(
|
||||
'-i "${file.path}" '
|
||||
'-metadata:s:v:0 rotate=0 ' // Keep rotation metadata
|
||||
'-vf "format=yuv420p10le,zscale=transfer=linear,tonemap=tonemap=hable:desat=0:peak=10,zscale=transfer=bt709:matrix=bt709:primaries=bt709,format=yuv420p" ' // Adjust color scheme
|
||||
'-color_primaries bt709 -color_trc bt709 -colorspace bt709 ' // Set color profile to BT.709
|
||||
'-c:v libx264 -crf 21 -preset medium ' // Compress with CRF=21 using H.264
|
||||
'-c:a copy ' // Keep original audio
|
||||
'-f hls -hls_time 10 -hls_flags single_file '
|
||||
'-metadata:s:v:0 rotate=0 '
|
||||
'-vf "format=yuv420p10le,zscale=transfer=linear,tonemap=tonemap=hable:desat=0:peak=10,zscale=transfer=bt709:matrix=bt709:primaries=bt709,format=yuv420p" '
|
||||
'-color_primaries bt709 -color_trc bt709 -colorspace bt709 '
|
||||
'-c:v libx264 -crf 23 -preset medium '
|
||||
'-c:a copy '
|
||||
'-f hls -hls_time 2 -hls_flags single_file '
|
||||
'-hls_list_size 0 -hls_key_info_file ${keyinfo.path} '
|
||||
'$prefix/output.m3u8',
|
||||
);
|
||||
@@ -241,8 +222,8 @@ class PreviewVideoStore {
|
||||
'-i "${file.path}" '
|
||||
'-metadata:s:v:0 rotate=0 '
|
||||
'-vf "scale=-2:720,fps=30" '
|
||||
'-c:v libx264 -b:v 2000k -preset medium '
|
||||
'-c:a aac -b:a 128k -f hls -hls_time 10 -hls_flags single_file '
|
||||
'-c:v libx264 -b:v 2000k -crf 23 -preset medium '
|
||||
'-c:a aac -b:a 128k -f hls -hls_time 2 -hls_flags single_file '
|
||||
'-hls_list_size 0 -hls_key_info_file ${keyinfo.path} '
|
||||
'$prefix/output.m3u8',
|
||||
);
|
||||
@@ -254,8 +235,8 @@ class PreviewVideoStore {
|
||||
'-vf "scale=-2:720,fps=30,format=yuv420p10le,zscale=transfer=linear,tonemap=tonemap=hable:desat=0:peak=10,zscale=transfer=bt709:matrix=bt709:primaries=bt709,format=yuv420p" '
|
||||
'-color_primaries bt709 -color_trc bt709 -colorspace bt709 '
|
||||
'-x264-params "colorprim=bt709:transfer=bt709:colormatrix=bt709" '
|
||||
'-c:v libx264 -b:v 2000k -preset medium '
|
||||
'-c:a aac -b:a 128k -f hls -hls_time 10 -hls_flags single_file '
|
||||
'-c:v libx264 -b:v 2000k -crf 23 -preset medium '
|
||||
'-c:a aac -b:a 128k -f hls -hls_time 2 -hls_flags single_file '
|
||||
'-hls_list_size 0 -hls_key_info_file ${keyinfo.path} '
|
||||
'$prefix/output.m3u8',
|
||||
);
|
||||
@@ -601,21 +582,49 @@ class PreviewVideoStore {
|
||||
final files = await FilesDB.instance.getAllFilesAfterDate(
|
||||
fileType: FileType.video,
|
||||
beginDate: cutoff,
|
||||
userID: Configuration.instance.getUserID()!,
|
||||
);
|
||||
|
||||
final previewIds = FileDataService.instance.previewIds;
|
||||
final allFiles = files
|
||||
.where((file) => previewIds?[file.uploadedFileID] == null)
|
||||
.toList();
|
||||
.sorted((a, b) {
|
||||
// put higher duration videos last
|
||||
final first = a.duration == null || a.duration! >= 10 * 60 ? 1 : 0;
|
||||
final second = b.duration == null || b.duration! >= 10 * 60 ? 1 : 0;
|
||||
return first.compareTo(second);
|
||||
}).toList();
|
||||
|
||||
// set all video status to be in queue
|
||||
for (final file in allFiles) {
|
||||
_items[file.uploadedFileID!] = PreviewItem(
|
||||
for (final enteFile in allFiles) {
|
||||
final fileSize = enteFile.fileSize;
|
||||
FFProbeProps? props;
|
||||
|
||||
if (fileSize != null && fileSize <= 10 * 1024 * 1024) {
|
||||
final file = await getFile(enteFile, isOrigin: true);
|
||||
if (file != null) {
|
||||
props = await getVideoPropsAsync(file);
|
||||
final videoData = List.from(props?.propData?["streams"] ?? [])
|
||||
.firstWhereOrNull((e) => e["type"] == "video");
|
||||
|
||||
final codec = videoData["codec_name"]?.toString().toLowerCase();
|
||||
final codecIsH264 = codec?.contains("h264") ?? false;
|
||||
|
||||
if (codecIsH264) {
|
||||
_items.removeWhere((key, value) => value.file == enteFile);
|
||||
Bus.instance.fire(PreviewUpdatedEvent(_items));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_items[enteFile.uploadedFileID!] = PreviewItem(
|
||||
status: PreviewItemStatus.inQueue,
|
||||
file: file,
|
||||
collectionID: file.collectionID ?? 0,
|
||||
file: enteFile,
|
||||
collectionID: enteFile.collectionID ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Bus.instance.fire(PreviewUpdatedEvent(_items));
|
||||
|
||||
final file = allFiles.first;
|
||||
|
||||
@@ -15,6 +15,7 @@ import "package:photos/db/ml/db.dart";
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/extensions/user_extension.dart";
|
||||
import "package:photos/models/api/collection/user.dart";
|
||||
import "package:photos/models/base_location.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import 'package:photos/models/collection/collection_items.dart';
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
@@ -35,6 +36,7 @@ import "package:photos/models/search/hierarchical/magic_filter.dart";
|
||||
import "package:photos/models/search/hierarchical/top_level_generic_filter.dart";
|
||||
import "package:photos/models/search/search_constants.dart";
|
||||
import "package:photos/models/search/search_types.dart";
|
||||
import "package:photos/models/trip_memory.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/filter/db_filters.dart";
|
||||
@@ -1189,11 +1191,413 @@ class SearchService {
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> getTripsResults(
|
||||
BuildContext context,
|
||||
int? limit,
|
||||
) async {
|
||||
final List<GenericSearchResult> searchResults = [];
|
||||
final allFiles = await getAllFilesForSearch();
|
||||
final Iterable<LocalEntity<LocationTag>> locationTagEntities =
|
||||
(await locationService.getLocationTags());
|
||||
if (allFiles.isEmpty) return [];
|
||||
final currentTime = DateTime.now().toLocal();
|
||||
final currentMonth = currentTime.month;
|
||||
final cutOffTime = currentTime.subtract(const Duration(days: 365));
|
||||
|
||||
final Map<LocalEntity<LocationTag>, List<EnteFile>> tagToItemsMap = {};
|
||||
for (int i = 0; i < locationTagEntities.length; i++) {
|
||||
tagToItemsMap[locationTagEntities.elementAt(i)] = [];
|
||||
}
|
||||
final List<(List<EnteFile>, Location)> smallRadiusClusters = [];
|
||||
final List<(List<EnteFile>, Location)> wideRadiusClusters = [];
|
||||
// Go through all files and cluster the ones not inside any location tag
|
||||
for (EnteFile file in allFiles) {
|
||||
if (!file.hasLocation ||
|
||||
file.uploadedFileID == null ||
|
||||
!file.isOwner ||
|
||||
file.creationTime == null) {
|
||||
continue;
|
||||
}
|
||||
// Check if the file is inside any location tag
|
||||
bool hasLocationTag = false;
|
||||
for (LocalEntity<LocationTag> tag in tagToItemsMap.keys) {
|
||||
if (isFileInsideLocationTag(
|
||||
tag.item.centerPoint,
|
||||
file.location!,
|
||||
tag.item.radius,
|
||||
)) {
|
||||
hasLocationTag = true;
|
||||
tagToItemsMap[tag]!.add(file);
|
||||
}
|
||||
}
|
||||
// Cluster the files not inside any location tag (incremental clustering)
|
||||
if (!hasLocationTag) {
|
||||
// Small radius clustering for base locations
|
||||
bool foundSmallCluster = false;
|
||||
for (final cluster in smallRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
0.6,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
foundSmallCluster = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundSmallCluster) {
|
||||
smallRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
// Wide radius clustering for trip locations
|
||||
bool foundWideCluster = false;
|
||||
for (final cluster in wideRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
100.0,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
foundWideCluster = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundWideCluster) {
|
||||
wideRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Identify base locations
|
||||
final List<BaseLocation> baseLocations = [];
|
||||
for (final cluster in smallRadiusClusters) {
|
||||
final files = cluster.$1;
|
||||
final location = cluster.$2;
|
||||
// Check that the photos are distributed over a longer time range (3+ months)
|
||||
final creationTimes = <int>[];
|
||||
final Set<int> uniqueDays = {};
|
||||
for (final file in files) {
|
||||
creationTimes.add(file.creationTime!);
|
||||
final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!);
|
||||
final dayStamp =
|
||||
DateTime(date.year, date.month, date.day).microsecondsSinceEpoch;
|
||||
uniqueDays.add(dayStamp);
|
||||
}
|
||||
creationTimes.sort();
|
||||
if (creationTimes.length < 10) continue;
|
||||
final firstCreationTime = DateTime.fromMicrosecondsSinceEpoch(
|
||||
creationTimes.first,
|
||||
);
|
||||
final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch(
|
||||
creationTimes.last,
|
||||
);
|
||||
if (lastCreationTime.difference(firstCreationTime).inDays < 90) {
|
||||
continue;
|
||||
}
|
||||
// Check for a minimum average number of days photos are clicked in range
|
||||
final daysRange = lastCreationTime.difference(firstCreationTime).inDays;
|
||||
if (uniqueDays.length < daysRange * 0.1) continue;
|
||||
// Check if it's a current or old base location
|
||||
final bool isCurrent = lastCreationTime.isAfter(
|
||||
DateTime.now().subtract(
|
||||
const Duration(days: 90),
|
||||
),
|
||||
);
|
||||
baseLocations.add(BaseLocation(files, location, isCurrent));
|
||||
}
|
||||
|
||||
// Identify trip locations
|
||||
final List<TripMemory> tripLocations = [];
|
||||
clusteredLocations:
|
||||
for (final cluster in wideRadiusClusters) {
|
||||
final files = cluster.$1;
|
||||
final location = cluster.$2;
|
||||
// Check that it's at least 10km away from any base or tag location
|
||||
bool tooClose = false;
|
||||
for (final baseLocation in baseLocations) {
|
||||
if (isFileInsideLocationTag(
|
||||
baseLocation.location,
|
||||
location,
|
||||
10.0,
|
||||
)) {
|
||||
tooClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (final tag in tagToItemsMap.keys) {
|
||||
if (isFileInsideLocationTag(
|
||||
tag.item.centerPoint,
|
||||
location,
|
||||
10.0,
|
||||
)) {
|
||||
tooClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tooClose) continue clusteredLocations;
|
||||
|
||||
// Check that the photos are distributed over a short time range (2-30 days) or multiple short time ranges only
|
||||
files.sort((a, b) => a.creationTime!.compareTo(b.creationTime!));
|
||||
// Find distinct time blocks (potential trips)
|
||||
List<EnteFile> currentBlockFiles = [files.first];
|
||||
int blockStart = files.first.creationTime!;
|
||||
int lastTime = files.first.creationTime!;
|
||||
DateTime lastDateTime = DateTime.fromMicrosecondsSinceEpoch(lastTime);
|
||||
|
||||
for (int i = 1; i < files.length; i++) {
|
||||
final currentFile = files[i];
|
||||
final currentTime = currentFile.creationTime!;
|
||||
final gap = DateTime.fromMicrosecondsSinceEpoch(currentTime)
|
||||
.difference(lastDateTime)
|
||||
.inDays;
|
||||
|
||||
// If gap is too large, end current block and check if it's a valid trip
|
||||
if (gap > 15) {
|
||||
// 10 days gap to separate trips. If gap is small, it's likely not a trip
|
||||
if (gap < 90) continue clusteredLocations;
|
||||
|
||||
final blockDuration = lastDateTime
|
||||
.difference(DateTime.fromMicrosecondsSinceEpoch(blockStart))
|
||||
.inDays;
|
||||
|
||||
// Check if current block is a valid trip (2-30 days)
|
||||
if (blockDuration >= 2 && blockDuration <= 30) {
|
||||
tripLocations.add(
|
||||
TripMemory(
|
||||
List.from(currentBlockFiles),
|
||||
location,
|
||||
blockStart,
|
||||
lastTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Start new block
|
||||
currentBlockFiles = [];
|
||||
blockStart = currentTime;
|
||||
}
|
||||
|
||||
currentBlockFiles.add(currentFile);
|
||||
lastTime = currentTime;
|
||||
lastDateTime = DateTime.fromMicrosecondsSinceEpoch(lastTime);
|
||||
}
|
||||
// Check final block
|
||||
final lastBlockDuration = lastDateTime
|
||||
.difference(DateTime.fromMicrosecondsSinceEpoch(blockStart))
|
||||
.inDays;
|
||||
if (lastBlockDuration >= 2 && lastBlockDuration <= 30) {
|
||||
tripLocations.add(
|
||||
TripMemory(
|
||||
List.from(currentBlockFiles),
|
||||
location,
|
||||
blockStart,
|
||||
lastTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any trip locations should be merged
|
||||
final List<TripMemory> mergedTrips = [];
|
||||
for (final trip in tripLocations) {
|
||||
final tripFirstTime = DateTime.fromMicrosecondsSinceEpoch(
|
||||
trip.firstCreationTime,
|
||||
);
|
||||
final tripLastTime = DateTime.fromMicrosecondsSinceEpoch(
|
||||
trip.lastCreationTime,
|
||||
);
|
||||
bool merged = false;
|
||||
for (int idx = 0; idx < mergedTrips.length; idx++) {
|
||||
final otherTrip = mergedTrips[idx];
|
||||
final otherTripFirstTime =
|
||||
DateTime.fromMicrosecondsSinceEpoch(otherTrip.firstCreationTime);
|
||||
final otherTripLastTime =
|
||||
DateTime.fromMicrosecondsSinceEpoch(otherTrip.lastCreationTime);
|
||||
if (tripFirstTime
|
||||
.isBefore(otherTripLastTime.add(const Duration(days: 3))) &&
|
||||
tripLastTime.isAfter(
|
||||
otherTripFirstTime.subtract(const Duration(days: 3)),
|
||||
)) {
|
||||
mergedTrips[idx] = TripMemory(
|
||||
otherTrip.files + trip.files,
|
||||
otherTrip.location,
|
||||
min(otherTrip.firstCreationTime, trip.firstCreationTime),
|
||||
max(otherTrip.lastCreationTime, trip.lastCreationTime),
|
||||
);
|
||||
_logger.finest('Merged two trip locations');
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (merged) continue;
|
||||
mergedTrips.add(
|
||||
TripMemory(
|
||||
trip.files,
|
||||
trip.location,
|
||||
trip.firstCreationTime,
|
||||
trip.lastCreationTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Remove too small and too recent trips
|
||||
final List<TripMemory> validTrips = [];
|
||||
for (final trip in mergedTrips) {
|
||||
if (trip.files.length >= 20 &&
|
||||
trip.averageCreationTime < cutOffTime.microsecondsSinceEpoch) {
|
||||
validTrips.add(trip);
|
||||
}
|
||||
}
|
||||
|
||||
// For now for testing let's just surface all base locations
|
||||
for (final baseLocation in baseLocations) {
|
||||
String name = "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
final String? locationName = await _tryFindLocationName(
|
||||
baseLocation.files,
|
||||
base: true,
|
||||
);
|
||||
if (locationName != null) {
|
||||
name =
|
||||
"$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
}
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.event,
|
||||
name,
|
||||
baseLocation.files,
|
||||
hierarchicalSearchFilter: TopLevelGenericFilter(
|
||||
filterName: name,
|
||||
occurrence: kMostRelevantFilter,
|
||||
filterResultType: ResultType.event,
|
||||
matchedUploadedIDs: filesToUploadedFileIDs(baseLocation.files),
|
||||
filterIcon: Icons.event_outlined,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// For now we surface the two most recent trips of current month, and if none, the earliest upcoming redundant trip
|
||||
// Group the trips per month and then year
|
||||
final Map<int, Map<int, List<TripMemory>>> tripsByMonthYear = {};
|
||||
for (final trip in validTrips) {
|
||||
final tripDate =
|
||||
DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime);
|
||||
tripsByMonthYear
|
||||
.putIfAbsent(tripDate.month, () => {})
|
||||
.putIfAbsent(tripDate.year, () => [])
|
||||
.add(trip);
|
||||
}
|
||||
|
||||
// Flatten trips for the current month and annotate with their average date.
|
||||
final List<TripMemory> currentMonthTrips = [];
|
||||
if (tripsByMonthYear.containsKey(currentMonth)) {
|
||||
for (final trips in tripsByMonthYear[currentMonth]!.values) {
|
||||
for (final trip in trips) {
|
||||
currentMonthTrips.add(trip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are past trips this month, show the one or two most recent ones.
|
||||
if (currentMonthTrips.isNotEmpty) {
|
||||
currentMonthTrips.sort(
|
||||
(a, b) => b.averageCreationTime.compareTo(a.averageCreationTime),
|
||||
);
|
||||
final tripsToShow = currentMonthTrips.take(2);
|
||||
for (final trip in tripsToShow) {
|
||||
final year =
|
||||
DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime).year;
|
||||
final String? locationName = await _tryFindLocationName(trip.files);
|
||||
String name = "Trip in $year";
|
||||
if (locationName != null) {
|
||||
name = "Trip to $locationName";
|
||||
} else if (year == currentTime.year - 1) {
|
||||
name = "Last year's trip";
|
||||
}
|
||||
final photoSelection = await _bestSelection(trip.files);
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.event,
|
||||
name,
|
||||
photoSelection,
|
||||
hierarchicalSearchFilter: TopLevelGenericFilter(
|
||||
filterName: name,
|
||||
occurrence: kMostRelevantFilter,
|
||||
filterResultType: ResultType.event,
|
||||
matchedUploadedIDs: filesToUploadedFileIDs(photoSelection),
|
||||
filterIcon: Icons.event_outlined,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (limit != null && searchResults.length >= limit) {
|
||||
return searchResults;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise, if no trips happened in the current month,
|
||||
// look for the earliest upcoming trip in another month that has 3+ trips.
|
||||
else {
|
||||
// TODO lau: make sure the same upcoming trip isn't shown multiple times over multiple months
|
||||
final sortedUpcomingMonths =
|
||||
List<int>.generate(12, (i) => ((currentMonth + i) % 12) + 1);
|
||||
checkUpcomingMonths:
|
||||
for (final month in sortedUpcomingMonths) {
|
||||
if (tripsByMonthYear.containsKey(month)) {
|
||||
final List<TripMemory> thatMonthTrips = [];
|
||||
for (final trips in tripsByMonthYear[month]!.values) {
|
||||
for (final trip in trips) {
|
||||
thatMonthTrips.add(trip);
|
||||
}
|
||||
}
|
||||
if (thatMonthTrips.length >= 3) {
|
||||
// take and use the third earliest trip
|
||||
thatMonthTrips.sort(
|
||||
(a, b) => a.averageCreationTime.compareTo(b.averageCreationTime),
|
||||
);
|
||||
final trip = thatMonthTrips[2];
|
||||
final year =
|
||||
DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime)
|
||||
.year;
|
||||
final String? locationName = await _tryFindLocationName(trip.files);
|
||||
String name = "Trip in $year";
|
||||
if (locationName != null) {
|
||||
name = "Trip to $locationName";
|
||||
} else if (year == currentTime.year - 1) {
|
||||
name = "Last year's trip";
|
||||
}
|
||||
final photoSelection = await _bestSelection(trip.files);
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.event,
|
||||
name,
|
||||
photoSelection,
|
||||
hierarchicalSearchFilter: TopLevelGenericFilter(
|
||||
filterName: name,
|
||||
occurrence: kMostRelevantFilter,
|
||||
filterResultType: ResultType.event,
|
||||
matchedUploadedIDs: filesToUploadedFileIDs(photoSelection),
|
||||
filterIcon: Icons.event_outlined,
|
||||
),
|
||||
),
|
||||
);
|
||||
break checkUpcomingMonths;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
Future<List<GenericSearchResult>> onThisDayOrWeekResults(
|
||||
BuildContext context,
|
||||
int? limit,
|
||||
) async {
|
||||
final List<GenericSearchResult> searchResults = [];
|
||||
final trips = await getTripsResults(context, limit);
|
||||
if (trips.isNotEmpty) {
|
||||
searchResults.addAll(trips);
|
||||
}
|
||||
final allFiles = await getAllFilesForSearch();
|
||||
if (allFiles.isEmpty) return [];
|
||||
|
||||
@@ -1455,6 +1859,26 @@ class SearchService {
|
||||
return ((dayOfYear - 1) ~/ 7) + 1;
|
||||
}
|
||||
|
||||
Future<String?> _tryFindLocationName(
|
||||
List<EnteFile> files, {
|
||||
bool base = false,
|
||||
}) async {
|
||||
final results = await locationService.getFilesInCity(files, '');
|
||||
final List<City> sortedByResultCount = results.keys.toList()
|
||||
..sort((a, b) => results[b]!.length.compareTo(results[a]!.length));
|
||||
if (sortedByResultCount.isEmpty) return null;
|
||||
final biggestPlace = sortedByResultCount.first;
|
||||
if (results[biggestPlace]!.length > files.length / 2) {
|
||||
return biggestPlace.city;
|
||||
}
|
||||
if (results.length > 2 &&
|
||||
results.keys.map((city) => city.country).toSet().length == 1 &&
|
||||
!base) {
|
||||
return biggestPlace.country;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the best selection of files from the given list.
|
||||
/// Makes sure that the selection is not more than [prefferedSize] or 10 files,
|
||||
/// and that each year of the original list is represented.
|
||||
|
||||
@@ -133,11 +133,11 @@ class SyncService {
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e is DioError) {
|
||||
if (e.type == DioErrorType.connectTimeout ||
|
||||
e.type == DioErrorType.sendTimeout ||
|
||||
e.type == DioErrorType.receiveTimeout ||
|
||||
e.type == DioErrorType.other) {
|
||||
if (e is DioException) {
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.sendTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.unknown) {
|
||||
Bus.instance.fire(
|
||||
SyncStatusUpdate(
|
||||
SyncStatus.paused,
|
||||
|
||||
@@ -124,7 +124,7 @@ class UserService {
|
||||
} else {
|
||||
throw Exception("send-ott action failed, non-200");
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.info(e);
|
||||
final String? enteErrCode = e.response?.data["code"];
|
||||
@@ -185,7 +185,7 @@ class UserService {
|
||||
);
|
||||
final publicKey = response.data["publicKey"];
|
||||
return publicKey;
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null && e.response?.statusCode == 404) {
|
||||
return null;
|
||||
}
|
||||
@@ -221,7 +221,7 @@ class UserService {
|
||||
}
|
||||
}
|
||||
return userDetails;
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
_logger.info(e);
|
||||
rethrow;
|
||||
}
|
||||
@@ -231,7 +231,7 @@ class UserService {
|
||||
try {
|
||||
final response = await _enteDio.get("/users/sessions");
|
||||
return Sessions.fromMap(response.data);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
_logger.info(e);
|
||||
rethrow;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ class UserService {
|
||||
"token": token,
|
||||
},
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
_logger.info(e);
|
||||
rethrow;
|
||||
}
|
||||
@@ -254,7 +254,7 @@ class UserService {
|
||||
Future<void> leaveFamilyPlan() async {
|
||||
try {
|
||||
await _enteDio.delete("/family/leave");
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
_logger.warning('failed to leave family plan', e);
|
||||
rethrow;
|
||||
}
|
||||
@@ -271,7 +271,7 @@ class UserService {
|
||||
}
|
||||
} catch (e) {
|
||||
// check if token is already invalid
|
||||
if (e is DioError && e.response?.statusCode == 401) {
|
||||
if (e is DioException && e.response?.statusCode == 401) {
|
||||
await Configuration.instance.logout();
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
return;
|
||||
@@ -342,7 +342,7 @@ class UserService {
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null) {
|
||||
if (e.response!.statusCode == 404 || e.response!.statusCode == 410) {
|
||||
throw PassKeySessionExpiredError();
|
||||
@@ -460,7 +460,7 @@ class UserService {
|
||||
// should never reach here
|
||||
throw Exception("unexpected response during email verification");
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
_logger.info(e);
|
||||
await dialog.hide();
|
||||
if (e.response != null && e.response!.statusCode == 410) {
|
||||
@@ -532,7 +532,7 @@ class UserService {
|
||||
S.of(context).oops,
|
||||
S.of(context).verificationFailedPleaseTryAgain,
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
await dialog.hide();
|
||||
if (e.response != null && e.response!.statusCode == 403) {
|
||||
// ignore: unawaited_futures
|
||||
@@ -592,7 +592,7 @@ class UserService {
|
||||
} else {
|
||||
throw Exception("get-srp-attributes action failed");
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null && e.response!.statusCode == 404) {
|
||||
throw SrpSetupNotCompleteError();
|
||||
}
|
||||
@@ -865,7 +865,7 @@ class UserService {
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response!.statusCode == 404) {
|
||||
@@ -932,7 +932,7 @@ class UserService {
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe('error while recovery 2fa', e);
|
||||
if (e.response != null && e.response!.statusCode == 404) {
|
||||
@@ -1031,7 +1031,7 @@ class UserService {
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe("error during recovery", e);
|
||||
if (e.response != null && e.response!.statusCode == 404) {
|
||||
@@ -1126,7 +1126,7 @@ class UserService {
|
||||
} catch (e, s) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e, s);
|
||||
if (e is DioError) {
|
||||
if (e is DioException) {
|
||||
if (e.response != null && e.response!.statusCode == 401) {
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
@@ -1311,34 +1311,30 @@ class UserService {
|
||||
|
||||
for (final c in CollectionsService.instance.getActiveCollections()) {
|
||||
// Add collaborators and viewers of collections owned by user
|
||||
if (c.owner?.id == ownerID) {
|
||||
for (final User? u in c.sharees ?? []) {
|
||||
if (u != null && u.id != null && u.email.isNotEmpty) {
|
||||
if (c.owner.id == ownerID) {
|
||||
for (final User u in c.sharees) {
|
||||
if (u.id != null && u.email.isNotEmpty) {
|
||||
if (!existingEmails.contains(u.email)) {
|
||||
relevantUsers.add(u);
|
||||
existingEmails.add(u.email);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (c.owner?.id != null && c.owner!.email.isNotEmpty) {
|
||||
} else if (c.owner.id != null && c.owner.email.isNotEmpty) {
|
||||
// Add owners of collections shared with user
|
||||
if (!existingEmails.contains(c.owner!.email)) {
|
||||
relevantUsers.add(c.owner!);
|
||||
existingEmails.add(c.owner!.email);
|
||||
if (!existingEmails.contains(c.owner.email)) {
|
||||
relevantUsers.add(c.owner);
|
||||
existingEmails.add(c.owner.email);
|
||||
}
|
||||
// Add collaborators of collections shared with user where user is a
|
||||
// viewer or a collaborator
|
||||
for (final User? u in c.sharees ?? []) {
|
||||
if (u != null &&
|
||||
u.id != null &&
|
||||
for (final User u in c.sharees) {
|
||||
if (u.id != null &&
|
||||
u.email.isNotEmpty &&
|
||||
u.email == ownerEmail &&
|
||||
(u.isCollaborator || u.isViewer)) {
|
||||
for (final User? u in c.sharees ?? []) {
|
||||
if (u != null &&
|
||||
u.id != null &&
|
||||
u.email.isNotEmpty &&
|
||||
u.isCollaborator) {
|
||||
for (final User u in c.sharees) {
|
||||
if (u.id != null && u.email.isNotEmpty && u.isCollaborator) {
|
||||
if (!existingEmails.contains(u.email)) {
|
||||
relevantUsers.add(u);
|
||||
existingEmails.add(u.email);
|
||||
@@ -1392,32 +1388,28 @@ class UserService {
|
||||
|
||||
for (final c in CollectionsService.instance.getActiveCollections()) {
|
||||
// Add collaborators and viewers of collections owned by user
|
||||
if (c.owner?.id == ownerID) {
|
||||
for (final User? u in c.sharees ?? []) {
|
||||
if (u != null && u.id != null && u.email.isNotEmpty) {
|
||||
if (c.owner.id == ownerID) {
|
||||
for (final User u in c.sharees) {
|
||||
if (u.id != null && u.email.isNotEmpty) {
|
||||
if (!emailIDs.contains(u.email)) {
|
||||
emailIDs.add(u.email);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (c.owner?.id != null && c.owner!.email.isNotEmpty) {
|
||||
} else if (c.owner.id != null && c.owner.email.isNotEmpty) {
|
||||
// Add owners of collections shared with user
|
||||
if (!emailIDs.contains(c.owner!.email)) {
|
||||
emailIDs.add(c.owner!.email);
|
||||
if (!emailIDs.contains(c.owner.email)) {
|
||||
emailIDs.add(c.owner.email);
|
||||
}
|
||||
// Add collaborators of collections shared with user where user is a
|
||||
// viewer or a collaborator
|
||||
for (final User? u in c.sharees ?? []) {
|
||||
if (u != null &&
|
||||
u.id != null &&
|
||||
for (final User u in c.sharees) {
|
||||
if (u.id != null &&
|
||||
u.email.isNotEmpty &&
|
||||
u.email == ownerEmail &&
|
||||
(u.isCollaborator || u.isViewer)) {
|
||||
for (final User? u in c.sharees ?? []) {
|
||||
if (u != null &&
|
||||
u.id != null &&
|
||||
u.email.isNotEmpty &&
|
||||
u.isCollaborator) {
|
||||
for (final User u in c.sharees) {
|
||||
if (u.id != null && u.email.isNotEmpty && u.isCollaborator) {
|
||||
if (!emailIDs.contains(u.email)) {
|
||||
emailIDs.add(u.email);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ class _LoginPasswordVerificationPageState
|
||||
password,
|
||||
dialog,
|
||||
);
|
||||
} on DioError catch (e, s) {
|
||||
} on DioException catch (e, s) {
|
||||
await dialog.hide();
|
||||
if (e.response != null && e.response!.statusCode == 401) {
|
||||
_logger.severe('server reject, failed verify SRP login', e, s);
|
||||
@@ -123,8 +123,10 @@ class _LoginPasswordVerificationPageState
|
||||
S.of(context).pleaseTryAgain,
|
||||
);
|
||||
} else {
|
||||
_logger.severe('API failure during SRP login', e, s);
|
||||
if (e.type == DioErrorType.other) {
|
||||
_logger.severe('API failure during SRP login ${e.type}', e, s);
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.sendTimeout) {
|
||||
await _showContactSupportDialog(
|
||||
context,
|
||||
S.of(context).noInternetConnection,
|
||||
|
||||
@@ -43,7 +43,7 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
|
||||
await userRemoteFlagService.markRecoveryVerificationAsDone();
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
if (e is DioError && e.type == DioErrorType.other) {
|
||||
if (e is DioException && e.type == DioExceptionType.connectionError) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
S.of(context).noInternetConnection,
|
||||
|
||||
@@ -147,7 +147,7 @@ extension CollectionFileActions on CollectionActions {
|
||||
// Newly created collection might not be cached
|
||||
final Collection? c =
|
||||
CollectionsService.instance.getCollectionByID(collectionID);
|
||||
if (c != null && c.owner!.id != currentUserID) {
|
||||
if (c != null && c.owner.id != currentUserID) {
|
||||
if (!showProgressDialog) {
|
||||
dialog = createProgressDialog(
|
||||
context,
|
||||
|
||||
@@ -340,7 +340,7 @@ class CollectionActions {
|
||||
) async {
|
||||
final textTheme = getEnteTextTheme(bContext);
|
||||
final currentUserID = Configuration.instance.getUserID()!;
|
||||
if (collection.owner!.id != currentUserID) {
|
||||
if (collection.owner.id != currentUserID) {
|
||||
throw AssertionError("Can not delete album owned by others");
|
||||
}
|
||||
if (collection.hasSharees) {
|
||||
@@ -495,7 +495,7 @@ class CollectionActions {
|
||||
bool isHidden = false,
|
||||
}) async {
|
||||
final int currentUserID = Configuration.instance.getUserID()!;
|
||||
final isCollectionOwner = collection.owner!.id == currentUserID;
|
||||
final isCollectionOwner = collection.owner.id == currentUserID;
|
||||
final FilesSplit split = FilesSplit.split(
|
||||
files,
|
||||
Configuration.instance.getUserID()!,
|
||||
@@ -631,7 +631,7 @@ class CollectionActions {
|
||||
if (targetCollection == null ||
|
||||
(CollectionType.uncategorized == targetCollection.type ||
|
||||
targetCollection.type == CollectionType.favorites) ||
|
||||
targetCollection.owner!.id != userID) {
|
||||
targetCollection.owner.id != userID) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -41,7 +41,7 @@ class AlbumRowItemWidget extends StatelessWidget {
|
||||
final Widget? linkIcon = c.hasLink && isOwner
|
||||
? Icon(
|
||||
Icons.link,
|
||||
color: c.publicURLs!.first!.isExpired ? warning500 : strokeBaseDark,
|
||||
color: c.publicURLs.first.isExpired ? warning500 : strokeBaseDark,
|
||||
)
|
||||
: null;
|
||||
return GestureDetector(
|
||||
@@ -115,7 +115,7 @@ class AlbumRowItemWidget extends StatelessWidget {
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: UserAvatarWidget(
|
||||
c.owner!,
|
||||
c.owner,
|
||||
thumbnailView: true,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -287,8 +287,8 @@ class AlbumVerticalListWidget extends StatelessWidget {
|
||||
CollectionActions(CollectionsService.instance);
|
||||
|
||||
if (collection.hasLink) {
|
||||
if (collection.publicURLs!.first!.enableCollect) {
|
||||
if (Configuration.instance.getUserID() == collection.owner!.id) {
|
||||
if (collection.publicURLs.first.enableCollect) {
|
||||
if (Configuration.instance.getUserID() == collection.owner.id) {
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
@@ -334,7 +334,7 @@ class AlbumVerticalListWidget extends StatelessWidget {
|
||||
context,
|
||||
S.of(context).collaborativeLinkCreatedFor(collection.displayName),
|
||||
);
|
||||
if (Configuration.instance.getUserID() == collection.owner!.id) {
|
||||
if (Configuration.instance.getUserID() == collection.owner.id) {
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
@@ -353,7 +353,7 @@ class AlbumVerticalListWidget extends StatelessWidget {
|
||||
BuildContext context,
|
||||
Collection collection,
|
||||
) {
|
||||
if (Configuration.instance.getUserID() == collection.owner!.id) {
|
||||
if (Configuration.instance.getUserID() == collection.owner.id) {
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
|
||||
@@ -108,7 +108,7 @@ class ReferralCodeWidget extends StatelessWidget {
|
||||
notifyParent?.call();
|
||||
} catch (e, s) {
|
||||
Logger("ReferralCodeWidget").severe("Failed to update code", e, s);
|
||||
if (e is DioError) {
|
||||
if (e is DioException) {
|
||||
if (e.response?.statusCode == 400) {
|
||||
await showInfoDialog(
|
||||
context,
|
||||
|
||||
@@ -117,28 +117,26 @@ class AdvancedSettingsScreen extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (flagService.internalUser) ...[
|
||||
const SizedBox(height: 24),
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: S.of(context).videoStreaming,
|
||||
),
|
||||
menuItemColor: colorScheme.fillFaint,
|
||||
singleBorderRadius: 8,
|
||||
alignCaptionedTextToLeft: true,
|
||||
trailingWidget: ToggleSwitchWidget(
|
||||
value: () => PreviewVideoStore
|
||||
.instance.isVideoStreamingEnabled,
|
||||
onChanged: () async {
|
||||
final isEnabled = PreviewVideoStore
|
||||
.instance.isVideoStreamingEnabled;
|
||||
|
||||
await PreviewVideoStore.instance
|
||||
.setIsVideoStreamingEnabled(!isEnabled);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: S.of(context).videoStreaming,
|
||||
),
|
||||
],
|
||||
menuItemColor: colorScheme.fillFaint,
|
||||
singleBorderRadius: 8,
|
||||
alignCaptionedTextToLeft: true,
|
||||
trailingWidget: ToggleSwitchWidget(
|
||||
value: () => PreviewVideoStore
|
||||
.instance.isVideoStreamingEnabled,
|
||||
onChanged: () async {
|
||||
final isEnabled = PreviewVideoStore
|
||||
.instance.isVideoStreamingEnabled;
|
||||
|
||||
await PreviewVideoStore.instance
|
||||
.setIsVideoStreamingEnabled(!isEnabled);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
|
||||
@@ -5,6 +5,8 @@ import "package:photos/db/ml/base.dart";
|
||||
import "package:photos/db/ml/db.dart";
|
||||
import "package:photos/events/people_changed_event.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import "package:photos/services/machine_learning/ml_indexing_isolate.dart";
|
||||
import "package:photos/services/machine_learning/ml_models_overview.dart";
|
||||
import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
@@ -104,6 +106,90 @@ class _MLUserDeveloperOptionsState extends State<MLUserDeveloperOptions> {
|
||||
isBottomBorderRadiusRemoved: true,
|
||||
isGestureDetectorDisabled: true,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.neutral,
|
||||
labelText: "Load face detection model",
|
||||
onTap: () async {
|
||||
try {
|
||||
await MLIndexingIsolate.instance
|
||||
.debugLoadSingleModel(MLModels.faceDetection);
|
||||
} catch (e, s) {
|
||||
_logger.severe(
|
||||
"Could not load face detection model",
|
||||
e,
|
||||
s,
|
||||
);
|
||||
await showGenericErrorDialog(
|
||||
context: context,
|
||||
error: e,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.neutral,
|
||||
labelText: "Load face recognition model",
|
||||
onTap: () async {
|
||||
try {
|
||||
await MLIndexingIsolate.instance
|
||||
.debugLoadSingleModel(MLModels.faceEmbedding);
|
||||
} catch (e, s) {
|
||||
_logger.severe(
|
||||
"Could not load face detection model",
|
||||
e,
|
||||
s,
|
||||
);
|
||||
await showGenericErrorDialog(
|
||||
context: context,
|
||||
error: e,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.neutral,
|
||||
labelText: "Load clip image model",
|
||||
onTap: () async {
|
||||
try {
|
||||
await MLIndexingIsolate.instance
|
||||
.debugLoadSingleModel(MLModels.clipImageEncoder);
|
||||
} catch (e, s) {
|
||||
_logger.severe(
|
||||
"Could not load face detection model",
|
||||
e,
|
||||
s,
|
||||
);
|
||||
await showGenericErrorDialog(
|
||||
context: context,
|
||||
error: e,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.neutral,
|
||||
labelText: "Load clip text model",
|
||||
onTap: () async {
|
||||
try {
|
||||
await MLIndexingIsolate.instance
|
||||
.debugLoadSingleModel(MLModels.clipTextEncoder);
|
||||
} catch (e, s) {
|
||||
_logger.severe(
|
||||
"Could not load face detection model",
|
||||
e,
|
||||
s,
|
||||
);
|
||||
await showGenericErrorDialog(
|
||||
context: context,
|
||||
error: e,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SafeArea(
|
||||
child: SizedBox(
|
||||
height: 12,
|
||||
|
||||
@@ -363,8 +363,8 @@ class _AddParticipantPage extends State<AddParticipantPage> {
|
||||
|
||||
List<User> _getSuggestedUser() {
|
||||
final Set<String> existingEmails = {};
|
||||
for (final User? u in widget.collection.sharees ?? []) {
|
||||
if (u != null && u.id != null && u.email.isNotEmpty) {
|
||||
for (final User u in widget.collection.sharees) {
|
||||
if (u.id != null && u.email.isNotEmpty) {
|
||||
existingEmails.add(u.email);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,11 +64,11 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isOwner =
|
||||
widget.collection.owner?.id == Configuration.instance.getUserID();
|
||||
widget.collection.owner.id == Configuration.instance.getUserID();
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
final currentUserID = Configuration.instance.getUserID()!;
|
||||
final int participants = 1 + widget.collection.getSharees().length;
|
||||
final User owner = widget.collection.owner!;
|
||||
final User owner = widget.collection.owner;
|
||||
if (owner.id == currentUserID && owner.email == "") {
|
||||
owner.email = Configuration.instance.getEmail()!;
|
||||
}
|
||||
@@ -107,11 +107,9 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: isOwner
|
||||
? S.of(context).you
|
||||
: widget.collection.owner != null
|
||||
? _nameIfAvailableElseEmail(
|
||||
widget.collection.owner!,
|
||||
)
|
||||
: '',
|
||||
: _nameIfAvailableElseEmail(
|
||||
widget.collection.owner,
|
||||
),
|
||||
makeTextBold: isOwner,
|
||||
),
|
||||
leadingIconWidget: UserAvatarWidget(
|
||||
|
||||
@@ -47,13 +47,13 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isCollectEnabled =
|
||||
widget.collection!.publicURLs?.firstOrNull?.enableCollect ?? false;
|
||||
widget.collection!.publicURLs.firstOrNull?.enableCollect ?? false;
|
||||
final isDownloadEnabled =
|
||||
widget.collection!.publicURLs?.firstOrNull?.enableDownload ?? true;
|
||||
widget.collection!.publicURLs.firstOrNull?.enableDownload ?? true;
|
||||
final isPasswordEnabled =
|
||||
widget.collection!.publicURLs?.firstOrNull?.passwordEnabled ?? false;
|
||||
widget.collection!.publicURLs.firstOrNull?.passwordEnabled ?? false;
|
||||
final enteColorScheme = getEnteColorScheme(context);
|
||||
final PublicURL url = widget.collection!.publicURLs!.firstOrNull!;
|
||||
final PublicURL url = widget.collection!.publicURLs.firstOrNull!;
|
||||
final String collectionKey = Base58Encode(
|
||||
CollectionsService.instance.getCollectionKey(widget.collection!.id),
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ class _ItemsWidgetState extends State<ItemsWidget> {
|
||||
bool isCustomLimit = false;
|
||||
@override
|
||||
void initState() {
|
||||
currentDeviceLimit = widget.collection.publicURLs!.first!.deviceLimit;
|
||||
currentDeviceLimit = widget.collection.publicURLs.first.deviceLimit;
|
||||
initialDeviceLimit = currentDeviceLimit;
|
||||
if (!publicLinkDeviceLimits.contains(currentDeviceLimit)) {
|
||||
isCustomLimit = true;
|
||||
|
||||
@@ -61,7 +61,7 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_sharees = widget.collection.sharees ?? [];
|
||||
_sharees = widget.collection.sharees;
|
||||
final bool hasUrl = widget.collection.hasLink;
|
||||
final children = <Widget>[];
|
||||
children.add(
|
||||
@@ -136,7 +136,7 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
|
||||
}
|
||||
|
||||
final bool hasExpired =
|
||||
widget.collection.publicURLs?.firstOrNull?.isExpired ?? false;
|
||||
widget.collection.publicURLs.firstOrNull?.isExpired ?? false;
|
||||
children.addAll([
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
@@ -166,7 +166,7 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
|
||||
CollectionsService.instance.getCollectionKey(widget.collection.id),
|
||||
);
|
||||
final String url =
|
||||
"${widget.collection.publicURLs!.first!.url}#$collectionKey";
|
||||
"${widget.collection.publicURLs.first.url}#$collectionKey";
|
||||
children.addAll(
|
||||
[
|
||||
MenuItemWidget(
|
||||
|
||||
@@ -275,7 +275,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
||||
final existingCollection =
|
||||
CollectionsService.instance.getCollectionByID(collection.id);
|
||||
|
||||
if (collection.owner!.id! == Configuration.instance.getUserID() ||
|
||||
if (collection.isOwner(Configuration.instance.getUserID() ?? -1) ||
|
||||
(existingCollection != null && !existingCollection.isDeleted)) {
|
||||
await routeToPage(
|
||||
context,
|
||||
@@ -286,8 +286,8 @@ class _HomeWidgetState extends State<HomeWidget> {
|
||||
return;
|
||||
}
|
||||
final dialog = createProgressDialog(context, "Loading...");
|
||||
final publicUrl = collection.publicURLs![0];
|
||||
if (!publicUrl!.enableDownload) {
|
||||
final publicUrl = collection.publicURLs[0];
|
||||
if (!publicUrl.enableDownload) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.canNotOpenTitle,
|
||||
|
||||
@@ -109,7 +109,7 @@ class QuickLinkAlbumItem extends StatelessWidget {
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
c.hasLink
|
||||
? (c.publicURLs!.first!.isExpired
|
||||
? (c.publicURLs.first.isExpired
|
||||
? Icon(
|
||||
Icons.link_outlined,
|
||||
color: colorScheme.warning500,
|
||||
|
||||
@@ -804,7 +804,7 @@ class _FileSelectionActionsWidgetState
|
||||
.getCollectionKey(_cachedCollectionForSharedLink!.id),
|
||||
);
|
||||
final String url =
|
||||
"${_cachedCollectionForSharedLink!.publicURLs?.first?.url}#$collectionKey";
|
||||
"${_cachedCollectionForSharedLink!.publicURLs.first.url}#$collectionKey";
|
||||
unawaited(Clipboard.setData(ClipboardData(text: url)));
|
||||
await shareImageAndUrl(
|
||||
placeholderBytes,
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomAppBar extends PreferredSize {
|
||||
@override
|
||||
final Widget child;
|
||||
@override
|
||||
final Size preferredSize;
|
||||
final double height;
|
||||
|
||||
const CustomAppBar(
|
||||
this.child,
|
||||
this.preferredSize, {
|
||||
Key? key,
|
||||
this.height = kToolbarHeight,
|
||||
}) : super(key: key, child: child, preferredSize: preferredSize);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: preferredSize.height,
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,6 @@ class _DetailPageState extends State<DetailPage> {
|
||||
return FileAppBar(
|
||||
_files![selectedIndex],
|
||||
_onFileRemoved,
|
||||
100,
|
||||
widget.config.mode == DetailPageMode.full,
|
||||
enableFullScreenNotifier: _enableFullScreenNotifier,
|
||||
);
|
||||
|
||||
@@ -23,7 +23,6 @@ import "package:photos/services/local_authentication_service.dart";
|
||||
import "package:photos/services/preview_video_store.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import 'package:photos/ui/collections/collection_action_sheet.dart';
|
||||
import 'package:photos/ui/viewer/file/custom_app_bar.dart';
|
||||
import "package:photos/ui/viewer/file_details/favorite_widget.dart";
|
||||
import "package:photos/ui/viewer/file_details/upload_icon_widget.dart";
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
@@ -35,14 +34,12 @@ import 'package:photos/utils/toast_util.dart';
|
||||
class FileAppBar extends StatefulWidget {
|
||||
final EnteFile file;
|
||||
final Function(EnteFile) onFileRemoved;
|
||||
final double height;
|
||||
final bool shouldShowActions;
|
||||
final ValueNotifier<bool> enableFullScreenNotifier;
|
||||
|
||||
const FileAppBar(
|
||||
this.file,
|
||||
this.onFileRemoved,
|
||||
this.height,
|
||||
this.shouldShowActions, {
|
||||
required this.enableFullScreenNotifier,
|
||||
super.key,
|
||||
@@ -98,8 +95,9 @@ class FileAppBarState extends State<FileAppBar> {
|
||||
|
||||
final isTrashedFile = widget.file is TrashFile;
|
||||
final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
|
||||
return CustomAppBar(
|
||||
ValueListenableBuilder(
|
||||
return PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: widget.enableFullScreenNotifier,
|
||||
builder: (context, bool isFullScreen, child) {
|
||||
return IgnorePointer(
|
||||
@@ -124,32 +122,33 @@ class FileAppBarState extends State<FileAppBar> {
|
||||
stops: const [0, 0.2, 1],
|
||||
),
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
child: AppBar(
|
||||
clipBehavior: Clip.none,
|
||||
key: ValueKey(isGuestView),
|
||||
iconTheme: const IconThemeData(
|
||||
color: Colors.white,
|
||||
), //same for both themes
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
isGuestView
|
||||
? _requestAuthentication()
|
||||
: Navigator.of(context).pop();
|
||||
},
|
||||
child: SafeArea(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
switchInCurve: Curves.easeInOut,
|
||||
switchOutCurve: Curves.easeInOut,
|
||||
child: AppBar(
|
||||
clipBehavior: Clip.none,
|
||||
key: ValueKey(isGuestView),
|
||||
iconTheme: const IconThemeData(
|
||||
color: Colors.white,
|
||||
), //same for both themes
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
isGuestView
|
||||
? _requestAuthentication()
|
||||
: Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
actions: shouldShowActions && !isGuestView ? _actions : [],
|
||||
elevation: 0,
|
||||
backgroundColor: const Color(0x00000000),
|
||||
),
|
||||
actions: shouldShowActions && !isGuestView ? _actions : [],
|
||||
elevation: 0,
|
||||
backgroundColor: const Color(0x00000000),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Size.fromHeight(Platform.isAndroid ? 84 : 96),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,10 +178,7 @@ class FileAppBarState extends State<FileAppBar> {
|
||||
}
|
||||
if (!isFileHidden && isFileUploaded) {
|
||||
_actions.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: FavoriteWidget(widget.file),
|
||||
),
|
||||
Center(child: FavoriteWidget(widget.file)),
|
||||
);
|
||||
}
|
||||
if (!isFileUploaded) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import "dart:io";
|
||||
|
||||
import 'package:chewie/chewie.dart';
|
||||
// import 'package:chewie/src/notifiers/player_notifier.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:fluttertoast/fluttertoast.dart";
|
||||
@@ -22,6 +23,7 @@ import "package:photos/utils/dialog_util.dart";
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
import "package:photos/utils/wakelock_util.dart";
|
||||
// import "package:provider/provider.dart";
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
@@ -56,6 +58,8 @@ class _PreviewVideoWidgetState extends State<PreviewVideoWidget> {
|
||||
late final StreamSubscription<GuestViewEvent> _fileSwipeLockEventSubscription;
|
||||
File? previewFile;
|
||||
|
||||
bool initializedPlayerNotifier = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -237,9 +241,6 @@ class _PreviewVideoWidgetState extends State<PreviewVideoWidget> {
|
||||
_videoPlayerController!.addListener(() {
|
||||
if (_isPlaying != _videoPlayerController!.value.isPlaying) {
|
||||
_isPlaying = _videoPlayerController!.value.isPlaying;
|
||||
if (widget.playbackCallback != null) {
|
||||
widget.playbackCallback!(_isPlaying);
|
||||
}
|
||||
unawaited(_keepScreenAliveOnPlaying(_isPlaying));
|
||||
}
|
||||
});
|
||||
@@ -251,10 +252,13 @@ class _PreviewVideoWidgetState extends State<PreviewVideoWidget> {
|
||||
looping: true,
|
||||
allowMuting: true,
|
||||
allowFullScreen: false,
|
||||
customControls: VideoControls(
|
||||
file: widget.file,
|
||||
onStreamChange: widget.onStreamChange,
|
||||
),
|
||||
customControls: (hideStuff) {
|
||||
widget.playbackCallback!(hideStuff);
|
||||
return VideoControls(
|
||||
file: widget.file,
|
||||
onStreamChange: widget.onStreamChange,
|
||||
);
|
||||
},
|
||||
);
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
|
||||
@@ -21,15 +21,15 @@ class CreationTimeItem extends StatefulWidget {
|
||||
class _CreationTimeItemState extends State<CreationTimeItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateTime =
|
||||
DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!);
|
||||
final dateTime = DateTime.fromMicrosecondsSinceEpoch(
|
||||
widget.file.creationTime!,
|
||||
isUtc: true,
|
||||
).toLocal();
|
||||
return InfoItemWidget(
|
||||
key: const ValueKey("Creation time"),
|
||||
leadingIcon: Icons.calendar_today_outlined,
|
||||
title: DateFormat.yMMMEd(Localizations.localeOf(context).languageCode)
|
||||
.format(
|
||||
DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!),
|
||||
),
|
||||
.format(dateTime),
|
||||
subtitleSection: Future.value([
|
||||
Text(
|
||||
getTimeIn12hrFormat(dateTime) + " " + dateTime.timeZoneName,
|
||||
|
||||
@@ -48,7 +48,7 @@ class _EmptyAlbumStateNewState extends State<CollectPhotosBottomButtons> {
|
||||
final String collectionKey = Base58Encode(
|
||||
CollectionsService.instance.getCollectionKey(widget.c.id),
|
||||
);
|
||||
final String url = "${widget.c.publicURLs!.first!.url}#$collectionKey";
|
||||
final String url = "${widget.c.publicURLs.first.url}#$collectionKey";
|
||||
await shareAlbumLinkWithPlaceholder(
|
||||
context,
|
||||
widget.c,
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:photos/ui/viewer/gallery/empty_state.dart';
|
||||
import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart";
|
||||
import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/state/inherited_search_filter_data.dart";
|
||||
import "package:photos/utils/date_time_util.dart";
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
import "package:photos/utils/hierarchical_search_util.dart";
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
@@ -131,19 +132,57 @@ class GalleryState extends State<Gallery> {
|
||||
_itemScroller = ItemScrollController();
|
||||
if (widget.reloadEvent != null) {
|
||||
_reloadEventSubscription = widget.reloadEvent!.listen((event) async {
|
||||
bool shouldReloadFromDB = true;
|
||||
if (event.source == 'uploadCompleted') {
|
||||
final Map<int, EnteFile> genIDToUploadedFiles = {};
|
||||
for (int i = 0; i < event.updatedFiles.length; i++) {
|
||||
if (event.updatedFiles[i].generatedID == null) {
|
||||
shouldReloadFromDB = true;
|
||||
break;
|
||||
}
|
||||
genIDToUploadedFiles[event.updatedFiles[i].generatedID!] =
|
||||
event.updatedFiles[i];
|
||||
}
|
||||
for (int i = 0; i < _allGalleryFiles.length; i++) {
|
||||
final file = _allGalleryFiles[i];
|
||||
if (file.generatedID == null) {
|
||||
continue;
|
||||
}
|
||||
final updateFile = genIDToUploadedFiles[file.generatedID!];
|
||||
if (updateFile != null &&
|
||||
updateFile.localID == file.localID &&
|
||||
areFromSameDay(
|
||||
updateFile.creationTime ?? 0,
|
||||
file.creationTime ?? 0,
|
||||
)) {
|
||||
_allGalleryFiles[i] = updateFile;
|
||||
genIDToUploadedFiles.remove(file.generatedID!);
|
||||
}
|
||||
}
|
||||
shouldReloadFromDB = genIDToUploadedFiles.isNotEmpty;
|
||||
}
|
||||
if (!shouldReloadFromDB) {
|
||||
final bool hasCalledSetState = _onFilesLoaded(_allGalleryFiles);
|
||||
_logger.info(
|
||||
'Skip softRefresh from DB, processed updated in memory with setStateReload $hasCalledSetState',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_debouncer.run(() async {
|
||||
// In soft refresh, setState is called for entire gallery only when
|
||||
// number of child change
|
||||
_logger.finest("Soft refresh all files on ${event.reason} ");
|
||||
final result = await _loadFiles();
|
||||
final bool hasReloaded = _onFilesLoaded(result.files);
|
||||
if (hasReloaded && kDebugMode) {
|
||||
final bool hasTriggeredSetState = _onFilesLoaded(result.files);
|
||||
if (hasTriggeredSetState && kDebugMode) {
|
||||
_logger.finest(
|
||||
"Reloaded gallery on soft refresh all files on ${event.reason}",
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
if (!hasTriggeredSetState && mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -208,8 +247,9 @@ class GalleryState extends State<Gallery> {
|
||||
_hasLoadedFiles = true;
|
||||
currentGroupedFiles = updatedGroupedFiles;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
} else {
|
||||
currentGroupedFiles = updatedGroupedFiles;
|
||||
return false;
|
||||
|
||||
@@ -820,7 +820,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
|
||||
"Cannot share empty collection of type $galleryType",
|
||||
);
|
||||
}
|
||||
if (Configuration.instance.getUserID() == widget.collection!.owner!.id) {
|
||||
if (Configuration.instance.getUserID() == widget.collection!.owner.id) {
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
|
||||
@@ -151,27 +151,35 @@ class _SharedPublicCollectionPageState
|
||||
}
|
||||
|
||||
Future<void> _joinAlbum() async {
|
||||
final dialog = createProgressDialog(
|
||||
final result = await showChoiceDialog(
|
||||
context,
|
||||
S.of(context).pleaseWait,
|
||||
isDismissible: true,
|
||||
title: context.l10n.joinAlbum,
|
||||
body: context.l10n.joinAlbumConfirmationDialogBody,
|
||||
firstButtonLabel: context.l10n.join,
|
||||
);
|
||||
await dialog.show();
|
||||
try {
|
||||
await RemoteSyncService.instance
|
||||
.joinAndSyncCollection(context, widget.c.collection.id);
|
||||
final c =
|
||||
CollectionsService.instance.getCollectionByID(widget.c.collection.id);
|
||||
await dialog.hide();
|
||||
Navigator.of(context).pop();
|
||||
await routeToPage(
|
||||
if (result != null && result.action == ButtonAction.first) {
|
||||
final dialog = createProgressDialog(
|
||||
context,
|
||||
CollectionPage(CollectionWithThumbnail(c!, null)),
|
||||
S.of(context).pleaseWait,
|
||||
isDismissible: true,
|
||||
);
|
||||
} catch (e, s) {
|
||||
logger.severe("Failed to join collection", e, s);
|
||||
await dialog.hide();
|
||||
showToast(context, S.of(context).somethingWentWrong);
|
||||
await dialog.show();
|
||||
try {
|
||||
await RemoteSyncService.instance
|
||||
.joinAndSyncCollection(context, widget.c.collection.id);
|
||||
final c = CollectionsService.instance
|
||||
.getCollectionByID(widget.c.collection.id);
|
||||
await dialog.hide();
|
||||
Navigator.of(context).pop();
|
||||
await routeToPage(
|
||||
context,
|
||||
CollectionPage(CollectionWithThumbnail(c!, null)),
|
||||
);
|
||||
} catch (e, s) {
|
||||
logger.severe("Failed to join collection", e, s);
|
||||
await dialog.hide();
|
||||
showToast(context, S.of(context).somethingWentWrong);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import "dart:async";
|
||||
import "package:email_validator/email_validator.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/people_changed_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
@@ -245,11 +246,13 @@ class _LinkEmailScreen extends State<LinkEmailScreen> {
|
||||
}
|
||||
|
||||
List<User> _getContacts() {
|
||||
final usersEmailsToAviod =
|
||||
final userEmailsToAviod =
|
||||
PersonService.instance.emailToPartialPersonDataMapCache.keys.toSet();
|
||||
final ownerEmail = Configuration.instance.getEmail();
|
||||
final relevantUsers = UserService.instance.getRelevantContacts()
|
||||
..add(User(email: ownerEmail!))
|
||||
..removeWhere(
|
||||
(user) => usersEmailsToAviod.contains(user.email),
|
||||
(user) => userEmailsToAviod.contains(user.email),
|
||||
);
|
||||
|
||||
relevantUsers.sort(
|
||||
|
||||
@@ -986,11 +986,13 @@ class _EmailSectionState extends State<_EmailSection> {
|
||||
}
|
||||
|
||||
List<User> _getContacts() {
|
||||
final usersEmailsToAviod =
|
||||
final userEmailsToAviod =
|
||||
PersonService.instance.emailToPartialPersonDataMapCache.keys;
|
||||
final ownerEmail = Configuration.instance.getEmail();
|
||||
final relevantUsers = UserService.instance.getRelevantContacts()
|
||||
..add(User(email: ownerEmail!))
|
||||
..removeWhere(
|
||||
(user) => usersEmailsToAviod.contains(user.email),
|
||||
(user) => userEmailsToAviod.contains(user.email),
|
||||
);
|
||||
|
||||
relevantUsers.sort(
|
||||
|
||||
@@ -76,7 +76,7 @@ Future<ButtonResult?> showErrorDialogForException({
|
||||
}) async {
|
||||
String errorMessage =
|
||||
message ?? S.of(context).tempErrorContactSupportIfPersists;
|
||||
if (exception is DioError &&
|
||||
if (exception is DioException &&
|
||||
exception.response != null &&
|
||||
exception.response!.data["code"] != null) {
|
||||
errorMessage =
|
||||
@@ -107,9 +107,12 @@ String parseErrorForUI(
|
||||
if (error == null) {
|
||||
return genericError;
|
||||
}
|
||||
if (error is DioError) {
|
||||
final DioError dioError = error;
|
||||
if (dioError.type == DioErrorType.other) {
|
||||
if (error is DioException) {
|
||||
final DioException dioError = error;
|
||||
if (dioError.type == DioExceptionType.receiveTimeout ||
|
||||
dioError.type == DioExceptionType.connectionError ||
|
||||
dioError.type == DioExceptionType.sendTimeout ||
|
||||
dioError.type == DioExceptionType.cancel) {
|
||||
if (dioError.error.toString().contains('Failed host lookup')) {
|
||||
return S.of(context).networkHostLookUpErr;
|
||||
} else if (dioError.error.toString().contains('SocketException')) {
|
||||
@@ -122,15 +125,15 @@ String parseErrorForUI(
|
||||
return genericError;
|
||||
}
|
||||
String errorInfo = "";
|
||||
if (error is DioError) {
|
||||
final DioError dioError = error;
|
||||
if (dioError.type == DioErrorType.response) {
|
||||
if (error is DioException) {
|
||||
final DioException dioError = error;
|
||||
if (dioError.type == DioExceptionType.badResponse) {
|
||||
if (dioError.response?.data["code"] != null) {
|
||||
errorInfo = "Reason: " + dioError.response!.data["code"];
|
||||
} else {
|
||||
errorInfo = "Reason: " + dioError.response!.data.toString();
|
||||
}
|
||||
} else if (dioError.type == DioErrorType.other) {
|
||||
} else if (dioError.type == DioExceptionType.badCertificate) {
|
||||
errorInfo = "Reason: " + dioError.error.toString();
|
||||
} else {
|
||||
errorInfo = "Reason: " + dioError.type.toString();
|
||||
|
||||
@@ -47,7 +47,7 @@ Future<Map<String, IfdTag>> getExif(EnteFile file) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, IfdTag>?> getExifFromSourceFile(File originFile) async {
|
||||
Future<Map<String, IfdTag>?> tryExifFromFile(File originFile) async {
|
||||
try {
|
||||
final exif = await readExifAsync(originFile);
|
||||
return exif;
|
||||
@@ -125,7 +125,25 @@ bool? checkPanoramaFromEXIF(File? file, Map<String, IfdTag>? exifData) {
|
||||
return element?.printable == "6";
|
||||
}
|
||||
|
||||
Future<DateTime?> getCreationTimeFromEXIF(
|
||||
class ParsedExifDateTime {
|
||||
late final DateTime? time;
|
||||
late final String? dateTime;
|
||||
late final String? offsetTime;
|
||||
ParsedExifDateTime(DateTime this.time, String? dateTime, this.offsetTime) {
|
||||
if (dateTime != null && dateTime.endsWith('Z')) {
|
||||
this.dateTime = dateTime.substring(0, dateTime.length - 1);
|
||||
} else {
|
||||
this.dateTime = dateTime;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "ParsedExifDateTime{time: $time, dateTime: $dateTime, offsetTime: $offsetTime}";
|
||||
}
|
||||
}
|
||||
|
||||
Future<ParsedExifDateTime?> tryParseExifDateTime(
|
||||
File? file,
|
||||
Map<String, IfdTag>? exifData,
|
||||
) async {
|
||||
@@ -137,46 +155,55 @@ Future<DateTime?> getCreationTimeFromEXIF(
|
||||
: exif.containsKey(kImageDateTime)
|
||||
? exif[kImageDateTime]!.printable
|
||||
: null;
|
||||
if (exifTime != null && exifTime != kEmptyExifDateTime) {
|
||||
String? exifOffsetTime;
|
||||
for (final key in kExifOffSetKeys) {
|
||||
if (exif.containsKey(key)) {
|
||||
exifOffsetTime = exif[key]!.printable;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime);
|
||||
if (exifTime == null || exifTime == kEmptyExifDateTime) {
|
||||
return null;
|
||||
}
|
||||
String? exifOffsetTime;
|
||||
for (final key in kExifOffSetKeys) {
|
||||
if (exif.containsKey(key)) {
|
||||
exifOffsetTime = exif[key]!.printable;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime);
|
||||
} catch (e) {
|
||||
_logger.severe("failed to getCreationTimeFromEXIF", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) {
|
||||
final DateTime result = DateFormat(kExifDateTimePattern).parse(exifTime);
|
||||
if (offsetString == null) {
|
||||
return result;
|
||||
ParsedExifDateTime getDateTimeInDeviceTimezone(
|
||||
String exifTime,
|
||||
String? offsetString,
|
||||
) {
|
||||
final hasOffset = (offsetString ?? '') != '';
|
||||
final DateTime result =
|
||||
DateFormat(kExifDateTimePattern).parse(exifTime, hasOffset);
|
||||
if (hasOffset && offsetString!.toUpperCase() != "Z") {
|
||||
try {
|
||||
final List<String> splitHHMM = offsetString.split(":");
|
||||
final int offsetHours = int.parse(splitHHMM[0]);
|
||||
final int offsetMinutes =
|
||||
int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1);
|
||||
// Adjust the date for the offset to get the photo's correct UTC time
|
||||
final photoUtcDate =
|
||||
result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes));
|
||||
// Convert the UTC time to the device's local time
|
||||
final deviceLocalTime = photoUtcDate.toLocal();
|
||||
return ParsedExifDateTime(
|
||||
deviceLocalTime,
|
||||
result.toIso8601String(),
|
||||
offsetString,
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe("offset parsing failed $exifTime && $offsetString", e, s);
|
||||
}
|
||||
}
|
||||
try {
|
||||
final List<String> splitHHMM = offsetString.split(":");
|
||||
// Parse the offset from the photo's time zone
|
||||
final int offsetHours = int.parse(splitHHMM[0]);
|
||||
final int offsetMinutes =
|
||||
int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1);
|
||||
// Adjust the date for the offset to get the photo's correct UTC time
|
||||
final photoUtcDate =
|
||||
result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes));
|
||||
// Getting the current device's time zone offset from UTC
|
||||
final now = DateTime.now();
|
||||
final localOffset = now.timeZoneOffset;
|
||||
// Adjusting the photo's UTC time to the device's local time
|
||||
final deviceLocalTime = photoUtcDate.add(localOffset);
|
||||
return deviceLocalTime;
|
||||
} catch (e, s) {
|
||||
_logger.severe("tz offset adjust failed $offsetString", e, s);
|
||||
}
|
||||
return result;
|
||||
return ParsedExifDateTime(
|
||||
result,
|
||||
result.toIso8601String(),
|
||||
(offsetString ?? '').toUpperCase() == 'Z' ? 'Z' : null,
|
||||
);
|
||||
}
|
||||
|
||||
Location? locationFromExif(Map<String, IfdTag> exif) {
|
||||
|
||||
@@ -216,7 +216,7 @@ Future<Map<String, Uint8List>?> _getFaceCrops(
|
||||
faceBoxes,
|
||||
);
|
||||
final Map<String, Uint8List> result = {};
|
||||
for (int i = 0; i < faceIds.length; i++) {
|
||||
for (int i = 0; i < faceCrop.length; i++) {
|
||||
result[faceIds[i]] = faceCrop[i];
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -41,6 +41,7 @@ import 'package:photos/services/sync_service.dart';
|
||||
import "package:photos/services/user_service.dart";
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/data_util.dart';
|
||||
import "package:photos/utils/exif_util.dart";
|
||||
import "package:photos/utils/file_key.dart";
|
||||
import 'package:photos/utils/file_uploader_util.dart';
|
||||
import "package:photos/utils/file_util.dart";
|
||||
@@ -82,7 +83,6 @@ class FileUploader {
|
||||
int _uploadCounter = 0;
|
||||
int _videoUploadCounter = 0;
|
||||
late ProcessType _processType;
|
||||
late bool _isBackground;
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
// _hasInitiatedForceUpload is used to track if user attempted force upload
|
||||
@@ -104,7 +104,6 @@ class FileUploader {
|
||||
|
||||
Future<void> init(SharedPreferences preferences, bool isBackground) async {
|
||||
_prefs = preferences;
|
||||
_isBackground = isBackground;
|
||||
_processType =
|
||||
isBackground ? ProcessType.background : ProcessType.foreground;
|
||||
final currentTime = DateTime.now().microsecondsSinceEpoch;
|
||||
@@ -538,7 +537,7 @@ class FileUploader {
|
||||
|
||||
MediaUploadData? mediaUploadData;
|
||||
try {
|
||||
mediaUploadData = await getUploadDataFromEnteFile(file);
|
||||
mediaUploadData = await getUploadDataFromEnteFile(file, parseExif: true);
|
||||
} catch (e) {
|
||||
// This additional try catch block is added because for resumable upload,
|
||||
// we need to compute the hash before the next step. Previously, this
|
||||
@@ -730,8 +729,13 @@ class FileUploader {
|
||||
encThumbSize,
|
||||
);
|
||||
}
|
||||
final ParsedExifDateTime? exifTime = await tryParseExifDateTime(
|
||||
null,
|
||||
mediaUploadData.exifData,
|
||||
);
|
||||
final metadata =
|
||||
await file.getMetadataForUpload(mediaUploadData, exifTime);
|
||||
|
||||
final metadata = await file.getMetadataForUpload(mediaUploadData);
|
||||
final encryptedMetadataResult = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(jsonEncode(metadata)),
|
||||
fileAttributes.key!,
|
||||
@@ -773,22 +777,9 @@ class FileUploader {
|
||||
CryptoUtil.bin2base64(encryptedFileKeyData.encryptedData!);
|
||||
final keyDecryptionNonce =
|
||||
CryptoUtil.bin2base64(encryptedFileKeyData.nonce!);
|
||||
final Map<String, dynamic> pubMetadata = {};
|
||||
final Map<String, dynamic> pubMetadata =
|
||||
_buildPublicMagicData(mediaUploadData, exifTime);
|
||||
MetadataRequest? pubMetadataRequest;
|
||||
if ((mediaUploadData.height ?? 0) != 0 &&
|
||||
(mediaUploadData.width ?? 0) != 0) {
|
||||
pubMetadata[heightKey] = mediaUploadData.height;
|
||||
pubMetadata[widthKey] = mediaUploadData.width;
|
||||
pubMetadata[mediaTypeKey] =
|
||||
mediaUploadData.isPanorama == true ? 1 : 0;
|
||||
}
|
||||
if (mediaUploadData.motionPhotoStartIndex != null) {
|
||||
pubMetadata[motionVideoIndexKey] =
|
||||
mediaUploadData.motionPhotoStartIndex;
|
||||
}
|
||||
if (mediaUploadData.thumbnail == null) {
|
||||
pubMetadata[noThumbKey] = true;
|
||||
}
|
||||
if (pubMetadata.isNotEmpty) {
|
||||
pubMetadataRequest = await getPubMetadataRequest(
|
||||
file,
|
||||
@@ -823,14 +814,12 @@ class FileUploader {
|
||||
}
|
||||
await UploadLocksDB.instance.deleteMultipartTrack(lockKey);
|
||||
|
||||
if (!_isBackground) {
|
||||
Bus.instance.fire(
|
||||
LocalPhotosUpdatedEvent(
|
||||
[remoteFile],
|
||||
source: "downloadComplete",
|
||||
),
|
||||
);
|
||||
}
|
||||
Bus.instance.fire(
|
||||
LocalPhotosUpdatedEvent(
|
||||
[remoteFile],
|
||||
source: "uploadCompleted",
|
||||
),
|
||||
);
|
||||
_logger.info("File upload complete for " + remoteFile.toString());
|
||||
uploadCompleted = true;
|
||||
Bus.instance.fire(FileUploadedEvent(remoteFile));
|
||||
@@ -872,8 +861,36 @@ class FileUploader {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildPublicMagicData(
|
||||
MediaUploadData mediaUploadData,
|
||||
ParsedExifDateTime? exifTime,
|
||||
) {
|
||||
final Map<String, dynamic> pubMetadata = {};
|
||||
if ((mediaUploadData.height ?? 0) != 0 &&
|
||||
(mediaUploadData.width ?? 0) != 0) {
|
||||
pubMetadata[heightKey] = mediaUploadData.height;
|
||||
pubMetadata[widthKey] = mediaUploadData.width;
|
||||
pubMetadata[mediaTypeKey] = mediaUploadData.isPanorama == true ? 1 : 0;
|
||||
}
|
||||
if (mediaUploadData.motionPhotoStartIndex != null) {
|
||||
pubMetadata[motionVideoIndexKey] = mediaUploadData.motionPhotoStartIndex;
|
||||
}
|
||||
if (mediaUploadData.thumbnail == null) {
|
||||
pubMetadata[noThumbKey] = true;
|
||||
}
|
||||
if (exifTime != null) {
|
||||
if (exifTime.dateTime != null) {
|
||||
pubMetadata[dateTimeKey] = exifTime.dateTime;
|
||||
}
|
||||
if (exifTime.offsetTime != null) {
|
||||
pubMetadata[offsetTimeKey] = exifTime.offsetTime;
|
||||
}
|
||||
}
|
||||
return pubMetadata;
|
||||
}
|
||||
|
||||
bool isPutOrUpdateFileError(Object e) {
|
||||
if (e is DioError) {
|
||||
if (e is DioException) {
|
||||
return e.requestOptions.path.contains("/files") ||
|
||||
e.requestOptions.path.contains("/files/update");
|
||||
}
|
||||
@@ -1168,7 +1185,7 @@ class FileUploader {
|
||||
file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
|
||||
file.metadataDecryptionHeader = metadataDecryptionHeader;
|
||||
return file;
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 413) {
|
||||
throw FileTooLargeForPlanError();
|
||||
} else if (e.response?.statusCode == 426) {
|
||||
@@ -1236,7 +1253,7 @@ class FileUploader {
|
||||
file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
|
||||
file.metadataDecryptionHeader = metadataDecryptionHeader;
|
||||
return file;
|
||||
} on DioError catch (e) {
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 426) {
|
||||
_onStorageLimitExceeded();
|
||||
} else if (attempt < kMaximumUploadAttempts) {
|
||||
@@ -1292,7 +1309,7 @@ class FileUploader {
|
||||
.map((e) => UploadURL.fromMap(e))
|
||||
.toList();
|
||||
_uploadURLs.addAll(urls);
|
||||
} on DioError catch (e, s) {
|
||||
} on DioException catch (e, s) {
|
||||
if (e.response != null) {
|
||||
if (e.response!.statusCode == 402) {
|
||||
final error = NoActiveSubscriptionError();
|
||||
@@ -1342,8 +1359,8 @@ class FileUploader {
|
||||
);
|
||||
|
||||
return uploadURL.objectKey;
|
||||
} on DioError catch (e) {
|
||||
if (e.message.startsWith("HttpException: Content size")) {
|
||||
} on DioException catch (e) {
|
||||
if (e.message?.startsWith("HttpException: Content size") ?? false) {
|
||||
rethrow;
|
||||
} else if (attempt < kMaximumUploadAttempts) {
|
||||
_logger.info("Upload failed for $fileName, retrying");
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'dart:ui' as ui;
|
||||
|
||||
import "package:archive/archive_io.dart";
|
||||
import "package:computer/computer.dart";
|
||||
import "package:exif/exif.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:motion_photos/motion_photos.dart";
|
||||
import 'package:motionphoto/motionphoto.dart';
|
||||
@@ -44,6 +45,8 @@ class MediaUploadData {
|
||||
// For iOS, this value will be always null.
|
||||
final int? motionPhotoStartIndex;
|
||||
|
||||
final Map<String, IfdTag>? exifData;
|
||||
|
||||
bool? isPanorama;
|
||||
|
||||
MediaUploadData(
|
||||
@@ -55,6 +58,7 @@ class MediaUploadData {
|
||||
this.width,
|
||||
this.motionPhotoStartIndex,
|
||||
this.isPanorama,
|
||||
this.exifData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,20 +73,27 @@ class FileHashData {
|
||||
FileHashData(this.fileHash, {this.zipHash});
|
||||
}
|
||||
|
||||
Future<MediaUploadData> getUploadDataFromEnteFile(EnteFile file) async {
|
||||
Future<MediaUploadData> getUploadDataFromEnteFile(
|
||||
EnteFile file, {
|
||||
bool parseExif = false,
|
||||
}) async {
|
||||
if (file.isSharedMediaToAppSandbox) {
|
||||
return await _getMediaUploadDataFromAppCache(file);
|
||||
return await _getMediaUploadDataFromAppCache(file, parseExif);
|
||||
} else {
|
||||
return await _getMediaUploadDataFromAssetFile(file);
|
||||
return await _getMediaUploadDataFromAssetFile(file, parseExif);
|
||||
}
|
||||
}
|
||||
|
||||
Future<MediaUploadData> _getMediaUploadDataFromAssetFile(EnteFile file) async {
|
||||
Future<MediaUploadData> _getMediaUploadDataFromAssetFile(
|
||||
EnteFile file,
|
||||
bool parseExif,
|
||||
) async {
|
||||
File? sourceFile;
|
||||
Uint8List? thumbnailData;
|
||||
bool isDeleted;
|
||||
String? zipHash;
|
||||
String fileHash;
|
||||
Map<String, IfdTag>? exifData;
|
||||
|
||||
// The timeouts are to safeguard against https://github.com/CaiJingLong/flutter_photo_manager/issues/467
|
||||
final asset = await file.getAsset
|
||||
@@ -115,8 +126,11 @@ Future<MediaUploadData> _getMediaUploadDataFromAssetFile(EnteFile file) async {
|
||||
InvalidReason.sourceFileMissing,
|
||||
);
|
||||
}
|
||||
if (parseExif) {
|
||||
exifData = await tryExifFromFile(sourceFile);
|
||||
}
|
||||
// h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
|
||||
await _decorateEnteFileData(file, asset, sourceFile);
|
||||
await _decorateEnteFileData(file, asset, sourceFile, exifData);
|
||||
fileHash = CryptoUtil.bin2base64(await CryptoUtil.getHash(sourceFile));
|
||||
|
||||
if (file.fileType == FileType.livePhoto && Platform.isIOS) {
|
||||
@@ -177,6 +191,7 @@ Future<MediaUploadData> _getMediaUploadDataFromAssetFile(EnteFile file) async {
|
||||
height: h,
|
||||
width: w,
|
||||
motionPhotoStartIndex: motionPhotoStartingIndex,
|
||||
exifData: exifData,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -284,6 +299,7 @@ Future<void> _decorateEnteFileData(
|
||||
EnteFile file,
|
||||
AssetEntity asset,
|
||||
File sourceFile,
|
||||
Map<String, IfdTag>? exifData,
|
||||
) async {
|
||||
// h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
|
||||
if (file.location == null ||
|
||||
@@ -298,6 +314,13 @@ Future<void> _decorateEnteFileData(
|
||||
file.location = props.location;
|
||||
}
|
||||
}
|
||||
if (Platform.isAndroid && exifData != null) {
|
||||
//Fix for missing location data in lower android versions.
|
||||
final Location? exifLocation = locationFromExif(exifData);
|
||||
if (Location.isValidLocation(exifLocation)) {
|
||||
file.location = exifLocation;
|
||||
}
|
||||
}
|
||||
if (file.title == null || file.title!.isEmpty) {
|
||||
_logger.warning("Title was missing ${file.tag}");
|
||||
file.title = await asset.titleAsync;
|
||||
@@ -330,9 +353,13 @@ Future<MetadataRequest> getPubMetadataRequest(
|
||||
);
|
||||
}
|
||||
|
||||
Future<MediaUploadData> _getMediaUploadDataFromAppCache(EnteFile file) async {
|
||||
Future<MediaUploadData> _getMediaUploadDataFromAppCache(
|
||||
EnteFile file,
|
||||
bool parseExif,
|
||||
) async {
|
||||
File sourceFile;
|
||||
Uint8List? thumbnailData;
|
||||
Map<String, IfdTag>? exifData;
|
||||
const bool isDeleted = false;
|
||||
final localPath = getSharedMediaFilePath(file);
|
||||
sourceFile = File(localPath);
|
||||
@@ -350,6 +377,7 @@ Future<MediaUploadData> _getMediaUploadDataFromAppCache(EnteFile file) async {
|
||||
Map<String, int>? dimensions;
|
||||
if (file.fileType == FileType.image) {
|
||||
dimensions = await getImageHeightAndWith(imagePath: localPath);
|
||||
exifData = await tryExifFromFile(sourceFile);
|
||||
} else if (thumbnailData != null) {
|
||||
// the thumbnail null check is to ensure that we are able to generate thum
|
||||
// for video, we need to use the thumbnail data with any max width/height
|
||||
@@ -368,6 +396,7 @@ Future<MediaUploadData> _getMediaUploadDataFromAppCache(EnteFile file) async {
|
||||
FileHashData(fileHash),
|
||||
height: dimensions?['height'],
|
||||
width: dimensions?['width'],
|
||||
exifData: exifData,
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.warning("failed to generate thumbnail", e, s);
|
||||
|
||||
@@ -170,7 +170,7 @@ Future<List<Uint8List>> generateFaceThumbnailsUsingCanvas(
|
||||
return faceThumbnails;
|
||||
} catch (e, s) {
|
||||
_logger.severe(
|
||||
'Error generating face thumbnails. cropImage problematic input argument: ${faceBoxes[i]}',
|
||||
'Error generating face thumbnails. cropImage problematic input argument: ${i}th facebox: ${faceBoxes[i].toString()}',
|
||||
e,
|
||||
s,
|
||||
);
|
||||
|
||||
@@ -165,9 +165,9 @@ Future<List<EnteFile>> convertIncomingSharedMediaToFile(
|
||||
enteFile.fileType =
|
||||
media.type == SharedMediaType.image ? FileType.image : FileType.video;
|
||||
if (enteFile.fileType == FileType.image) {
|
||||
final exifTime = await getCreationTimeFromEXIF(ioFile, null);
|
||||
if (exifTime != null) {
|
||||
enteFile.creationTime = exifTime.microsecondsSinceEpoch;
|
||||
final dateResult = await tryParseExifDateTime(ioFile, null);
|
||||
if (dateResult != null && dateResult.time != null) {
|
||||
enteFile.creationTime = dateResult.time!.microsecondsSinceEpoch;
|
||||
}
|
||||
} else if (enteFile.fileType == FileType.video) {
|
||||
enteFile.duration = (media.duration ?? 0) ~/ 1000;
|
||||
|
||||
@@ -192,7 +192,7 @@ Future<void> _downloadAndDecryptThumbnail(FileDownloadItem item) async {
|
||||
.data;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioError && CancelToken.isCancel(e)) {
|
||||
if (e is DioException && CancelToken.isCancel(e)) {
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
|
||||
@@ -7,7 +7,7 @@ environment:
|
||||
|
||||
dependencies:
|
||||
collection:
|
||||
dio: ^4.0.6
|
||||
dio: ^5.8.0+1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
shared_preferences: ^2.0.5
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -21,10 +29,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
|
||||
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.6"
|
||||
version: "5.8.0+1"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -273,5 +289,5 @@ packages:
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ environment:
|
||||
|
||||
dependencies:
|
||||
collection:
|
||||
dio: ^4.0.6
|
||||
dio: ^5.8.0+1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
shared_preferences: ^2.0.5
|
||||
|
||||
@@ -259,11 +259,11 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: forked_video_player_plus
|
||||
resolved-ref: "2d8908efe9d7533ec76abe2e59444547c4031f28"
|
||||
url: "https://github.com/ente-io/chewie.git"
|
||||
ref: latest-changes
|
||||
resolved-ref: "318339620ff3262b45645aaebc77c5f50a4b6e83"
|
||||
url: "https://github.com/prateekmedia/chewie.git"
|
||||
source: git
|
||||
version: "1.7.1"
|
||||
version: "1.10.0"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -337,6 +337,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.2"
|
||||
cronet_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cronet_http
|
||||
sha256: "3af9c4d57bf07ef4b307e77b22be4ad61bea19ee6ff65e62184863f3a09f1415"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
cross_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -361,6 +369,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
cupertino_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cupertino_http
|
||||
sha256: "6fcf79586ad872ddcd6004d55c8c2aab3cdf0337436e8f99837b1b6c30665d0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -421,10 +437,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
|
||||
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.6"
|
||||
version: "5.8.0+1"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
dots_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1173,6 +1197,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
http_profile:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_profile
|
||||
sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1338,6 +1370,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: f377c585ea9c08d48b427dc2e03780af2889d1bb094440da853c6883c1acba4b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1702,6 +1742,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
native_dio_adapter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: native_dio_adapter
|
||||
sha256: "7420bc9517b2abe09810199a19924617b45690a44ecfb0616ac9babc11875c03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
native_video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1735,6 +1783,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "62e79ab8c3ed6f6a340ea50dd48d65898f5d70425d404f0d99411f6e56e04584"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1779,18 +1835,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
|
||||
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
version: "8.2.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
|
||||
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.1.0"
|
||||
page_transition:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2859,18 +2915,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d
|
||||
sha256: "36c88af0b930121941345306d259ec4cc4ecca3b151c02e3a9e71aede83c615e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
version: "1.2.10"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus_platform_interface
|
||||
sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16"
|
||||
sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
wallpaper_manager_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -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: 0.9.95+995
|
||||
version: 0.9.98+998
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -30,8 +30,8 @@ dependencies:
|
||||
cached_network_image: ^3.0.0
|
||||
chewie:
|
||||
git:
|
||||
url: https://github.com/ente-io/chewie.git
|
||||
ref: forked_video_player_plus
|
||||
url: https://github.com/prateekmedia/chewie.git
|
||||
ref: latest-changes
|
||||
collection: # dart
|
||||
computer:
|
||||
git: "https://github.com/ente-io/computer.git"
|
||||
@@ -42,7 +42,7 @@ dependencies:
|
||||
dart_ui_isolate: ^1.1.1
|
||||
defer_pointer: ^0.0.2
|
||||
device_info_plus: ^9.0.3
|
||||
dio: ^4.0.6
|
||||
dio: ^5.8.0+1
|
||||
dots_indicator: ^2.0.0
|
||||
dotted_border: ^2.1.0
|
||||
dropdown_button2: ^2.0.0
|
||||
@@ -130,6 +130,7 @@ dependencies:
|
||||
git: "https://github.com/ente-io/motionphoto.git"
|
||||
move_to_background: ^1.0.2
|
||||
nanoid: ^1.0.0
|
||||
native_dio_adapter: ^1.4.0
|
||||
native_video_player:
|
||||
git:
|
||||
url: https://github.com/ashilkn/native_video_player.git
|
||||
@@ -141,7 +142,7 @@ dependencies:
|
||||
url: https://github.com/ente-io/onnxruntime.git
|
||||
ref: ios_only
|
||||
open_mail_app: ^0.4.5
|
||||
package_info_plus: ^4.1.0
|
||||
package_info_plus:
|
||||
page_transition: ^2.0.2
|
||||
panorama:
|
||||
git:
|
||||
|
||||
@@ -293,6 +293,7 @@ func main() {
|
||||
BillingCtrl: billingController,
|
||||
UserRepo: userRepo,
|
||||
UserCacheCtrl: userCacheCtrl,
|
||||
UsageRepo: usageRepo,
|
||||
}
|
||||
|
||||
publicCollectionCtrl := &controller.PublicCollectionController{
|
||||
@@ -621,6 +622,7 @@ func main() {
|
||||
familiesJwtAuthAPI.GET("/family/members", familyHandler.FetchMembers)
|
||||
familiesJwtAuthAPI.DELETE("/family/remove-member/:id", familyHandler.RemoveMember)
|
||||
familiesJwtAuthAPI.DELETE("/family/revoke-invite/:id", familyHandler.RevokeInvite)
|
||||
familiesJwtAuthAPI.POST("/family/modify-storage", familyHandler.ModifyStorageLimit)
|
||||
|
||||
emergencyHandler := &api.EmergencyHandler{
|
||||
Controller: emergencyCtrl,
|
||||
@@ -943,7 +945,7 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR
|
||||
}
|
||||
})
|
||||
|
||||
schedule(c, "@every 10m", func() {
|
||||
schedule(c, "@every 8m", func() {
|
||||
fileController.CleanupDeletedFiles()
|
||||
})
|
||||
schedule(c, "@every 101s", func() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user