Merge branch 'main' into usearch_again

This commit is contained in:
laurenspriem
2025-05-06 15:34:59 +05:30
703 changed files with 32908 additions and 14545 deletions

View File

@@ -26,6 +26,20 @@ body:
label: Version
description: The version can be seen at the bottom of settings.
placeholder: e.g. v1.2.3
- type: input
attributes:
label: Last working version
description: >
The version where the feature was last known to be working. It is
fine if you don't remember the exact version (mention roughly
then), but if there just isn't a last known working version, then
it is likely that what is being reported is not an issue but a
feature request. The difference between the two categories is not
just semantic - feature requests use GitHub discussions and so can
be [upvoted by the
community](https://github.com/ente-io/ente/discussions/categories/feature-requests)
(issues can't be).
placeholder: e.g. v1.2.3
- type: dropdown
attributes:
label: What product are you using?

View File

@@ -36,7 +36,7 @@ permissions:
jobs:
build-linux-latest:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
defaults:
run:
@@ -93,7 +93,7 @@ jobs:
- name: Install dependencies for desktop build
run: |
sudo apt-get update -y
sudo apt-get install -y libsecret-1-dev libsodium-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff6 xz-utils libarchive-tools libcurl4-openssl-dev
sudo apt-get install -y libsecret-1-dev libsodium-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff5 xz-utils libarchive-tools libcurl4-openssl-dev
sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu'
- name: Install appimagetool

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "desktop/yarn.lock"

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "docs/yarn.lock"

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "docs/yarn.lock"

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "infra/staff/yarn.lock"

View File

@@ -25,7 +25,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "infra/staff/yarn.lock"

View File

@@ -8,7 +8,7 @@ on:
workflow_dispatch:
permissions:
contents: read
contents: write # for pushing the `ghcr/server` branch
packages: write
jobs:

View File

@@ -33,7 +33,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "web/yarn.lock"

View File

@@ -33,7 +33,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "web/yarn.lock"

View File

@@ -41,7 +41,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "web/yarn.lock"

View File

@@ -37,7 +37,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "web/yarn.lock"

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: "yarn"
cache-dependency-path: "web/yarn.lock"

View File

@@ -8,7 +8,7 @@ on:
workflow_dispatch:
permissions:
contents: read
contents: write # for pushing the `ghcr/web` branch
packages: write
jobs:

View File

@@ -35,7 +35,7 @@ Private sharing. Collaborative albums. Family plans. Easy import, easier export.
Background uploads. The list goes on. And of course, all of this, while being
fully end-to-end encrypted across platforms.
Ente Photos is a paid service, but we offer 5GB of free storage.
Ente Photos is a paid service, but we offer 10GB of free storage.
You can also clone this repository and choose to self-host.
<br />

3
auth/.fvmrc Normal file
View File

@@ -0,0 +1,3 @@
{
"flutter": "3.24.3"
}

5
auth/.gitignore vendored
View File

@@ -41,4 +41,7 @@ lib/generated_plugin_registrant.dart
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
android/key.properties
dist/
dist/
# FVM Version Cache
.fvm/

View File

@@ -5,6 +5,8 @@ gradle-wrapper.jar
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
/app/.cxx/
/.kotlin/
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="0%" />
</foreground>
<monochrome>
<inset
android:drawable="@drawable/ic_launcher_monochrome"
android:inset="0%" />
</monochrome>
</adaptive-icon>

View File

@@ -132,6 +132,10 @@
"Binance US"
]
},
{
"title": "Bitkub",
"slug": "bitkub"
},
{
"title": "Bitfinex"
},
@@ -183,6 +187,9 @@
"title": "Bluesky",
"slug": "blue_sky"
},
{
"title": "bonify"
},
{
"title": "Booking",
"altNames": [
@@ -208,6 +215,13 @@
{
"title": "Bugzilla"
},
{
"title": "Bundesagentur für Arbeit",
"slug": "bundesagentur_fur_arbeit",
"altNames": [
"Agentur für Arbeit"
]
},
{
"title": "ButterflyMX",
"slug": "butterflymx"
@@ -285,6 +299,15 @@
"title": "CSGORoll",
"slug": "csgoroll"
},
{
"title": "Cwallet",
"altNames": [
"cwallet",
"c-wallet",
"c wallet",
"cwallet.com"
]
},
{
"title": "DCS",
"altNames": [
@@ -376,6 +399,13 @@
],
"hex": "858585"
},
{
"title": "Fanatical",
"slug": "fanatical",
"altNames": [
"FANATICAL"
]
},
{
"title": "Fastmail"
},
@@ -405,6 +435,9 @@
"title": "Firefox",
"slug": "mozilla"
},
{
"title": "fortrabbit"
},
{
"title": "ForUsAll"
},
@@ -499,12 +532,19 @@
"slug": "id_me"
},
{
"title": "Infomaniak"
"title": "ImmoScout24",
"slug": "immo_scout_24",
"altNames": [
"ImmobilienScout24"
]
},
{
"title": "Impact.com",
"slug": "impact"
},
{
"title": "Infomaniak"
},
{
"title": "ING"
},
@@ -606,8 +646,7 @@
},
{
"title": "LinkedIn",
"slug": "linkedin",
"hex": "2596be"
"slug": "linkedin"
},
{
"title": "Linux.Do",
@@ -658,6 +697,14 @@
"mathworks"
]
},
{
"title": "Mbin",
"altNames": [
"kbin",
"thebrainbin",
"gehirneimer"
]
},
{
"title": "Mercado Livre",
"slug": "mercado_livre",
@@ -888,6 +935,13 @@
{
"title": "PostNL"
},
{
"title": "Postmark",
"slug": "postmarkapp",
"altNames": [
"postmarkapp"
]
},
{
"title": "PostScan Mail",
"slug": "postscanmail",
@@ -952,6 +1006,15 @@
"slug": "realvnc",
"hex": "488aec"
},
{
"title": "RedotPay",
"altNames": [
"redotpay",
"redot pay",
"redot-pay",
"redotpay.com"
]
},
{
"title": "Registro br",
"slug": "registro_br",

View File

@@ -1 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 150 150"><defs><style>.e{fill:#2a54ff;}.f{fill:url(#d);}.g{fill:none;}</style><linearGradient id="d" x1="17.68" y1="116.45" x2="132.14" y2="32.11" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2a54ff"/><stop offset=".52" stop-color="#2143cb"/><stop offset="1" stop-color="#2a54ff"/></linearGradient></defs><g id="b"><path id="c" class="g" d="M0,0H150V150H0V0Z"/></g><path class="f" d="M140.2,22.33c-25.18-.09-49.79,10.83-66.63,29.47-6.06,6.27-10.1,13.95-14.96,21.06-11.64,15.93-29.81,25.14-49.5,25.13h0v28.65h0c25.17,.1,49.78-10.86,66.63-29.5,6.03-6.27,10.13-13.94,14.96-21.06,11.64-15.91,29.81-25.12,49.5-25.11V22.33h0Z"/><path class="e" d="M140.2,97.99c-19.68,0-37.86-9.2-49.5-25.11-4.81-7.12-8.92-14.78-14.94-21.06C58.95,33.18,34.3,22.24,9.13,22.35h0v28.65h0c21.8-.11,42.05,11.62,53.01,30.46,3.22,5.62,7.06,10.9,11.45,15.74,16.83,18.63,41.46,29.59,66.63,29.5l-.02-28.7h0Z"/></svg>
<?xml version='1.0' encoding='utf-8'?>
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 150 150">
<defs>
<linearGradient id="d" x1="17.68" y1="116.45" x2="132.14" y2="32.11"
gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#2a54ff" />
<stop offset=".52" stop-color="#2143cb" />
<stop offset="1" stop-color="#2a54ff" />
</linearGradient>
</defs>
<g id="b">
<path id="c" d="M0,0H150V150H0V0Z" fill="none" />
</g>
<path
d="M140.2,22.33c-25.18-.09-49.79,10.83-66.63,29.47-6.06,6.27-10.1,13.95-14.96,21.06-11.64,15.93-29.81,25.14-49.5,25.13h0v28.65h0c25.17,.1,49.78-10.86,66.63-29.5,6.03-6.27,10.13-13.94,14.96-21.06,11.64-15.91,29.81-25.12,49.5-25.11V22.33h0Z"
fill="url(#d)" />
<path
d="M140.2,97.99c-19.68,0-37.86-9.2-49.5-25.11-4.81-7.12-8.92-14.78-14.94-21.06C58.95,33.18,34.3,22.24,9.13,22.35h0v28.65h0c21.8-.11,42.05,11.62,53.01,30.46,3.22,5.62,7.06,10.9,11.45,15.74,16.83,18.63,41.46,29.59,66.63,29.5l-.02-28.7h0Z"
fill="#2a54ff" />
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 245.73 156" xmlns="http://www.w3.org/2000/svg"><g fill="#4cba64"><path d="m167.87 0a23.32 23.32 0 0 0 0 33l44.89 44.9-45 45-22.89-22.9a23.34 23.34 0 0 0 -33 0l55.86 55.87 78-78z"/><circle cx="167.87" cy="78" r="16"/><path d="m77.87 156a23.34 23.34 0 0 0 0-33l-44.87-44.9 45-45 22.87 22.9a23.34 23.34 0 0 0 33 0l-55.87-55.87-78 78z"/><circle cx="77.87" cy="78" r="16"/></g></svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="logosandtypes_com" data-name="logosandtypes com" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 150 150">
<defs>
<style>
.cls-1 {
fill: #101010;
}
.cls-2 {
fill: none;
}
.cls-3 {
fill: url(#linear-gradient);
}
</style>
<linearGradient id="linear-gradient" x1="186.97" y1="96.04" x2="45.7" y2="96.04" gradientTransform="translate(0 150.11) scale(1 -1)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#165cc3"/>
<stop offset="1" stop-color="#3ddabb"/>
</linearGradient>
</defs>
<g id="Layer_3" data-name="Layer 3">
<g id="Layer_2" data-name="Layer 2">
<path id="Layer_3-2" data-name="Layer 3-2" class="cls-2" d="M0,0H150V150H0V0Z"/>
</g>
</g>
<path class="cls-1" d="M111.63,75.01c.06,.86,.08,1.72,.08,2.59,0,20.52-16.62,37.16-37.14,37.16-20.52,0-37.16-16.62-37.16-37.14,0-20.52,16.62-37.16,37.14-37.16,0,0,.02,0,.02,0,1.61,0,3.22,.1,4.82,.32l12.7-17.11C62.3,14,30.31,30.3,20.63,60.09c-9.68,29.79,6.62,61.78,36.41,71.47,29.79,9.68,61.78-6.62,71.47-36.41,4.29-13.2,3.59-27.52-1.97-40.24l-14.9,20.11Z"/>
<polygon class="cls-3" points="120.26 4.82 74.49 66.53 62.93 53.99 45.67 69.89 76.4 103.32 149.5 4.82 120.26 4.82"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Created with Inkscape (http://www.inkscape.org/) by Marsupilami -->
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="768" height="768" viewBox="-4.3240767 -4.3240767 152.8084434 152.7840434" id="svg7076">
<defs id="defs7078"/>
<path d="M 0,72.07202 C 0,32.27318 32.2935,0 72.08013,0 c 39.78662,0 72.08017,32.27318 72.08017,72.07202 0,39.80291 -32.29355,72.06387 -72.08017,72.06387 -17.63317,0 -33.75958,-6.32434 -46.30232,-16.82687 11.769,-19.46163 46.13944,-77.28864 46.13944,-77.28864 l 17.0223,28.5022 c 0,0 -8.95912,0.0448 -17.06303,0 -8.14464,-0.0448 -10.46588,1.7063 -14.00878,7.11027 -2.9321,4.4877 -9.85505,16.21193 -10.01793,16.42776 -0.81448,1.29093 -0.3258,2.54114 1.58818,2.54114 l 55.18001,0 28.01759,0 c 1.66968,0 2.64704,-1.16875 1.58822,-2.6226 L 73.34255,2.43932 c -0.81447,-1.37236 -2.11759,-1.25021 -2.85061,0 L 8.4704,105.97411 C 3.09495,95.87068 0,84.32969 0,72.07202" id="path8406" style="fill:#ec1c23;fill-rule:nonzero;stroke:none"/>
</svg>
<!-- version: 20110311, original size: 144.16029 144.13589, border: 3% -->

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,130 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<style type="text/css">
.st0{fill:#C5C8CA;}
.st1{fill:#9DA4A8;}
.st2{fill:#B7BBBD;}
.st3{fill:#CBCFD1;}
.st4{fill:#BBBFC2;}
.st5{fill:#CACDCE;}
.st6{fill:#BFC3C5;}
.st7{fill:#BCC0C2;}
.st8{fill:#BDC1C4;}
.st9{fill:#C7CACC;}
.st10{fill:url(#SVGID_1_);}
.st11{fill:#FFFFFF;}
.st12{fill:#B8BCBF;}
.st13{fill:#C4C7C9;}
.st14{fill:#C1C5C7;}
.st15{fill:url(#SVGID_00000003093454306001190100000011813141018663887528_);}
.st16{fill:url(#SVGID_00000017503418065689336600000007511615486600436881_);}
.st17{fill:url(#SVGID_00000057845154053127761930000017803385842445649033_);}
.st18{fill:url(#SVGID_00000156571711195124538550000006687723982713171592_);}
.st19{fill:#DF3030;}
.st20{fill:url(#SVGID_00000001636660173574603980000008731795684331757470_);}
.st21{fill:#17181C;}
.st22{fill:url(#SVGID_00000180343933242210086490000003762167186865041053_);}
.st23{fill:url(#SVGID_00000015338415700440354440000005681408021599925436_);}
</style>
<g>
<path class="st0" d="M14.4,29.5c0.1,0,0.1,0,0.2,0c0.1,0,0.2,0,0.2,0H14.4z"/>
<path class="st1" d="M15.3,29.5h0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0L15.3,29.5
C15.2,29.5,15.3,29.5,15.3,29.5z"/>
<path class="st2" d="M15.3,29.5L15.3,29.5l-0.2,0C15.2,29.5,15.2,29.5,15.3,29.5z"/>
<path class="st3" d="M15.5,29.5L15.5,29.5L15.5,29.5L15.5,29.5L15.5,29.5z"/>
<path class="st0" d="M14.1,29.5c0.1,0,0.1,0,0.2,0H14.1z"/>
<path class="st4" d="M13.9,29.5C13.9,29.5,14,29.5,13.9,29.5c0.1,0,0.1,0,0.2,0H13.9z"/>
<path class="st5" d="M13.6,29.5C13.6,29.5,13.6,29.5,13.6,29.5c0.1,0,0.1,0,0.1,0H13.6z"/>
<path class="st6" d="M13.7,29.5C13.8,29.5,13.8,29.5,13.7,29.5c0.1,0,0.1,0,0.1,0H13.7z"/>
<path class="st7" d="M13.3,29.4C13.3,29.4,13.3,29.4,13.3,29.4C13.4,29.4,13.4,29.4,13.3,29.4L13.3,29.4z"/>
<path class="st8" d="M13.4,29.5C13.4,29.4,13.5,29.4,13.4,29.5C13.5,29.4,13.5,29.4,13.4,29.5L13.4,29.5z"/>
<path class="st8" d="M13.1,29.4C13.1,29.4,13.1,29.4,13.1,29.4C13.1,29.4,13.1,29.4,13.1,29.4L13.1,29.4z"/>
<path class="st9" d="M13.2,29.4C13.2,29.4,13.2,29.4,13.2,29.4C13.2,29.4,13.2,29.4,13.2,29.4C13.2,29.4,13.2,29.4,13.2,29.4
C13.3,29.4,13.3,29.4,13.2,29.4L13.2,29.4z"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="21.8812" y1="-88.078" x2="8.2545" y2="-104.6955" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
<stop offset="0" style="stop-color:#020037"/>
<stop offset="1" style="stop-color:#050F62"/>
</linearGradient>
<path class="st10" d="M15,0.4C11.1,0.4,7.5,2,4.7,4.7C2,7.4,0.5,11.1,0.5,15c0,1.7,0.3,3.4,0.9,5.1c0.3,0,0.5,0,0.8,0
c2.9,0,5.8,0.9,8.2,2.6c2.4,1.7,4.2,4.1,5.1,6.9c3.8-0.1,7.4-1.7,10-4.4c2.6-2.7,4.1-6.4,4.1-10.1c0-3.9-1.5-7.6-4.3-10.3
C22.6,2,18.9,0.4,15,0.4"/>
<path class="st11" d="M20.7,22.5C20.7,22.5,20.7,22.5,20.7,22.5L20.7,22.5c0,0.4,0.1,0.8,0.3,1c0.2,0.2,0.6,0.3,1,0.3c0,0,0,0,0,0
c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c-0.4,0-0.7,0.1-1,0.3c-0.2,0.2-0.3,0.6-0.3,1c0,0,0,0,0,0c0,0,0,0,0,0
c0,0,0,0,0,0c0,0,0,0,0,0h0l0,0c0-0.4-0.1-0.7-0.3-1c-0.2-0.2-0.6-0.3-1-0.3c0,0,0,0,0,0l0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0
c0,0,0,0,0,0c0.4,0,0.7-0.1,1-0.3S20.7,22.9,20.7,22.5C20.7,22.5,20.7,22.5,20.7,22.5C20.7,22.5,20.7,22.5,20.7,22.5z"/>
<path class="st11" d="M6.9,15.5C6.9,15.5,6.9,15.5,6.9,15.5L6.9,15.5c0,0.4,0.1,0.8,0.3,1c0.2,0.2,0.6,0.3,1,0.3c0,0,0,0,0,0
c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c-0.4,0-0.7,0.1-1,0.3c-0.2,0.2-0.3,0.6-0.3,1c0,0,0,0,0,0c0,0,0,0,0,0
c0,0,0,0,0,0c0,0,0,0,0,0h0l0,0c0-0.4-0.1-0.7-0.3-1c-0.2-0.2-0.6-0.3-1-0.3c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
c0,0,0,0,0,0c0.4,0,0.7-0.1,1-0.3C6.8,16.2,6.9,15.9,6.9,15.5C6.9,15.5,6.9,15.5,6.9,15.5C6.9,15.5,6.9,15.5,6.9,15.5z"/>
<path class="st11" d="M10.6,4.1L10.6,4.1C10.7,4.1,10.7,4.1,10.6,4.1c0,0.3,0.1,0.5,0.3,0.7c0.2,0.2,0.4,0.3,0.7,0.2h0v0l0,0l0,0
l0,0l0,0c-0.3,0-0.5,0.1-0.7,0.2c-0.2,0.2-0.3,0.4-0.2,0.7l0,0l0,0l0,0l0,0h0v0c0-0.3-0.1-0.5-0.2-0.7C10.2,5.1,10,5,9.7,5.1h0v0v0
h0C10,5,10.2,5,10.4,4.8C10.6,4.6,10.7,4.3,10.6,4.1C10.6,4.1,10.6,4.1,10.6,4.1C10.6,4.1,10.6,4.1,10.6,4.1z"/>
<path class="st12" d="M12.8,29.4C12.8,29.4,12.8,29.4,12.8,29.4C12.8,29.4,12.8,29.4,12.8,29.4C12.8,29.4,12.8,29.4,12.8,29.4
C12.8,29.4,12.8,29.4,12.8,29.4L12.8,29.4z"/>
<path class="st13" d="M13,29.4C13,29.4,13,29.4,13,29.4C13,29.4,13,29.4,13,29.4L13,29.4z"/>
<path class="st14" d="M12.9,29.4C12.9,29.4,12.9,29.4,12.9,29.4C12.9,29.4,12.9,29.4,12.9,29.4L12.9,29.4z"/>
<linearGradient id="SVGID_00000173122186048074043340000017421439166240502921_" gradientUnits="userSpaceOnUse" x1="19.2457" y1="-89.3156" x2="22.9553" y2="-91.7188" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
<stop offset="0" style="stop-color:#E5E5E5"/>
<stop offset="1" style="stop-color:#B7B8C1"/>
</linearGradient>
<path style="fill:url(#SVGID_00000173122186048074043340000017421439166240502921_);" d="M21.8,1.2c-1.4,0.7-3,1.9-4.4,4.2
c-2.5,3.9-3.2,7.4-3.2,7.4L16,14l0.3,0.2l1.9,1.2c0,0,2.9-2,5.4-5.9c1.5-2.3,2-4.3,2-5.8c-0.8-0.1-1.5-0.4-2.2-0.8
C22.8,2.5,22.2,1.9,21.8,1.2z"/>
<linearGradient id="SVGID_00000127763695479642710240000017533313096818365313_" gradientUnits="userSpaceOnUse" x1="21.2378" y1="-99.9826" x2="19.0472" y2="-97.8815" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
<stop offset="0" style="stop-color:#EC4F4F"/>
<stop offset="1" style="stop-color:#A91919"/>
</linearGradient>
<path style="fill:url(#SVGID_00000127763695479642710240000017533313096818365313_);" d="M20.8,16.8c0.9-1.4,0.3-3.2,0-3.8
c-0.7,0.8-1.5,1.5-2.3,2.1c0.1,0.4,0.3,0.8,0.3,1.2c0,0.1,0,0.2-0.1,0.3c-0.4,0.6-0.8,1.3-1.1,2c-0.1,0.1-0.1,0.2-0.1,0.3
c-0.1,0.2-0.1,0.3,0,0.5c0,0.3,0.2,0.5,0.3,0.8c0,0,0.1,0.1,0.1,0.1c0.1,0,0.1,0.1,0.2,0.1s0.1,0,0.2-0.1c0.1-0.1,0.3-0.2,0.4-0.4
C19.5,19,19.8,18.5,20.8,16.8z"/>
<linearGradient id="SVGID_00000060717637781723915790000002744012061535479481_" gradientUnits="userSpaceOnUse" x1="11.3158" y1="-99.2586" x2="14.8122" y2="-101.5237" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
<stop offset="0" style="stop-color:#F2A518"/>
<stop offset="1" style="stop-color:#F4E23E"/>
</linearGradient>
<path style="fill:url(#SVGID_00000060717637781723915790000002744012061535479481_);" d="M15.1,15.7l-1.7-1.1c-2,3.1-3.3,7-2.4,7.5
c0.9,0.6,3.9-2.2,5.9-5.3L15.1,15.7z"/>
<linearGradient id="SVGID_00000070084874335106853820000008402293642909580433_" gradientUnits="userSpaceOnUse" x1="-4386.2534" y1="747.6443" x2="-4497.9517" y2="769.0099" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
<stop offset="0" style="stop-color:#EC4F4F"/>
<stop offset="1" style="stop-color:#A91919"/>
</linearGradient>
<path style="fill:url(#SVGID_00000070084874335106853820000008402293642909580433_);" d="M15.2,9.5c-0.7-0.1-2.5,0.1-3.4,1.5
c-1.1,1.6-1.5,2.1-2,3.2c-0.1,0.2-0.1,0.3-0.2,0.5c0,0.1,0,0.1,0,0.2C9.6,15,9.7,15,9.7,15c0,0,0.1,0,0.2,0.1c0.3,0.1,0.6,0,0.8,0
c0.2,0,0.3-0.1,0.4-0.2c0.1-0.1,0.2-0.2,0.3-0.3c0.5-0.6,0.9-1.2,1.3-1.8c0.1-0.1,0.2-0.2,0.3-0.2c0.4-0.1,0.8-0.1,1.2-0.2l0,0
C14.5,11.4,14.8,10.4,15.2,9.5z"/>
<path class="st19" d="M25,0.6c-0.2-0.1-1.5-0.2-3.2,0.7c0.4,0.7,1,1.2,1.6,1.7c0.7,0.4,1.4,0.7,2.2,0.8C25.7,1.9,25.1,0.7,25,0.6z"
/>
<path class="st19" d="M18.4,15.5L14,12.7c-0.1,0-0.1,0-0.2,0l-0.9,1.4c0,0.1,0,0.1,0,0.2l4.4,2.8c0.1,0,0.1,0,0.2,0l0.9-1.4
C18.4,15.6,18.4,15.6,18.4,15.5z"/>
<linearGradient id="SVGID_00000044894753735506851200000013592864944465274029_" gradientUnits="userSpaceOnUse" x1="14.9436" y1="-95.9217" x2="16.3716" y2="-96.8468" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
<stop offset="0" style="stop-color:#B71E1E"/>
<stop offset="0.44" style="stop-color:#DF3030"/>
<stop offset="1" style="stop-color:#C51D1D"/>
</linearGradient>
<path style="fill:url(#SVGID_00000044894753735506851200000013592864944465274029_);" d="M17.8,11.6c-0.4-0.2-2.1,1.6-3.2,3.3
c-0.8,1.2-1.4,3-1.1,3.2c0.4,0.2,1.7-1,2.5-2.3C17.1,14.2,18.1,11.9,17.8,11.6z"/>
<path class="st21" d="M21.2,8.6c1.3,0,2.3-1,2.3-2.3s-1-2.3-2.3-2.3c-1.3,0-2.3,1-2.3,2.3S20,8.6,21.2,8.6z"/>
<linearGradient id="SVGID_00000090987122570624474440000002432161440392897685_" gradientUnits="userSpaceOnUse" x1="20.068" y1="-87.0655" x2="22.3556" y2="-88.5473" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
<stop offset="0" style="stop-color:#CED1EC"/>
<stop offset="1" style="stop-color:#FFFFFF"/>
</linearGradient>
<path style="fill:url(#SVGID_00000090987122570624474440000002432161440392897685_);" d="M21.2,7.7c0.8,0,1.4-0.6,1.4-1.4
S22,5,21.2,5c-0.8,0-1.4,0.6-1.4,1.4S20.5,7.7,21.2,7.7z"/>
<linearGradient id="SVGID_00000044151119195171880090000016489263670362291109_" gradientUnits="userSpaceOnUse" x1="14.4192" y1="-110.4727" x2="2.0973" y2="-101.7197" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
<stop offset="0" style="stop-color:#B7B7BD"/>
<stop offset="0.68" style="stop-color:#EFEFEF"/>
</linearGradient>
<path style="fill:url(#SVGID_00000044151119195171880090000016489263670362291109_);" d="M2.1,20c-0.3,0-0.5,0-0.8,0
c1,2.8,2.9,5.2,5.3,6.9s5.3,2.6,8.3,2.6c0.1,0,0.3,0,0.4,0c-0.9-2.8-2.7-5.2-5.1-6.9C7.9,20.9,5.1,20,2.1,20z"/>
</g>
</svg>
<svg xml:space="preserve" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1"
viewBox="0 0 30 30">
<path d="M14.4 29.5h.4z" fill="#c5c8ca" />
<path fill="#9da4a8" d="M15.3 29.5h.1zc-.1 0 0 0 0 0" />
<path fill="#b7bbbd" d="M15.3 29.5h-.2z" />
<path d="M14.1 29.5h.2z" fill="#c5c8ca" />
<path fill="#bbbfc2" d="M13.9 29.5s.1 0 0 0h.2z" />
<path fill="#cacdce" d="M13.6 29.5h.1z" />
<path fill="#bfc3c5" d="M13.7 29.5q.15 0 0 0h.1z" />
<path fill="#bcc0c2" d="M13.3 29.4q.15 0 0 0" />
<path fill="#bdc1c4" d="M13.4 29.5c0-.1.1-.1 0 0q.15-.15 0 0m-.3-.1" />
<path fill="#c7cacc" d="M13.2 29.4q.15 0 0 0" />
<linearGradient id="SVGID_1_" x1="21.8812" x2="8.2545" y1="-88.078" y2="-104.6955" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#020037" />
<stop offset="1" stop-color="#050f62" />
</linearGradient>
<path fill="url(#SVGID_1_)" d="M15 .4C11.1.4 7.5 2 4.7 4.7 2 7.4.5 11.1.5 15q0 2.55.9 5.1h.8c2.9 0 5.8.9 8.2 2.6s4.2 4.1 5.1 6.9c3.8-.1 7.4-1.7 10-4.4s4.1-6.4 4.1-10.1c0-3.9-1.5-7.6-4.3-10.3C22.6 2 18.9.4 15 .4" />
<path fill="#fff" d="M20.7 22.5c0 .4.1.8.3 1s.6.3 1 .3c-.4 0-.7.1-1 .3-.2.2-.3.6-.3 1 0-.4-.1-.7-.3-1-.2-.2-.6-.3-1-.3.4 0 .7-.1 1-.3s.3-.6.3-1m-13.8-7c0 .4.1.8.3 1s.6.3 1 .3c-.4 0-.7.1-1 .3-.2.2-.3.6-.3 1 0-.4-.1-.7-.3-1-.2-.2-.6-.3-1-.3.4 0 .7-.1 1-.3.2-.3.3-.6.3-1m3.7-11.4q.15 0 0 0c0 .3.1.5.3.7s.4.3.7.2c-.3 0-.5.1-.7.2-.2.2-.3.4-.2.7 0-.3-.1-.5-.2-.7-.3-.1-.5-.2-.8-.1.3-.1.5-.1.7-.3s.3-.5.2-.7" />
<linearGradient id="SVGID_00000173122186048074043340000017421439166240502921_" x1="19.2457" x2="22.9553" y1="-89.3156" y2="-91.7188" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#e5e5e5" />
<stop offset="1" stop-color="#b7b8c1" />
</linearGradient>
<path fill="url(#SVGID_00000173122186048074043340000017421439166240502921_)" d="M21.8 1.2c-1.4.7-3 1.9-4.4 4.2-2.5 3.9-3.2 7.4-3.2 7.4L16 14l.3.2 1.9 1.2s2.9-2 5.4-5.9c1.5-2.3 2-4.3 2-5.8-.8-.1-1.5-.4-2.2-.8-.6-.4-1.2-1-1.6-1.7" />
<linearGradient id="SVGID_00000127763695479642710240000017533313096818365313_" x1="21.2378" x2="19.0472" y1="-99.9826" y2="-97.8815" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ec4f4f" />
<stop offset="1" stop-color="#a91919" />
</linearGradient>
<path fill="url(#SVGID_00000127763695479642710240000017533313096818365313_)" d="M20.8 16.8c.9-1.4.3-3.2 0-3.8-.7.8-1.5 1.5-2.3 2.1.1.4.3.8.3 1.2 0 .1 0 .2-.1.3-.4.6-.8 1.3-1.1 2-.1.1-.1.2-.1.3-.1.2-.1.3 0 .5 0 .3.2.5.3.8l.1.1c.1 0 .1.1.2.1s.1 0 .2-.1.3-.2.4-.4c.8-.9 1.1-1.4 2.1-3.1" />
<linearGradient id="SVGID_00000060717637781723915790000002744012061535479481_" x1="11.3158" x2="14.8122" y1="-99.2586" y2="-101.5237" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f2a518" />
<stop offset="1" stop-color="#f4e23e" />
</linearGradient>
<path fill="url(#SVGID_00000060717637781723915790000002744012061535479481_)" d="m15.1 15.7-1.7-1.1c-2 3.1-3.3 7-2.4 7.5.9.6 3.9-2.2 5.9-5.3z" />
<linearGradient id="SVGID_00000070084874335106853820000008402293642909580433_" x1="-4386.2534" x2="-4497.9517" y1="747.6443" y2="769.0099" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ec4f4f" />
<stop offset="1" stop-color="#a91919" />
</linearGradient>
<path fill="url(#SVGID_00000070084874335106853820000008402293642909580433_)" d="M15.2 9.5c-.7-.1-2.5.1-3.4 1.5-1.1 1.6-1.5 2.1-2 3.2-.1.2-.1.3-.2.5v.2c0 .1.1.1.1.1s.1 0 .2.1c.3.1.6 0 .8 0s.3-.1.4-.2l.3-.3c.5-.6.9-1.2 1.3-1.8.1-.1.2-.2.3-.2.4-.1.8-.1 1.2-.2.3-1 .6-2 1-2.9" />
<path fill="#df3030" d="M25 .6c-.2-.1-1.5-.2-3.2.7.4.7 1 1.2 1.6 1.7.7.4 1.4.7 2.2.8.1-1.9-.5-3.1-.6-3.2m-6.6 14.9L14 12.7h-.2l-.9 1.4v.2l4.4 2.8h.2l.9-1.4z" />
<linearGradient id="SVGID_00000044894753735506851200000013592864944465274029_" x1="14.9436" x2="16.3716" y1="-95.9217" y2="-96.8468" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b71e1e" />
<stop offset=".44" stop-color="#df3030" />
<stop offset="1" stop-color="#c51d1d" />
</linearGradient>
<path fill="url(#SVGID_00000044894753735506851200000013592864944465274029_)" d="M17.8 11.6c-.4-.2-2.1 1.6-3.2 3.3-.8 1.2-1.4 3-1.1 3.2.4.2 1.7-1 2.5-2.3 1.1-1.6 2.1-3.9 1.8-4.2" />
<path fill="#17181c" d="M21.2 8.6c1.3 0 2.3-1 2.3-2.3S22.5 4 21.2 4s-2.3 1-2.3 2.3 1.1 2.3 2.3 2.3" />
<linearGradient id="SVGID_00000090987122570624474440000002432161440392897685_" x1="20.068" x2="22.3556" y1="-87.0655" y2="-88.5473" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ced1ec" />
<stop offset="1" stop-color="#fff" />
</linearGradient>
<path fill="url(#SVGID_00000090987122570624474440000002432161440392897685_)" d="M21.2 7.7c.8 0 1.4-.6 1.4-1.4S22 5 21.2 5s-1.4.6-1.4 1.4.7 1.3 1.4 1.3" />
<linearGradient id="SVGID_00000044151119195171880090000016489263670362291109_" x1="14.4192" x2="2.0973" y1="-110.4727" y2="-101.7197" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b7b7bd" />
<stop offset=".68" stop-color="#efefef" />
</linearGradient>
<path fill="url(#SVGID_00000044151119195171880090000016489263670362291109_)" d="M2.1 20h-.8c1 2.8 2.9 5.2 5.3 6.9s5.3 2.6 8.3 2.6h.4c-.9-2.8-2.7-5.2-5.1-6.9C7.9 20.9 5.1 20 2.1 20" />
</svg>

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 399.76401 400"
preserveAspectRatio="xMinYMid"
aria-labelledby="navbar-fanatical-logo"
version="1.1"
id="svg2"
sodipodi:docname="Untitled.svg"
width="399.76401"
height="400"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:zoom="0.69295302"
inkscape:cx="205.64165"
inkscape:cy="207.08475"
inkscape:window-width="1920"
inkscape:window-height="938"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
fill="none"
id="g2">
<path
fill="#ff9800"
d="m 2.8756,166.0056 h 284.671 a 2.9981,2.9981 0 0 0 2.7221,-1.7424 l 25.8632,-56.0452 c 0.6946,-1.504 0.0391,-3.2867 -1.464,-3.9817 a 2.9968,2.9968 0 0 0 -1.258,-0.2767 L 24.4917,103.9952 C 58.4482,42.0187 124.261,0 199.882,0 c 110.3917,0 199.882,89.543 199.882,200 0,110.457 -89.4903,200 -199.882,200 C 89.4902,400 0,310.457 0,200 0,188.412 0.985,177.054 2.8756,166.0056 Z M 125.9256,328 c 0,2.2091 1.7898,4 3.9977,4 h 5.1722 l 62.8312,-79.0111 h 49.4291 a 2.9981,2.9981 0 0 0 2.722,-1.7422 l 25.835,-55.976 a 3.0015,3.0015 0 0 0 0.2761,-1.2577 c 0,-1.6569 -1.3423,-3 -2.9982,-3 H 125.9257 V 328 Z"
id="path1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><path d="M115 144c0 6-2 12-7 16s-9 7-16 7-11-3-16-7-6-10-6-16 2-12 6-16 10-7 16-7 12 3 16 7c5 5 7 10 7 16zm71-23-8 38-7 34a63 63 0 0 1-36 42c-5 2-11 3-17 3s-10 0-14-2l-7-4c-2-1-4-3-4-5l-1-6c0-4 1-7 3-9s6-4 10-4l9 2c3 1 4 4 6 6l4 8 3 7c3-3 5-7 7-13l7-22 16-75h-18l2-9h18l1-7c1-6 4-11 7-17s7-10 12-14c4-4 10-8 16-10s11-4 17-4l13 1 8 4 4 6 1 6a15 15 0 0 1-3 8l-4 4-7 1-8-2-6-6-4-8-3-7c-3 3-5 7-7 12l-6 23-2 10h22l-2 9h-22z"/></svg>

After

Width:  |  Height:  |  Size: 491 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,4 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 724 264">
<path
d="M38.53 260.65H.43V27.86h38.1zm86.46 2.77c-42.25 0-66.48-22.96-66.48-63V89.33h38.1v108.28c0 23.61 8.7 32.39 32.12 32.39 30.35 0 42.73-14.54 42.73-50.17v-90.5h38.1v171.33h-36.54v-29.91c-4.99 22.98-27.12 32.67-48.03 32.67zm347.2-2.77H434.4V149.87c0-22.5-7.01-30.87-25.88-30.87-24.28 0-37.11 14.45-37.11 41.79v99.86h-37.79V149.87c0-21.93-7.23-30.87-24.94-30.87-31.59 0-38.05 32.96-38.05 41.79v99.86h-38.1V89.33h36.54v29.96c6.49-21.02 27.02-33.71 47.72-33.71 20.69 0 38.09 7.9 45.64 33.71 10.13-26.76 28.35-33.71 50.15-33.71 37.88 0 59.61 18.88 59.61 51.81v123.26h0zm76.65 2.77c-52.62 0-61.55-33.45-61.55-50.52 0-20.1 8.83-38.21 27.93-45.55 8.41-3.11 16.52-5.43 24.84-7.1 7.33-1.47 18.64-3.03 26.91-4.17l2.73-.38c14.38-2 29.67-9.21 29.67-18.62 0-16-20.51-18.39-32.74-18.39-13.87 0-23.64 3.57-27.53 10.05-3.49 6.46-3.73 7.97-4.62 13.6l-.62 4.43h-38.1l.68-5.61c1.35-11.14 3.41-19.03 6.48-24.83 10.54-20.39 31.77-30.75 63.08-30.75 26.11 0 44.63 8.23 53.26 15.94 5.31 4.6 9.1 9.84 11.89 16.46 5.84 12.36 6.32 20.63 6.32 29.4v86.43c0 8.07.78 14.97 2.31 20.5l1.76 6.35h-38.91l-.7-4.19c-.5-2.96-.67-19.75-.88-26.23-8.99 23.61-28.27 33.18-52.21 33.18zm50.53-93.72c-7.97 6.11-20.47 9.6-38.62 13.23-31.27 5.78-36.54 13.06-36.54 27.22 0 12.5 10.63 20.26 27.75 20.26 33.23 0 47.41-15.48 47.41-51.77v-8.94zm124.2-105.51C688.46 64.19 660 35.73 660 .62c0 35.11-28.46 63.57-63.57 63.57h0c35.11 0 63.57 28.46 63.57 63.57h0c0-35.11 28.46-63.57 63.57-63.57z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 724 264">
<path
d="M38.53 260.65H.43V27.86h38.1zm86.46 2.77c-42.25 0-66.48-22.96-66.48-63V89.33h38.1v108.28c0 23.61 8.7 32.39 32.12 32.39 30.35 0 42.73-14.54 42.73-50.17v-90.5h38.1v171.33h-36.54v-29.91c-4.99 22.98-27.12 32.67-48.03 32.67zm347.2-2.77H434.4V149.87c0-22.5-7.01-30.87-25.88-30.87-24.28 0-37.11 14.45-37.11 41.79v99.86h-37.79V149.87c0-21.93-7.23-30.87-24.94-30.87-31.59 0-38.05 32.96-38.05 41.79v99.86h-38.1V89.33h36.54v29.96c6.49-21.02 27.02-33.71 47.72-33.71 20.69 0 38.09 7.9 45.64 33.71 10.13-26.76 28.35-33.71 50.15-33.71 37.88 0 59.61 18.88 59.61 51.81v123.26h0zm76.65 2.77c-52.62 0-61.55-33.45-61.55-50.52 0-20.1 8.83-38.21 27.93-45.55 8.41-3.11 16.52-5.43 24.84-7.1 7.33-1.47 18.64-3.03 26.91-4.17l2.73-.38c14.38-2 29.67-9.21 29.67-18.62 0-16-20.51-18.39-32.74-18.39-13.87 0-23.64 3.57-27.53 10.05-3.49 6.46-3.73 7.97-4.62 13.6l-.62 4.43h-38.1l.68-5.61c1.35-11.14 3.41-19.03 6.48-24.83 10.54-20.39 31.77-30.75 63.08-30.75 26.11 0 44.63 8.23 53.26 15.94 5.31 4.6 9.1 9.84 11.89 16.46 5.84 12.36 6.32 20.63 6.32 29.4v86.43c0 8.07.78 14.97 2.31 20.5l1.76 6.35h-38.91l-.7-4.19c-.5-2.96-.67-19.75-.88-26.23-8.99 23.61-28.27 33.18-52.21 33.18zm50.53-93.72c-7.97 6.11-20.47 9.6-38.62 13.23-31.27 5.78-36.54 13.06-36.54 27.22 0 12.5 10.63 20.26 27.75 20.26 33.23 0 47.41-15.48 47.41-51.77v-8.94zm124.2-105.51C688.46 64.19 660 35.73 660 .62c0 35.11-28.46 63.57-63.57 63.57h0c35.11 0 63.57 28.46 63.57 63.57h0c0-35.11 28.46-63.57 63.57-63.57z"
fill="#ffffff" style="mix-blend-mode: difference;" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="320" height="320" viewBox="0 0 320 320" version="1.1" id="svg1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs1">
<linearGradient xlink:href="#linearGradient8" id="linearGradient9" x1="105.88936" y1="-7.966506" x2="153.52075" y2="74.533493" gradientUnits="userSpaceOnUse"/>
<linearGradient id="linearGradient8">
<stop style="stop-color:#2de2e6;stop-opacity:1;" offset="0" id="stop8"/>
<stop style="stop-color:#3399ff;stop-opacity:1;" offset="1" id="stop11"/>
</linearGradient>
<linearGradient xlink:href="#linearGradient6" id="linearGradient7" x1="97.229103" y1="82.033493" x2="97.229103" y2="107.03349" gradientUnits="userSpaceOnUse"/>
<linearGradient id="linearGradient6">
<stop style="stop-color:#f623ac;stop-opacity:1;" offset="0" id="stop6"/>
<stop style="stop-color:#f706cf;stop-opacity:1;" offset="1" id="stop7"/>
</linearGradient>
</defs>
<rect style="display:inline;opacity:1;fill:#29144a;fill-opacity:0;stroke:none;stroke-width:4.39823;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" id="rect35" width="323.65826" height="325.41187" x="0" y="-2.7059329"/>
<g id="g1" transform="matrix(0.97092512,0,0,0.95099897,34.423647,35.890832)">
<g id="layer1" style="display:inline;fill:none" transform="matrix(2.12871,0,0,2.0000001,-90.126008,15.218211)">
<path style="display:inline;opacity:1;fill:url(#linearGradient9);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" d="M 92.898977,-25.466506 23.616945,94.53349 a 4.9999999,4.9999999 60 0 0 4.330127,7.5 H 166.51114 a 4.9999996,4.9999996 120 0 0 4.33012,-7.499999 L 101.55923,-25.466506 a 5.0000002,5.0000002 180 0 0 -8.660253,0 z" id="path5" transform="translate(5.8501312,19.942587)"/>
<path id="path1" style="display:inline;opacity:1;fill:#241734;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" d="M 103.61607,27.033498 60.315284,102.0335 H 77.635596 L 90.625831,79.533498 103.61607,102.0335 116.60825,79.533498 129.59849,102.0335 h 17.32031 z m 0,15 10.82666,18.74973 a 7.4997612,7.4997612 90 0 1 0,7.50054 l -10.82666,18.74973 -10.824962,-18.74959 a 7.5008819,7.5008819 90 0 1 0,-7.50082 z" transform="translate(-0.5188027,0.07359983)"/>
<path style="display:inline;opacity:1;fill:url(#linearGradient7);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" d="M 27.947072,102.03349 H 166.51114 a 5.0000006,5.0000006 120 0 0 4.33012,-7.5 l -7.21687,-12.499996 h -40.41452 l -12.99038,-22.5 -12.990386,22.5 -12.990381,-22.5 -12.990381,22.5 H 30.833821 L 23.616945,94.53349 a 5,5 60 0 0 4.330127,7.5 z" id="path6" transform="matrix(1.0011548,0,0,1.0030727,5.7535451,19.687436)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><path id="A" d="M128 117v6a5 5 0 0 0-5 5h-6a5 5 0 1 0-10 0h-6a5 5 0 1 0-10 0h-6a5 5 0 1 0-10 0h-6a5 5 0 1 0-10 0h-6a5 5 0 1 0-10 0h-6a5 5 0 1 0-10 0h-6a5 5 0 1 0-10 0H5a5 5 0 0 0-5-5v-6a5 5 0 1 0 0-10v-6a5 5 0 0 0 0-10v-6a5 5 0 0 0 0-10v-6a5 5 0 0 0 0-10v-6a5 5 0 0 0 0-10v-6a5 5 0 0 0 0-10v-6a5 5 0 0 0 0-10V5a5 5 0 0 0 5-5h6a5 5 0 0 0 10 0h6a5 5 0 0 0 10 0h6a5 5 0 0 0 10 0h6a5 5 0 0 0 10 0h6a5 5 0 0 0 10 0h6a5 5 0 0 0 10 0h6a5 5 0 1 0 10 0h6a5 5 0 0 0 5 5v6a5 5 0 1 0 0 10v6a5 5 0 1 0 0 10v6a5 5 0 1 0 0 10v6a5 5 0 1 0 0 10v6a5 5 0 1 0 0 10v6a5 5 0 1 0 0 10v6a5 5 0 1 0 0 10z"/><rect id="B" width="96" height="96" x="16" y="16" rx="2"/></defs><g transform="matrix(.492308 0 0 .492308 .492308 .492308)" fill="none" fill-rule="evenodd"><use xlink:href="#A" fill="#f0f0f0"/><path d="M128.5 116.5v7h-.5a4.5 4.5 0 0 0-4.5 4.5v.5h-7v-.5a4.5 4.5 0 0 0-9 0v.5h-7v-.5a4.5 4.5 0 0 0-9 0v.5h-7v-.5a4.5 4.5 0 0 0-9 0v.5h-7v-.5a4.5 4.5 0 0 0-9 0v.5h-7v-.5a4.5 4.5 0 0 0-9 0v.5h-7v-.5a4.5 4.5 0 0 0-9 0v.5h-7v-.5a4.5 4.5 0 0 0-9 0v.5h-7v-.5a4.5 4.5 0 0 0-4.5-4.5h-.5v-7H0a4.5 4.5 0 0 0 0-9h-.5v-7H0a4.5 4.5 0 1 0 0-9h-.5v-7H0a4.5 4.5 0 1 0 0-9h-.5v-7H0a4.5 4.5 0 1 0 0-9h-.5v-7H0a4.5 4.5 0 1 0 0-9h-.5v-7H0a4.5 4.5 0 1 0 0-9h-.5v-7H0a4.5 4.5 0 1 0 0-9h-.5v-7H0A4.5 4.5 0 0 0 4.5 0v-.5h7V0a4.5 4.5 0 1 0 9 0v-.5h7V0a4.5 4.5 0 1 0 9 0v-.5h7V0a4.5 4.5 0 1 0 9 0v-.5h7V0a4.5 4.5 0 1 0 9 0v-.5h7V0a4.5 4.5 0 1 0 9 0v-.5h7V0a4.5 4.5 0 1 0 9 0v-.5h7V0a4.5 4.5 0 0 0 9 0v-.5h7V0a4.5 4.5 0 0 0 4.5 4.5h.5v7h-.5a4.5 4.5 0 0 0 0 9h.5v7h-.5a4.5 4.5 0 0 0 0 9h.5v7h-.5a4.5 4.5 0 0 0 0 9h.5v7h-.5a4.5 4.5 0 0 0 0 9h.5v7h-.5a4.5 4.5 0 0 0 0 9h.5v7h-.5a4.5 4.5 0 0 0 0 9h.5v7h-.5a4.5 4.5 0 0 0 0 9z" stroke="#ccc"/><path d="M127.5 117.478a5.5 5.5 0 0 1 0-10.956v-5.044a5.5 5.5 0 0 1 0-10.956v-5.044a5.5 5.5 0 0 1 0-10.956v-5.044a5.5 5.5 0 0 1 0-10.956v-5.044a5.5 5.5 0 0 1 0-10.956v-5.044a5.5 5.5 0 0 1 0-10.956v-5.044a5.5 5.5 0 0 1 0-10.956V5.478A5.502 5.502 0 0 1 122.522.5h-5.044a5.5 5.5 0 0 1-10.956 0h-5.044a5.5 5.5 0 0 1-10.956 0h-5.044a5.5 5.5 0 0 1-10.956 0h-5.044a5.5 5.5 0 0 1-10.956 0h-5.044a5.5 5.5 0 0 1-10.956 0h-5.044a5.5 5.5 0 0 1-10.956 0h-5.044a5.5 5.5 0 0 1-10.956 0H5.478A5.502 5.502 0 0 1 .5 5.478v5.044a5.5 5.5 0 0 1 0 10.956v5.044a5.5 5.5 0 0 1 0 10.956v5.044a5.5 5.5 0 0 1 0 10.956v5.044a5.5 5.5 0 0 1 0 10.956v5.044a5.5 5.5 0 0 1 0 10.956v5.044a5.5 5.5 0 0 1 0 10.956v5.044a5.5 5.5 0 0 1 0 10.956v5.044a5.502 5.502 0 0 1 4.978 4.978h5.044a5.5 5.5 0 0 1 10.956 0h5.044a5.5 5.5 0 0 1 10.956 0h5.044a5.5 5.5 0 0 1 10.956 0h5.044a5.5 5.5 0 0 1 10.956 0h5.044a5.5 5.5 0 0 1 10.956 0h5.044a5.5 5.5 0 0 1 10.956 0h5.044a5.5 5.5 0 0 1 10.956 0h5.044a5.502 5.502 0 0 1 4.978-4.978z" stroke="#fff"/><use xlink:href="#B" fill="#fedd00"/><rect width="95" height="95" x="16.5" y="16.5" rx="2" stroke="#d9b500"/><rect width="97" height="97" x="15.5" y="15.5" rx="2" stroke="#fff"/><path d="M50.3 86.084V42.3H43V35h25.886c16.168 0 19.522 10.664 19.522 18.06 0 5.934-2.408 10.492-4.902 12.986-4.042 4.042-9.546 4.988-17.888 4.988h-6.536v15.05h7.654v7.3H43v-7.3zm8.772-22.102h7.3c10.75 0 13.072-4.988 13.072-11.008 0-6.88-3.87-10.664-10.32-10.664H59.082z" fill="#000"/></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

View File

@@ -90,11 +90,11 @@ PODS:
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- Sentry/HybridSDK (8.36.0)
- sentry_flutter (8.9.0):
- Sentry/HybridSDK (8.46.0)
- sentry_flutter (8.14.2):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.36.0)
- Sentry/HybridSDK (= 8.46.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -232,44 +232,44 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
connectivity_plus: 3f6c9057f4cd64198dc826edfb0542892f825343
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
fk_user_agent: 137145b086229251761678fe034da53753f4ce59
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
flutter_email_sender: 2397f5e84aaacfb61af569637a963e7c687858d8
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_native_splash: 35ddbc7228eafcb3969dcc5f1fbbe27c1145a4f0
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
move_to_background: 155f7bfbd34d43ad847cb630d2d2d87c17199710
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
package_info_plus: 580e9a5f1b6ca5594e7c9ed5f92d1dfb2a66b5e1
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
qr_code_scanner: d77f94ecc9abf96d9b9b8fc04ef13f611e5a147a
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57
sentry_flutter: 0eb93e5279eb41e2392212afe1ccd2fecb4f8cbe
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
share_plus: 011d6fb4f9d2576b83179a3a5c5e323202cdabcf
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sodium_libs: 6c6d0e83f4ee427c6464caa1f1bdc2abf3ca0b7f
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
sqlite3_flutter_libs: 9379996d65aa23dcda7585a5b58766cebe0aa042
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
PODFILE CHECKSUM: 78f002751f1a8f65042b8da97902ba4124271c5a

View File

@@ -66,14 +66,14 @@ class Configuration {
String? _volatilePassword;
final _secureStorageOptionsIOS = const IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
);
Future<void> init() async {
_preferences = await SharedPreferences.getInstance();
sqfliteFfiInit();
_secureStorage = const FlutterSecureStorage();
_secureStorage = const FlutterSecureStorage(
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
_tempDirectory = (await DirectoryUtils.getDirectoryForInit()).path;
final tempDirectory = io.Directory(_tempDirectory);
try {
@@ -98,7 +98,6 @@ class Configuration {
Future<void> _initOfflineAccount() async {
_offlineAuthKey = await _secureStorage.read(
key: offlineAuthSecretKey,
iOptions: _secureStorageOptionsIOS,
);
}
@@ -108,22 +107,18 @@ class Configuration {
unawaited(
_secureStorage.delete(
key: key,
iOptions: _secureStorageOptionsIOS,
),
);
}
} else {
_key = await _secureStorage.read(
key: keyKey,
iOptions: _secureStorageOptionsIOS,
);
_secretKey = await _secureStorage.read(
key: secretKeyKey,
iOptions: _secureStorageOptionsIOS,
);
_authSecretKey = await _secureStorage.read(
key: authSecretKeyKey,
iOptions: _secureStorageOptionsIOS,
);
if (_key == null) {
await logout(autoLogout: true);
@@ -136,7 +131,6 @@ class Configuration {
for (String key in onlineSecureKeys) {
await _secureStorage.delete(
key: key,
iOptions: _secureStorageOptionsIOS,
);
}
await LockScreenSettings.instance.removePinAndPassword();
@@ -396,7 +390,6 @@ class Configuration {
await _secureStorage.write(
key: keyKey,
value: key,
iOptions: _secureStorageOptionsIOS,
);
}
@@ -405,7 +398,6 @@ class Configuration {
await _secureStorage.write(
key: secretKeyKey,
value: secretKey,
iOptions: _secureStorageOptionsIOS,
);
}
@@ -414,7 +406,6 @@ class Configuration {
await _secureStorage.write(
key: authSecretKeyKey,
value: authSecretKey,
iOptions: _secureStorageOptionsIOS,
);
}
@@ -463,18 +454,15 @@ class Configuration {
Future<void> optForOfflineMode() async {
if ((await _secureStorage.containsKey(
key: offlineAuthSecretKey,
iOptions: _secureStorageOptionsIOS,
))) {
_offlineAuthKey = await _secureStorage.read(
key: offlineAuthSecretKey,
iOptions: _secureStorageOptionsIOS,
);
} else {
_offlineAuthKey = CryptoUtil.bin2base64(CryptoUtil.generateKey());
await _secureStorage.write(
key: offlineAuthSecretKey,
value: _offlineAuthKey,
iOptions: _secureStorageOptionsIOS,
);
}
await _preferences.setBool(hasOptedForOfflineModeKey, true);

View File

@@ -1,6 +1,6 @@
{
"account": "الحساب",
"unlock": "فتح القفل",
"unlock": "فتح القُفْل",
"recoveryKey": "مفتاح الاسترداد",
"counterAppBarTitle": "العداد",
"@counterAppBarTitle": {
@@ -16,7 +16,7 @@
"secretCanNotBeEmpty": "لا يمكن أن يكون رمز السر فارغ",
"bothIssuerAndAccountCanNotBeEmpty": "لا يمكن أن يكون المُصدر والحساب فارغًا",
"incorrectDetails": "بيانات غير صحيحة",
"pleaseVerifyDetails": "من فضلك تأكد من بياناتك وحاول مرة أخرى",
"pleaseVerifyDetails": "الرجاء التأكد من البيانات وإعادة المحاولة",
"codeIssuerHint": "المصدِّر",
"codeSecretKeyHint": "الرمز السري",
"secret": "سري",
@@ -36,7 +36,7 @@
"codeAccountHint": "الحساب (you@domain.com)",
"codeTagHint": "وسم",
"accountKeyType": "نوع المفتاح",
"sessionExpired": "انتهت صلاحية الجلسة",
"sessionExpired": "انتهت صَلاحِيَة الجِلسة",
"@sessionExpired": {
"description": "Title of the dialog when the users current session is invalid/expired"
},
@@ -48,6 +48,8 @@
"nextTotpTitle": "التالي",
"deleteCodeTitle": "حذف الرمز؟",
"deleteCodeMessage": "هل أنت متأكد من أنك تريد حذف هذه الشيفرة؟ هذا الإجراء لا رجعة فيه.",
"trashCode": "حذف الكود؟",
"trashCodeMessage": "هل أنت متيقِّن أنك تريد حذف الكود الخاص بـ {account}؟",
"trash": "سلة المهملات",
"viewLogsAction": "عرض السجلات",
"sendLogsDescription": "سوف يُرسل هذا السجلات لنا لمساعدتنا على تصحيح مشكلتك. بينما نتخذ الاحتياطات لضمان عدم تسجيل المعلومات الحساسة، نشجعك على رؤية هذه السجلات قبل مشاركتها.",
@@ -63,7 +65,7 @@
},
"copyEmailAction": "نسخ البريد الإلكتروني",
"exportLogsAction": "تصدير السجلات",
"reportABug": "الابلاغ عن خلل تقني",
"reportABug": "ألإبلاغ عن خلل تقني",
"crashAndErrorReporting": "الإبلاغ عن الأعطال والأخطاء",
"reportBug": "الإبلاغ عن خلل",
"emailUsMessage": "الرجاء مراسلتنا على {email}",
@@ -79,22 +81,24 @@
"blog": "المدونة",
"merchandise": "إدارة المنتجات",
"verifyPassword": "التحقق من كلمة المرور",
"pleaseWait": "الرجاء الإنتظار...",
"pleaseWait": "انتظر قليلاً...",
"generatingEncryptionKeysTitle": "توليد مفاتيح التشفير...",
"recreatePassword": "إعادة كتابة كلمة المرور",
"recreatePasswordMessage": "الجهاز الحالي ليس قويًا بما يكفي للتحقق من كلمة المرور الخاصة بك، لذا نحتاج إلى إعادة إنشائها مرة واحدة بطريقة تعمل مع جميع الأجهزة.\n\nالرجاء تسجيل الدخول باستخدام مفتاح الاسترداد وإعادة إنشاء كلمة المرور الخاصة بك (يمكنك استخدام نفس كلمة المرور مرة أخرى إذا كنت ترغب في ذلك).",
"useRecoveryKey": "استخدم مفتاح الاسترداد",
"incorrectPasswordTitle": "كلمة المرور غير صحيحة",
"welcomeBack": "مرحبًا مجددًا!",
"madeWithLoveAtPrefix": "مصنوعة بـ❤️ في ",
"emailAlreadyRegistered": "البريد الإلكتروني مُسَجَّل من قبل.",
"emailNotRegistered": "البريد الإلكتروني غير مُسَجَّل.",
"madeWithLoveAtPrefix": "مصنوعة بـ❤️ في",
"supportDevs": "اشترك في <bold-green>ente</bold-green> لدعمنا",
"supportDiscount": "استخدم رمز القسيمة \"AUTH\" للحصول على 10% خصم من السنة الأولى",
"supportDiscount": "استخدم رمز القسيمة \"AUTH\" للحصول على 10% خَصْم من السنة الأولى",
"changeEmail": "غير البريد الإلكتروني",
"changePassword": "غير كلمة المرور",
"data": "البيانات",
"importCodes": "استورد شيفرات",
"importTypePlainText": "نص بسيط",
"importTypeEnteEncrypted": "تصدير مشفر ente",
"importTypeEnteEncrypted": "تصدير مشفَّر ente",
"passwordForDecryptingExport": "كلمة المرور لفك تشفير التصدير",
"passwordEmptyError": "لا يمكن أن تكون كلمة المرور فارغة",
"importFromApp": "استورد الشيفرات من {appName}",
@@ -111,10 +115,11 @@
"importLabel": "استيراد",
"importInstruction": "الرجاء تحديد ملف يحتوي على قائمة بالرموز الخاصة بك بالشكل التالي",
"importCodeDelimiterInfo": "يمكن فصل الرموز بفاصلة أو سطر جديد",
"selectFile": "اختيار الملف",
"selectFile": "حدد مِلَفّ",
"emailVerificationToggle": "تأكيد عنوان البريد الإلكتروني",
"emailVerificationEnableWarning": "لتجنب إقفال حسابك، تأكد من تخزين نسخة من بريدك الإلكتروني 2FA خارج Ente Auth قبل تمكين التحقق من البريد الإلكتروني.",
"authToChangeEmailVerificationSetting": "الرجاء المصادقة لتغيير التحقق من البريد الإلكتروني",
"authenticateGeneric": "الرجاء المصادقة",
"authToViewYourRecoveryKey": "الرجاء المصادقة لعرض مفتاح الاسترداد الخاص بك",
"authToChangeYourEmail": "الرجاء المصادقة لتغيير بريدك الإلكتروني",
"authToChangeYourPassword": "الرجاء المصادقة لتغيير كلمة المرور الخاصة بك",
@@ -129,7 +134,7 @@
"general": "العامة",
"settings": "الإعدادات",
"copied": "تم النسخ",
"pleaseTryAgain": "حاول مرة اخرى",
"pleaseTryAgain": "يرجى المحاولة مرة أخرى",
"existingUser": "المستخدم موجود",
"newUser": "جديد في Ente",
"delete": "حذف",
@@ -142,6 +147,8 @@
"leaveFamily": "مغادرة خطة العائلة",
"leaveFamilyMessage": "هل أنت متأكد من الخروج من خطة العائلة؟",
"inFamilyPlanMessage": "أنت مندرج ضمن خطة عائلية!",
"hintForMobile": "اضغط مطولاً على الكود لتعديل أو إزالته.",
"hintForDesktop": "انقر بزر الأيمن على الكود لتعديله.",
"scan": "مسح",
"scanACode": "فحص رمز Qr",
"verify": "التحقق",
@@ -151,6 +158,7 @@
"twoFactorAuthTitle": "المصادقة الثنائية",
"passkeyAuthTitle": "التحقق من مفتاح المرور",
"verifyPasskey": "تحقق من مفتاح المرور",
"loginWithTOTP": "",
"recoverAccount": "إسترجاع الحساب",
"enterRecoveryKeyHint": "أدخل رمز الاسترداد",
"recover": "استرداد",
@@ -172,7 +180,7 @@
"yesSendFeedbackAction": "نعم، ارسل الملاحظات",
"noDeleteAccountAction": "لا، حذف الحساب",
"initiateAccountDeleteTitle": "الرجاء المصادقة لبدء حذف الحساب",
"sendEmail": "ارسل بريد الكتروني",
"sendEmail": "إرسال بريد إلكتروني",
"createNewAccount": "إنشاء حساب جديد",
"weakStrength": "ضعيف",
"strongStrength": "قوي",
@@ -184,9 +192,9 @@
"language": "اللغة",
"social": "وسائل التواصل",
"security": "الأمان",
"lockscreen": "شاشة القفل",
"authToChangeLockscreenSetting": "الرجاء المصادقة لتغيير إعدادات شاشة القفل",
"deviceLockEnablePreSteps": "لتمكين قفل التطبيق، فضلا أعد شيفرة مرور للجهاز أو قفل الشاشة في إعدادات نظامك.",
"lockscreen": "شاشة القُفْل",
"authToChangeLockscreenSetting": "الرجاء المصادقة لتغيير إعدادات شاشة القُفْل",
"deviceLockEnablePreSteps": "لتفعيل قُفْل الجهاز، اضبط رمز مرور أو قُفْل الشاشة من الإعدادات",
"viewActiveSessions": "عرض الجلسات النشطة",
"authToViewYourActiveSessions": "الرجاء المصادقة لعرض جلساتك النشطة",
"searchHint": "بحث...",
@@ -199,6 +207,7 @@
"edit": "تعديل",
"share": "مشاركة",
"shareCodes": "شارك الرموز",
"shareCodesDuration": "حدد المدة التي تريد أن تشارك فيها ألاكوا د.",
"restore": "استعادة",
"copiedToClipboard": "تم النسخ إلى الحافظة",
"copiedNextToClipboard": "تم نسخ الرموز التالية إلى الحافظة",
@@ -326,6 +335,7 @@
}
},
"manualSort": "مخصّص",
"editOrder": "تعديل الطلب",
"mostFrequentlyUsed": "مستخدم بكثرة",
"mostRecentlyUsed": "مستخدمة مؤخراً",
"activeSessions": "الجلسات النشطة",
@@ -447,6 +457,9 @@
"customEndpoint": "متصل بـ{endpoint}",
"pinText": "ثبت",
"unpinText": "ألغِ التثبيت",
"pinnedCodeMessage": "ثُبِّت {code}",
"unpinnedCodeMessage": "أُلغِي تثبيت {code}",
"pinned": "ثُبِّت",
"tags": "الأوسمة",
"createNewTag": "أنشيء وسم جديد",
"tag": "وسم",
@@ -459,9 +472,9 @@
"viewRawCodes": "عرض الشيفرات الأصلية",
"rawCodes": "الشيفرات الأصلية",
"rawCodeData": "بيانات الشيفرات الأصلية",
"appLock": "قفل التطبيق",
"appLock": "قُفْل التطبيق",
"noSystemLockFound": "لا يوجد قفل نظام",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "لتمكين قفل التطبيق، فضلا أعد شيفرة مرور الجهاز أو قفل الشاشة في إعدادات نظامك.",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "لتفعيل قُفْل التطبيق، اضبط رمز مرور الجهاز أو قُفْل الشاشة من الإعدادات.",
"autoLock": "قفل تلقائي",
"immediately": "فورًا",
"reEnterPassword": "أعد إدخال كلمة المرور",
@@ -475,14 +488,30 @@
"hideContentDescriptionAndroid": "يخفي محتوى التطبيق في مبدل التطبيقات ويمنع لقطات الشاشة",
"hideContentDescriptioniOS": "يخفي محتوى التطبيق في مبدل التطبيقات",
"autoLockFeatureDescription": "الوقت الذي بعده ينقفل التطبيق بعدما يوضع في الخلفية",
"appLockDescription": "اختر بين شاشة القفل الافتراضية الخاصة بجهازك وشاشة قفل مخصصة برقم تعريف شخصي أو كلمة مرور.",
"appLockDescription": "اختر نوع قُفْل الشاشة: افتراضي أو مخصص.",
"pinLock": "قفل رقم التعريف الشخصي",
"enterPin": "أدخل رقم التعريف الشخصي",
"setNewPin": "عين رقم تعريف شخصي جديد",
"importFailureDescNew": "تعذر إعراب الملف المنتقى.",
"appLockNotEnabled": "قُفْل التطبيق غير مفعل ",
"appLockNotEnabledDescription": "لحماية بياناتك، فعِّل قُفْل التطبيق من إعدادات الأمان ← قُفْل التطبيق",
"authToViewPasskey": "الرجاء المصادقة لعرض مفتاح الاسترداد الخاص بك",
"appLockOfflineModeWarning": "لقد اخترتَ المُتابعة دون نُسخ احتياطية. إذا نَسيتَ قُفْل التطبيق، فلن تتمكن من الوصول إلى بياناتك.",
"duplicateCodes": "رموز مكررة",
"noDuplicates": "✨ لا تكرارات",
"youveNoDuplicateCodesThatCanBeCleared": "لا توجد لديك أي أكواد مكررة يمكن حذفها",
"deduplicateCodes": "أكواد مكررة",
"deselectAll": "ألغِ تحديد الكل",
"selectAll": "حدد الكل",
"deleteDuplicates": "احذف التكرار"
"deleteDuplicates": "احذف التكرار",
"plainHTML": "HTML عَادِي ",
"tellUsWhatYouThink": "شاركنا برأيك",
"dropReviewiOS": "اترك مراجعة على أبل ستور",
"dropReviewAndroid": "اترك مراجعة بلاي ستور",
"supportEnte": "دعم <bold-green>ente</bold-green></bold-green>",
"giveUsAStarOnGithub": "من فضلك أعطِنا نجمة على جيت هاب",
"free5GB": "5GB مجانًا على <bold-green>ente</bold-green> صور",
"loginWithAuthAccount": "سجّل الدخول باستخدام حساب المُصادقة",
"freeStorageOffer": "خَصْم 10٪ على صور <bold-green>ente</bold-green>",
"freeStorageOfferDescription": "استخدم الكود \"AUTH\" وأحصل على 10٪ خَصْم في السنة الأولى"
}

View File

@@ -499,11 +499,13 @@
"duplicateCodes": "Doppelte Codes",
"noDuplicates": "✨ Keine Duplikate",
"youveNoDuplicateCodesThatCanBeCleared": "Du hast keine doppelten Codes, die bereinigt werden können",
"deduplicateCodes": "Codes deduplizieren",
"deselectAll": "Alle abwählen",
"selectAll": "Alles auswählen",
"deleteDuplicates": "Duplikate löschen",
"plainHTML": "Reines HTML",
"tellUsWhatYouThink": "Sagen Sie uns, was Sie denken",
"dropReviewiOS": "Hinterlasse eine Rezension im App Store",
"dropReviewAndroid": "Hinterlasse eine Rezension im Google Play Store",
"supportEnte": "Support <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Gib uns einen Stern auf Github",

View File

@@ -506,6 +506,8 @@
"deleteDuplicates": "Eliminar duplicados",
"plainHTML": "HTML plano",
"tellUsWhatYouThink": "Cuéntanos cuál es su opinión",
"dropReviewiOS": "Deja una reseña en la App Store",
"dropReviewAndroid": "Deja una reseña en la 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",

View File

@@ -499,12 +499,15 @@
"appLockOfflineModeWarning": "Vous avez choisi de fonctionner sans sauvegardes. Si vous oubliez votre outil Applock, vous serez bloqué dans l'accès à vos données.",
"duplicateCodes": "Codes dupliqués",
"noDuplicates": "✨ Pas de doublons",
"youveNoDuplicateCodesThatCanBeCleared": "Vous n'avez aucun code en doublon pouvant être supprimé",
"deduplicateCodes": "Codes dédupliqués",
"deselectAll": "Tout désélectionner",
"selectAll": "Tout sélectionner",
"deleteDuplicates": "Supprimer les doublons",
"plainHTML": "HTML Pur",
"tellUsWhatYouThink": "Dites-nous ce que vous pensez",
"dropReviewiOS": "Laisser un avis sur l'App Store",
"dropReviewAndroid": "Laisser un avis sur le Play Store",
"supportEnte": "Soutenir <bold-green>Ente</bold-green>",
"giveUsAStarOnGithub": "Donnez-nous une étoile sur Github",
"free5GB": "5 Go gratuits sur <bold-green>Ente</bold-green> Photos",

View File

@@ -79,7 +79,7 @@
"contactSupport": "Lépj kapcsolatba az Ügyfélszolgálattal",
"rateUsOnStore": "Értékelj minket a következőn: {storeName}",
"blog": "Blog",
"merchandise": "Áru",
"merchandise": "Ajándéktárgyak",
"verifyPassword": "Jelszó megerősítése",
"pleaseWait": "Kérem várjon...",
"generatingEncryptionKeysTitle": "Titkosítási kulcs generálása...",
@@ -499,12 +499,15 @@
"appLockOfflineModeWarning": "Úgy döntött, hogy biztonsági mentés nélkül folytatja. Ha elfelejti az alkalmazászárat, akkor nem férhet hozzá adataihoz.",
"duplicateCodes": "Ismétlődő kódok",
"noDuplicates": "✨Nincs ismétlődés",
"youveNoDuplicateCodesThatCanBeCleared": "Nincsenek törölhető ismétlődő kódok",
"deduplicateCodes": "Ismétlődő kódok",
"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",
"tellUsWhatYouThink": "Mondja el mit gondol",
"dropReviewiOS": "Írj véleményt az App Store-ban",
"dropReviewAndroid": "Írj véleményt a 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",

View File

@@ -506,6 +506,8 @@
"deleteDuplicates": "Ištrinti dublikatus",
"plainHTML": "Grynasis HTML",
"tellUsWhatYouThink": "Pasakykite mums, ką manote",
"dropReviewiOS": "Rašyti apžvalgą parduotuvėje „App Store“",
"dropReviewAndroid": "Rašyti apžvalgą parduotuvėje „Play“ parduotuvė“",
"giveUsAStarOnGithub": "Suteikite mums žvaigždutę platformoje „Github“",
"free5GB": "5 GB nemokami programai „<bold-green>ente</bold-green>“ nuotraukos",
"loginWithAuthAccount": "Prisijungti su jūsų „Auth“ paskyra",

View File

@@ -506,6 +506,8 @@
"deleteDuplicates": "Dubbelen verwijderen",
"plainHTML": "Alleen HTML",
"tellUsWhatYouThink": "Vertel ons wat je vindt",
"dropReviewiOS": "Laat een beoordeling achter in de App Store",
"dropReviewAndroid": "Laat een beoordeling achter in de Play Store",
"supportEnte": "Steun <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Geef ons een ster op Github",
"free5GB": "5GB gratis op <bold-green>ente</bold-green> Photos",

View File

@@ -0,0 +1 @@
{}

View File

@@ -83,22 +83,22 @@
"verifyPassword": "Подтверждение пароля",
"pleaseWait": "Пожалуйста, подождите...",
"generatingEncryptionKeysTitle": "Генерируем ключи шифрования...",
"recreatePassword": "Воссоздать пароль заново",
"recreatePassword": "Повторное создание пароля",
"recreatePasswordMessage": "Текущее устройство недостаточно мощное для проверки пароля, поэтому нам нужно регенерировать его один раз таким образом, чтобы работать со всеми устройствами. \n\nПожалуйста, войдите, используя ваш ключ восстановления и сгенерируйте ваш пароль (вы можете использовать тот же самый, если пожелаете).",
"useRecoveryKey": "Использовать ключ восстановления",
"incorrectPasswordTitle": "Неправильный пароль",
"welcomeBack": "С возвращением!",
"emailAlreadyRegistered": "Адрес электронной почты уже зарегистрирован.",
"emailNotRegistered": "Адрес электронной почты не зарегистрирован.",
"madeWithLoveAtPrefix": "сделана с ❤️ в ",
"madeWithLoveAtPrefix": "сделано с ❤️ в ",
"supportDevs": "Подпишитесь на <bold-green>ente</bold-green> для поддержки нашего проекта",
"supportDiscount": "Используйте код скидки \"AUTH\", чтобы получить скидку 10% на первый год",
"changeEmail": "Изменить почту",
"supportDiscount": "Используйте кодовое слово \"AUTH\", чтобы получить скидку 10% на первый год",
"changeEmail": "Изменить адрес электронной почты",
"changePassword": "Изменить пароль",
"data": "Данные",
"importCodes": "Импортировать коды",
"importTypePlainText": "Обычный текст",
"importTypeEnteEncrypted": "Ente Зашифрованный экспорт",
"importTypeEnteEncrypted": "Зашифрованный экспорт из Ente",
"passwordForDecryptingExport": "Пароль для расшифровки экспорта",
"passwordEmptyError": "Пароль не может быть пустым",
"importFromApp": "Импорт кодов из {appName}",
@@ -106,27 +106,27 @@
"importSelectJsonFile": "Выбрать JSON-файл",
"importSelectAppExport": "Выбрать файл экспорта {appName}",
"importEnteEncGuide": "Выберите зашифрованный JSON файл, экспортированный из Ente",
"importRaivoGuide": "Используйте опцию «Export OTPs to Zip archive» в настройках Raivo.\n\nРаспакуйте zip-архив и импортируйте JSON-файл.",
"importBitwardenGuide": "Используйте опцию \"Экспортировать хранилище\" в Bitwarden Tools и импортируйте незашифрованный JSON файл.",
"importRaivoGuide": "Используйте опцию «Экспорт OTP-кодов в Zip-архив» в настройках Raivo.\n\nРаспакуйте zip-архив и импортируйте JSON-файл.",
"importBitwardenGuide": "Используйте опцию \"Экспортировать хранилище\" в Bitwarden и импортируйте незашифрованный JSON-файл.",
"importAegisGuide": "Используйте опцию «Экспортировать хранилище» в настройках Aegis.\n\nЕсли ваше хранилище зашифровано, то для его расшифровки потребуется ввести пароль хранилища.",
"import2FasGuide": "Используйте опцию \"Settings->Backup -Export\" в 2FAS.\n\nЕсли ваша резервная копия зашифрована, то для расшифровки резервной копии необходимо ввести пароль",
"importLastpassGuide": "Используйте опцию \"Перенести аккаунты\" в настройках Lastpass Authenticator и нажмите на \"Экспортировать учетные записи в файл\". Импортируйте загружённый JSON файл.",
"import2FasGuide": "Используйте опцию \"Настройки->Резервное копирование -Экспорт\" в 2FAS.\n\nЕсли ваша резервная копия зашифрована, то для расшифровки резервной копии необходимо ввести пароль",
"importLastpassGuide": "Используйте опцию \"Перенести аккаунты\" в настройках Lastpass Authenticator и нажмите на \"Экспортировать учетные записи в файл\". Импортируйте загружённый JSON-файл.",
"exportCodes": "Экспортировать коды",
"importLabel": "Импорт",
"importInstruction": "Пожалуйста, выберите файл, содержащий список ваших кодов в следующем формате",
"importCodeDelimiterInfo": "Коды могут быть разделены запятой или новой строкой",
"selectFile": "Выбрать файл",
"emailVerificationToggle": "Подтверждение электронной почты",
"emailVerificationEnableWarning": "Если вы храните у нас двухфакторную аутентификацию в своей электронной почте, включение проверки электронной почты может привести к тупиковой ситуации. Если у вас заблокирован доступ к одной службе, возможно, вы не сможете войти в другую.",
"emailVerificationToggle": "Подтверждение адреса электронной почты",
"emailVerificationEnableWarning": "Чтобы избежать блокировки вашей учетной записи, обязательно сохраните копию вашего электронного письма 2FA за пределами Ente Auth, прежде чем активировать проверку электронной почты.",
"authToChangeEmailVerificationSetting": "Авторизуйтесь, чтобы изменить подтверждение электронной почты",
"authenticateGeneric": "Пожалуйста, авторизуйтесь",
"authToViewYourRecoveryKey": "Пожалуйста, авторизуйтесь для просмотра вашего ключа восстановления",
"authToChangeYourEmail": "Пожалуйста, авторизуйтесь, чтобы изменить адрес электронной почты",
"authToChangeYourPassword": "Пожалуйста, авторизуйтесь, чтобы изменить пароль",
"authToViewSecrets": "Пожалуйста, авторизуйтесь для просмотра ваших секретов",
"authToInitiateSignIn": "Пожалуйста, авторизуйтесь, чтобы начать вход для резервного копирования.",
"authToInitiateSignIn": "Пожалуйста, авторизуйтесь, чтобы получить доступ к резервному копированию.",
"ok": "Ок",
"cancel": "Отменить",
"cancel": "Отмена",
"yes": "Да",
"no": "Нет",
"email": "Электронная почта",
@@ -136,30 +136,31 @@
"copied": "Скопировано",
"pleaseTryAgain": "Пожалуйста, попробуйте ещё раз",
"existingUser": "Существующий пользователь",
"newUser": "Впервые здесь, в Ente",
"newUser": "Впервые в Ente",
"delete": "Удалить",
"enterYourPasswordHint": "Введите пароль",
"forgotPassword": "Забыл пароль",
"oops": "Ой",
"suggestFeatures": "Предложить идеи",
"faq": "FAQ",
"somethingWentWrongMessage": "Что-то пошло не так. Попробуйте еще раз",
"faq": "ЧаВо",
"somethingWentWrongMessage": "Что-то пошло не так, пожалуйста, попробуйте еще раз",
"leaveFamily": "Покинуть семью",
"leaveFamilyMessage": "Вы уверены, что хотите отказаться от семейного плана?",
"inFamilyPlanMessage": "Вы на семейном плане!",
"hintForMobile": "Длительное нажмите на код для редактирования или удаления.",
"hintForDesktop": "Щелкните правой кнопкой мыши по коду, чтобы изменить или удалить.",
"hintForMobile": "Нажмите на код и удерживайте, чтобы редактировать его или удалить.",
"hintForDesktop": "Щелкните правой кнопкой мыши по коду, чтобы редактировать его или удалить.",
"scan": "Сканировать",
"scanACode": "Сканировать QR-код",
"verify": "Подтвердить",
"verifyEmail": "Подтвердить электронную почту",
"verifyEmail": "Подтвердить адрес электронной почты",
"enterCodeHint": "Введите 6-значный код из\nвашего приложения-аутентификатора",
"lostDeviceTitle": "Потеряно устройство?",
"twoFactorAuthTitle": "Двухфакторная аутентификация",
"passkeyAuthTitle": "Проверка с помощью пароля",
"passkeyAuthTitle": "Проверка с помощью ключа доступа",
"verifyPasskey": "Подтвердить пароль",
"loginWithTOTP": "Войти с помощью TOTP",
"recoverAccount": "Восстановить аккаунт",
"enterRecoveryKeyHint": "Введите свой ключ восстановления",
"enterRecoveryKeyHint": "Введите ключ восстановления",
"recover": "Восстановить",
"contactSupportViaEmailMessage": "Пожалуйста, отправьте электронное письмо на адрес {email} с вашего зарегистрированного адреса электронной почты",
"@contactSupportViaEmailMessage": {
@@ -171,7 +172,7 @@
},
"invalidQRCode": "Неверный QR-код",
"noRecoveryKeyTitle": "Нет ключа восстановления?",
"enterEmailHint": "Введите свою почту",
"enterEmailHint": "Введите адрес электронной почты",
"invalidEmailTitle": "Неверный адрес электронной почты",
"invalidEmailMessage": "Пожалуйста, введите действительный адрес электронной почты.",
"deleteAccount": "Удалить аккаунт",
@@ -182,7 +183,7 @@
"sendEmail": "Отправить электронное письмо",
"createNewAccount": "Создать новый аккаунт",
"weakStrength": "Слабый",
"strongStrength": "Крепкий",
"strongStrength": "Сильный",
"moderateStrength": "Средний",
"confirmPassword": "Подтвердить пароль",
"close": "Закрыть",
@@ -333,6 +334,9 @@
}
}
},
"manualSort": "Ручная",
"editOrder": "Изменить порядок",
"mostFrequentlyUsed": "Частота использования",
"mostRecentlyUsed": "Недавно использованные",
"activeSessions": "Активные сеансы",
"somethingWentWrongPleaseTryAgain": "Что-то пошло не так. Попробуйте еще раз",
@@ -376,7 +380,7 @@
"deleteCodeAuthMessage": "Аутентификация для удаления кода",
"showQRAuthMessage": "Аутентификация для отображения QR-кода",
"confirmAccountDeleteTitle": "Подтвердить удаление аккаунта",
"confirmAccountDeleteMessage": "Эта учетная запись связана с другими приложениями Ente, если вы ими пользуетесь.\n\nЗагруженные вами данные во всех приложениях ente будут запланированы к удалению, а ваша учетная запись будет удалена без возможности восстановления.",
"confirmAccountDeleteMessage": "Эта учетная запись связана с другими приложениями Ente, если вы ими пользуетесь.\n\nЗагруженные вами данные во всех приложениях Ente будут запланированы к удалению, а ваша учетная запись будет удалена без возможности восстановления.",
"androidBiometricHint": "Подтвердите личность",
"@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
@@ -436,7 +440,7 @@
"signOutOtherDevices": "Выйти из других устройств",
"doNotSignOut": "Не выходить",
"hearUsWhereTitle": "Как вы узнали о Ente? (необязательно)",
"hearUsExplanation": "Будет полезно, если вы укажете, где нашли нас, так как мы не отслеживаем установки приложения",
"hearUsExplanation": "Мы не отслеживаем установки приложений. Было бы полезно, если бы вы сказали, где нас нашли!",
"recoveryKeySaved": "Ключ восстановления сохранён в папке Загрузки!",
"waitingForBrowserRequest": "Ожидание запроса браузера...",
"waitingForVerification": "Ожидание подтверждения...",
@@ -453,6 +457,9 @@
"customEndpoint": "Подключено к {endpoint}",
"pinText": "Прикрепить",
"unpinText": "Открепить",
"pinnedCodeMessage": "{code} был закреплен",
"unpinnedCodeMessage": "{code} был откреплен",
"pinned": "Закреплено",
"tags": "Метки",
"createNewTag": "Создать новую метку",
"tag": "Метка",
@@ -462,9 +469,9 @@
"deleteTagMessage": "Вы уверены, что хотите удалить эту метку? Это действие необратимо.",
"somethingWentWrongParsingCode": "Мы не смогли разобрать коды {x}.",
"updateNotAvailable": "Обновление недоступно",
"viewRawCodes": "Просмотр сырых кодов",
"rawCodes": "Сырые коды",
"rawCodeData": "Сырая информация кодов",
"viewRawCodes": "Просмотр необработанных кодов",
"rawCodes": "Необработанные коды",
"rawCodeData": "Необработанные кодовые данные",
"appLock": "Блокировка приложения",
"noSystemLockFound": "Системная блокировка не найдена",
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Чтобы включить блокировку, настройте пароль устройства или блокировку экрана в настройках системы.",
@@ -482,20 +489,28 @@
"hideContentDescriptioniOS": "Скрывает содержимое приложения в переключателе приложений",
"autoLockFeatureDescription": "Время в фоне, после которого приложение блокируется",
"appLockDescription": "Выберите между экраном блокировки вашего устройства и пользовательским экраном блокировки с PIN-кодом или паролем.",
"pinLock": "Pin Замок",
"pinLock": "Pin блокировка",
"enterPin": "Введите PIN",
"setNewPin": "Установите новый PIN",
"importFailureDescNew": "Не удалось обработать выбранный файл.",
"appLockNotEnabled": "Блокировка приложения отключена",
"appLockNotEnabledDescription": "Пожалуйста, включите блокировку приложения в безопасности > Блокировка приложений",
"appLockNotEnabledDescription": "Пожалуйста, включите блокировку приложения в разделе Безопасность > Блокировка приложений",
"authToViewPasskey": "Пожалуйста, авторизуйтесь, чтобы просмотреть пароль",
"appLockOfflineModeWarning": "Вы решили продолжить работу без резервного копирования. Если вы забудете свой пароль, доступ к вашим данным будет заблокирован.",
"duplicateCodes": "Дублировать коды",
"noDuplicates": "✨ Дубликатов нет",
"youveNoDuplicateCodesThatCanBeCleared": "У вас нет повторяющихся кодов, которые можно было бы удалить",
"deduplicateCodes": "Дедупликационные коды",
"deselectAll": "Снять выделение",
"selectAll": "Выбрать все",
"deleteDuplicates": "Удалить повторяющиеся",
"plainHTML": "Обычный HTML",
"tellUsWhatYouThink": "Расскажите нам, что вы думаете",
"free5GB": "5GB бесплатно на <bold-green>ente</bold-green> фото",
"dropReviewiOS": "Оставьте отзыв в App Store",
"dropReviewAndroid": "Оставьте отзыв в Play Store",
"supportEnte": "Поддержка <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Поставьте нам звезду на Github",
"free5GB": "5Гб бесплатного пространства на <bold-green>ente</bold-green> Фото",
"loginWithAuthAccount": "Войти с помощью учетной записи Auth",
"freeStorageOffer": "Скидка 10% на <bold-green>ente</bold-green> фото",
"freeStorageOfferDescription": "Используйте код \"AUTH\", чтобы получить скидку 10% в первый год"

View File

@@ -1 +1,3 @@
{}
{
"importScanQrCode": ""
}

View File

@@ -51,7 +51,7 @@
"trashCode": "Xóa mã?",
"trashCodeMessage": "Bạn có chắc chắn muốn xóa mã cho {account} không?",
"trash": "Xóa",
"viewLogsAction": "Xem các bản ghi",
"viewLogsAction": "Xem nhật ký",
"sendLogsDescription": "Thao tác này sẽ gửi nhật ký để giúp chúng tôi gỡ lỗi sự cố của bạn. Mặc dù chúng tôi thực hiện các biện pháp phòng ngừa để đảm bảo rằng thông tin nhạy cảm không được ghi lại, nhưng chúng tôi khuyến khích bạn xem các nhật ký này trước khi chia sẻ chúng.",
"preparingLogsTitle": "Đang chuẩn bị nhật ký...",
"emailLogsTitle": "Nhật ký email",
@@ -506,6 +506,8 @@
"deleteDuplicates": "Xóa trùng lặp",
"plainHTML": "HTML thuần",
"tellUsWhatYouThink": "Hãy cho chúng tôi biết bạn nghĩ gì",
"dropReviewiOS": "Đánh giá ngay trên App Store",
"dropReviewAndroid": "Đánh giá ngay trên Play Store",
"supportEnte": "Hỗ trợ <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Cho chúng tôi ngôi sao trên Github",
"free5GB": "Miễn phí 5GB cho <bold-green>ente</bold-green> Hình ảnh",

View File

@@ -504,10 +504,10 @@
"deselectAll": "取消全选",
"selectAll": "全选",
"deleteDuplicates": "删除重复项",
"plainHTML": "Plain HTML",
"plainHTML": " HTML",
"tellUsWhatYouThink": "告诉我们您的想法",
"dropReviewiOS": "在 App Store 上发表评",
"dropReviewAndroid": "在 Play 商店上发表评",
"dropReviewiOS": "在 App Store 上发表评",
"dropReviewAndroid": "在 Play 商店上发表评",
"supportEnte": "支持 <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "在 Github 上给我们一个星标",
"free5GB": "<bold-green>ente</bold-green> Photos 上 5GB 可用空间",

View File

@@ -504,8 +504,10 @@
"deselectAll": "取消全選",
"selectAll": "全選",
"deleteDuplicates": "刪除重複項",
"plainHTML": "Plain HTML",
"plainHTML": "HTML",
"tellUsWhatYouThink": "告訴我們您的想法",
"dropReviewiOS": "在 App Store 上發表意見",
"dropReviewAndroid": "在 Play 商店上發表評測",
"supportEnte": "支援 <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "在 Github 上給我們一個星標",
"free5GB": "<bold-green>ente</bold-green> Photos 上 5GB 可用空間",

View File

@@ -114,18 +114,19 @@ class Code {
CodeDisplay? display,
int digits, {
Algorithm algorithm = Algorithm.sha1,
int period = defaultPeriod,
}) {
final String encodedIssuer = Uri.encodeQueryComponent(issuer);
return Code(
account,
issuer,
digits,
defaultPeriod,
period,
secret,
algorithm,
type,
0,
"otpauth://${type.name}/$issuer:$account?algorithm=${algorithm.name.toUpperCase()}&digits=$digits&issuer=$encodedIssuer&period=30&secret=$secret",
"otpauth://${type.name}/$issuer:$account?algorithm=${algorithm.name.toUpperCase()}&digits=$digits&issuer=$encodedIssuer&period=$period&secret=$secret",
display: display ?? CodeDisplay(),
);
}

View File

@@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
class FieldLabel extends StatelessWidget {
final String label;
final double width;
const FieldLabel(
this.label, {
super.key,
this.width = 80,
});
@override
@@ -14,7 +16,7 @@ class FieldLabel extends StatelessWidget {
return Padding(
padding: const EdgeInsets.only(right: 12.0),
child: SizedBox(
width: 80,
width: width,
child: Text(
label,
style: getEnteTextTheme(context).miniBoldMuted,

View File

@@ -18,6 +18,7 @@ import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/custom_icon_widget.dart';
import 'package:ente_auth/ui/components/models/button_result.dart';
import 'package:ente_auth/ui/custom_icon_page.dart';
import 'package:ente_auth/ui/topt_selector_widget.dart';
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
@@ -40,11 +41,13 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
final int _notesLimit = 500;
final int _otherTextLimit = 200;
final int defaultDigits = 6;
final int defaultPeriodInSeconds = 30;
late TextEditingController _issuerController;
late TextEditingController _accountController;
late TextEditingController _secretController;
late TextEditingController _notesController;
late TextEditingController _digitsController;
late TextEditingController _periodController;
late bool _secretKeyObscured;
late List<String> selectedTags = [...?widget.code?.display.tags];
List<String> allTags = [];
@@ -53,6 +56,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
String _customIconID = "";
late IconType _iconSrc;
late Algorithm _algorithm;
late Type _type;
final ValueNotifier<bool> showAdvancedOptions = ValueNotifier<bool>(false);
@override
void initState() {
@@ -74,6 +79,11 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
? widget.code!.digits.toString()
: defaultDigits.toString(),
);
_periodController = TextEditingController(
text: widget.code != null
? widget.code!.period.toString()
: defaultPeriodInSeconds.toString(),
);
_secretKeyObscured = widget.code != null;
_loadTags();
@@ -112,6 +122,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
: IconType.customIcon;
_algorithm = widget.code == null ? Algorithm.sha1 : widget.code!.algorithm;
_type = widget.code == null ? Type.totp : widget.code!.type;
super.initState();
}
@@ -134,6 +145,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
_accountController.dispose();
_notesController.dispose();
_digitsController.dispose();
_periodController.dispose();
super.dispose();
}
@@ -282,76 +294,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
),
const SizedBox(height: 12),
widget.code == null
? Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
),
child: ExpansionTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
collapsedBackgroundColor: Colors.transparent,
tilePadding: EdgeInsets.zero,
title: Text(
"Advanced",
style: getEnteTextTheme(context).small,
),
children: <Widget>[
Row(
children: [
const FieldLabel("Digits"),
Expanded(
child: TextFormField(
keyboardType: TextInputType.number,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter a number";
}
final intValue = int.tryParse(value);
if (intValue == null) {
return "Only integers are allowed";
}
if (intValue < 1 || intValue > 10) {
return "OTP digits must be between 1 and 10";
}
return null;
},
maxLines: 1,
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(
vertical: 12.0,
),
),
style: getEnteTextTheme(context).small,
controller: _digitsController,
),
),
],
),
const SizedBox(height: 22),
Row(
children: [
const FieldLabel("Algorithm"),
AlgorithmSelectorWidget(
currentAlgorithm: _algorithm,
onSelected: (newAlgorithm) async {
setState(() {
_algorithm = newAlgorithm;
});
},
),
],
),
const SizedBox(height: 12),
],
),
)
? advanceOptionWidget()
: const SizedBox.shrink(),
const SizedBox(height: 12),
Wrap(
@@ -419,11 +362,25 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
return;
}
final period =
int.tryParse(_periodController.text.trim());
if (period != null && (period < 10 || period > 60)) {
String message =
"Period must be between 10 and 60 seconds";
_showIncorrectDetailsDialog(
context,
message: message,
);
return;
}
if ((_accountController.text.trim().isEmpty &&
_issuerController.text.trim().isEmpty) ||
_secretController.text.trim().isEmpty ||
_digitsController.text.trim().isEmpty ||
digits == null) {
digits == null ||
_periodController.text.trim().isEmpty ||
period == null) {
String message;
if (_secretController.text.trim().isEmpty) {
message = context.l10n.secretCanNotBeEmpty;
@@ -431,6 +388,10 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
message = "Digits cannot be empty";
} else if (digits == null) {
message = "Digits is not a integer";
} else if (_periodController.text.isEmpty) {
message = "Period cannot be empty";
} else if (period == null) {
message = "Period is not a integer";
} else {
message =
context.l10n.bothIssuerAndAccountCanNotBeEmpty;
@@ -462,6 +423,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
final secret = _secretController.text.trim().replaceAll(' ', '');
final notes = _notesController.text.trim();
final digits = int.tryParse(_digitsController.text.trim());
final period = int.tryParse(_periodController.text.trim());
final isStreamCode = issuer.toLowerCase() == "steam" ||
issuer.toLowerCase().contains('steampowered.com');
@@ -498,13 +460,14 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
final Code newCode = widget.code == null
? Code.fromAccountAndSecret(
isStreamCode ? Type.steam : Type.totp,
isStreamCode ? Type.steam : _type,
account,
issuer,
secret,
display,
isStreamCode ? Code.steamDigits : digits!,
algorithm: _algorithm,
period: period!,
)
: widget.code!.copyWith(
account: account,
@@ -513,6 +476,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
display: display,
algorithm: _algorithm,
digits: digits!,
type: _type,
period: period,
);
// Verify the validity of the code
@@ -558,4 +523,159 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
_iconSrc = newCustomIcon.type;
});
}
Widget advanceOptionWidget() {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
showAdvancedOptions.value = !showAdvancedOptions.value;
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Advanced',
),
ValueListenableBuilder<bool>(
valueListenable: showAdvancedOptions,
builder: (context, isExpanded, child) {
return Icon(
isExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 24,
);
},
),
],
),
),
ValueListenableBuilder<bool>(
valueListenable: showAdvancedOptions,
builder: (context, isExpanded, child) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return SizeTransition(
sizeFactor: animation,
child: child,
);
},
child: isExpanded
? SizedBox(
width: 400,
child: Padding(
padding: const EdgeInsets.only(
top: 16,
),
child: GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
childAspectRatio: 2.5,
crossAxisSpacing: 16,
mainAxisSpacing: 14,
physics: const NeverScrollableScrollPhysics(),
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const FieldLabel("Algorithm", width: 60),
AlgorithmSelectorWidget(
currentAlgorithm: _algorithm,
onSelected: (newAlgorithm) async {
setState(() {
_algorithm = newAlgorithm;
});
},
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const FieldLabel("Type", width: 60),
ToptSelectorWidget(
currentTopt: _type,
onSelected: (newTopt) async {
setState(() {
_type = newTopt;
});
},
),
],
),
Row(
children: [
const FieldLabel("Period", width: 60),
Expanded(
child: TextFormField(
keyboardType: TextInputType.number,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter a number";
}
final intValue = int.tryParse(value);
if (intValue == null) {
return "Only integers are allowed";
}
if (intValue < 1 || intValue > 60) {
return "Period must be between 1 and 60";
}
return null;
},
maxLines: 1,
style: getEnteTextTheme(
context,
).small,
controller: _periodController,
),
),
],
),
Row(
children: [
const FieldLabel("Digits", width: 60),
Expanded(
child: TextFormField(
keyboardType: TextInputType.number,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter a number";
}
final intValue = int.tryParse(value);
if (intValue == null) {
return "Only integers are allowed";
}
if (intValue < 1 || intValue > 10) {
return "OTP digits must be between 1 and 10";
}
return null;
},
maxLines: 1,
style: getEnteTextTheme(
context,
).small,
controller: _digitsController,
),
),
],
),
],
),
),
)
: const SizedBox.shrink(),
);
},
),
],
),
);
}
}

View File

@@ -580,7 +580,9 @@ class _HomePageState extends State<HomePage> {
return ClipRect(
child: CodeWidget(
key: ValueKey('${code.hashCode}_${newIndex}_$_codeSortKey'),
key: ValueKey(
'${code.hashCode}_${newIndex}_$_codeSortKey',
),
code,
isCompactMode: isCompactMode,
sortKey: _codeSortKey,
@@ -667,11 +669,13 @@ class _HomePageState extends State<HomePage> {
}
return false;
}
int lastScanTime = DateTime.now().millisecondsSinceEpoch - 1000;
void _handleDeeplink(BuildContext context, String? link) {
bool isAccountConfigured = Configuration.instance.hasConfiguredAccount();
bool isOfflineModeEnabled = Configuration.instance.hasOptedForOfflineMode() &&
Configuration.instance.getOfflineSecretKey() != null;
bool isOfflineModeEnabled =
Configuration.instance.hasOptedForOfflineMode() &&
Configuration.instance.getOfflineSecretKey() != null;
if (!(isAccountConfigured || isOfflineModeEnabled) || link == null) {
return;
}

View File

@@ -94,6 +94,7 @@ Future<int?> _processAegisExportFile(
final isEncrypted = decodedJson['header']['slots'] != null;
Map? aegisDB;
if (isEncrypted) {
await dialog.hide();
String? password;
try {
await showTextInputDialog(
@@ -109,6 +110,7 @@ Future<int?> _processAegisExportFile(
await dialog.hide();
return null;
}
await dialog.show();
final content = decryptAegisVault(decodedJson, password: password!);
aegisDB = jsonDecode(content);
} catch (e, s) {

View File

@@ -0,0 +1,72 @@
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:flutter/material.dart';
class ToptSelectorWidget extends StatelessWidget {
final Type currentTopt;
final void Function(Type) onSelected;
const ToptSelectorWidget({
super.key,
required this.currentTopt,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
Text toptOptionText(Type type) {
return Text(
type.name.toUpperCase(),
style: getEnteTextTheme(context).small,
);
}
return GestureDetector(
onTapDown: (TapDownDetails details) async {
final int? selectedValue = await showMenu<int>(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy + 300,
),
items: List.generate(Type.values.length, (index) {
return PopupMenuItem(
value: index,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
toptOptionText(Type.values[index]),
if (Type.values[index] == currentTopt)
Icon(
Icons.check,
color: Theme.of(context).iconTheme.color,
),
],
),
);
}),
);
if (selectedValue != null) {
onSelected(Type.values[selectedValue]);
}
},
child: Container(
padding: const EdgeInsets.only(bottom: 4),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
toptOptionText(currentTopt),
const SizedBox(width: 8),
const Icon(Icons.arrow_drop_down),
],
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
import "dart:convert";
import "dart:io";
import "dart:typed_data";
import "package:ente_auth/core/configuration.dart";
@@ -49,6 +50,8 @@ class LockScreenSettings {
/// Function to Check if the migration for lock screen changes has
/// already been done by checking a stored boolean value.
await runLockScreenChangesMigration();
await _clearLsDataInKeychainIfFreshInstall();
}
Future<void> setOfflineModeWarningStatus(bool value) async {
@@ -210,4 +213,17 @@ class LockScreenSettings {
Future<bool> isPasswordSet() async {
return await _secureStorage.containsKey(key: password);
}
// If the app was uninstalled (without logging out if it was used with
// backups), keychain items of the app persist in the keychain. To avoid using
// old keychain items, we delete them on reinstall.
Future<void> _clearLsDataInKeychainIfFreshInstall() async {
if ((Platform.isIOS || Platform.isMacOS) &&
!Configuration.instance.isLoggedIn() &&
!Configuration.instance.hasOptedForOfflineMode()) {
await _secureStorage.delete(key: password);
await _secureStorage.delete(key: pin);
await _secureStorage.delete(key: saltKey);
}
}
}

View File

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

View File

@@ -5,15 +5,15 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
version: "72.0.0"
version: "76.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
version: "0.3.3"
adaptive_theme:
dependency: "direct main"
description:
@@ -26,10 +26,10 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
version: "6.7.0"
version: "6.11.0"
ansicolor:
dependency: transitive
description:
@@ -90,10 +90,10 @@ packages:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.12.0"
auto_size_text:
dependency: "direct main"
description:
@@ -130,10 +130,10 @@ packages:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
build:
dependency: transitive
description:
@@ -202,10 +202,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
@@ -234,10 +234,10 @@ packages:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
code_builder:
dependency: transitive
description:
@@ -250,10 +250,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.1"
confetti:
dependency: "direct main"
description:
@@ -435,10 +435,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.2"
ffi:
dependency: "direct main"
description:
@@ -675,14 +675,13 @@ packages:
source: hosted
version: "9.2.2"
flutter_secure_storage_linux:
dependency: "direct overridden"
dependency: transitive
description:
path: flutter_secure_storage_linux
ref: develop
resolved-ref: "5a5692b609b3886cdd49b2ed06b9c079ecdff996"
url: "https://github.com/mogol/flutter_secure_storage.git"
source: git
version: "1.2.1"
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
@@ -945,18 +944,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.5"
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
@@ -1025,18 +1024,18 @@ packages:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
version: "0.1.3-main.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
@@ -1057,10 +1056,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.16.0"
mime:
dependency: transitive
description:
@@ -1169,10 +1168,10 @@ packages:
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.9.1"
path_drawing:
dependency: transitive
description:
@@ -1361,18 +1360,18 @@ packages:
dependency: "direct main"
description:
name: sentry
sha256: "033287044a6644a93498969449d57c37907e56f5cedb17b88a3ff20a882261dd"
sha256: "599701ca0693a74da361bc780b0752e1abc98226cf5095f6b069648116c896bb"
url: "https://pub.dev"
source: hosted
version: "8.9.0"
version: "8.14.2"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
sha256: "3780b5a0bb6afd476857cfbc6c7444d969c29a4d9bd1aa5b6960aa76c65b737a"
sha256: "5ba2cf40646a77d113b37a07bd69f61bb3ec8a73cbabe5537b05a7c89d2656f8"
url: "https://pub.dev"
source: hosted
version: "8.9.0"
version: "8.14.2"
share_plus:
dependency: "direct main"
description:
@@ -1473,7 +1472,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
version: "0.0.0"
sodium:
dependency: transitive
description:
@@ -1510,10 +1509,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.10.1"
sprintf:
dependency: transitive
description:
@@ -1567,10 +1566,10 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.12.1"
steam_totp:
dependency: "direct main"
description:
@@ -1591,10 +1590,10 @@ packages:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
stream_transform:
dependency: transitive
description:
@@ -1607,10 +1606,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.4.1"
styled_text:
dependency: "direct main"
description:
@@ -1631,18 +1630,18 @@ packages:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.7.4"
timezone:
dependency: transitive
description:
@@ -1807,10 +1806,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "14.2.5"
version: "14.3.1"
watcher:
dependency: transitive
description:
@@ -1900,5 +1899,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.5.0 <4.0.0"
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.24.0"

View File

@@ -1,7 +1,7 @@
name: ente_auth
description: ente two-factor authenticator
version: 4.3.3+433
version: 4.3.6+437
publish_to: none
environment:
@@ -84,8 +84,8 @@ dependencies:
protobuf: ^3.0.0
qr_code_scanner: ^1.0.1
qr_flutter: ^4.1.0
sentry: ^8.7.0
sentry_flutter: ^8.7.0
sentry: ^8.14.2
sentry_flutter: ^8.14.2
share_plus: ^10.0.2
shared_preferences: ^2.0.5
sqflite:
@@ -107,12 +107,6 @@ dependencies:
window_manager: ^0.4.2
xdg_directories: ^1.0.4
dependency_overrides:
flutter_secure_storage_linux:
git:
url: https://github.com/mogol/flutter_secure_storage.git
ref: develop
path: flutter_secure_storage_linux
dev_dependencies:
build_runner: ^2.1.11
flutter_test:
@@ -148,12 +142,17 @@ flutter:
fonts:
- asset: fonts/Montserrat-Bold.ttf
flutter_icons:
# run "dart run flutter_launcher_icons" to generate icons
flutter_launcher_icons:
image_path: "assets/generation-icons/icon-light.png"
android: "launcher_icon"
adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png"
adaptive_icon_background: "assets/generation-icons/icon-light-adaptive-bg.png"
adaptive_icon_monochrome: "assets/generation-icons/icon-monochrome.png"
adaptive_icon_foreground_inset: 0
ios: true
image_path: "assets/generation-icons/icon-light.png"
remove_alpha_ios: true
flutter_native_splash:

View File

@@ -14,6 +14,7 @@
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <local_auth_windows/local_auth_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <sentry_flutter/sentry_flutter_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sodium_libs/sodium_libs_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
@@ -38,6 +39,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
SentryFlutterPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
SodiumLibsPluginCApiRegisterWithRegistrar(

View File

@@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows
local_auth_windows
screen_retriever
sentry_flutter
share_plus
sodium_libs
sqlite3_flutter_libs
@@ -21,7 +22,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
sentry_flutter
)
set(PLUGIN_BUNDLED_LIBRARIES)

View File

@@ -60,7 +60,7 @@ ente account list
#### Change export directory
```shell
ente account update --email email@domain.com --dir ~/photos
ente account update --app auth/photos --email email@domain.com --dir ~/photos
```
### Export

View File

@@ -52,7 +52,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
- name: Increase yarn timeout
# `yarn install` times out sometimes on the Windows runner,

View File

@@ -1,9 +1,14 @@
# CHANGELOG
## v1.7.12 (Unreleased)
## v1.7.13 (Unreleased)
- .
## v1.7.12
- Improved video player with streaming support (for already processed videos).
- Support Arabic translations.
## v1.7.11
- Improved file viewer.

BIN
desktop/build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -39,6 +39,15 @@ export default ts.config(
"error",
{ allowTernary: true },
],
// Allow force unwrapping potentially optional values.
//
// See: [Note: non-null-assertions have better stack trace]
"@typescript-eslint/no-non-null-assertion": "off",
// Allow `while(true)` etc.
"@typescript-eslint/no-unnecessary-condition": [
"error",
{ allowConstantLoopConditions: true },
],
},
},
);

View File

@@ -1,6 +1,6 @@
{
"name": "ente",
"version": "1.7.12-beta",
"version": "1.7.13-beta",
"private": true,
"description": "Desktop client for Ente Photos",
"repository": "github:ente-io/photos-desktop",
@@ -27,36 +27,36 @@
"dependencies": {
"any-shell-escape": "^0.1.1",
"auto-launch": "^5.0.6",
"chokidar": "^3.6.0",
"chokidar": "^4.0.3",
"clip-bpe-js": "^0.0.6",
"comlink": "^4.4.2",
"compare-versions": "^6.1.1",
"electron-log": "^5.3.2",
"electron-log": "^5.4.0",
"electron-store": "^8.2.0",
"electron-updater": "^6.4.0",
"electron-updater": "^6.6.3",
"ffmpeg-static": "^5.2.0",
"lru-cache": "^11.0.2",
"lru-cache": "^11.1.0",
"next-electron-server": "^1.0.0",
"node-stream-zip": "^1.15.0",
"onnxruntime-node": "^1.20.1"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
"@tsconfig/node20": "^20.1.4",
"@eslint/js": "^9.25.1",
"@tsconfig/node22": "^22.0.1",
"@types/auto-launch": "^5.0.5",
"@types/ffmpeg-static": "^3.0.3",
"ajv": "^8.17.1",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"electron": "^34.3.4",
"electron-builder": "^26.0.0",
"electron": "^36.1.0",
"electron-builder": "^26.0.14",
"eslint": "^9",
"prettier": "3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-packagejson": "^2.5.10",
"shx": "^0.3.4",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1"
"typescript": "^5.8.3",
"typescript-eslint": "^8.31.1"
},
"packageManager": "yarn@1.22.22",
"productName": "ente"

View File

@@ -24,7 +24,7 @@
*
* Note that `vips.js` would've already run once `beforeBuild.js` is run, but on
* our CI we prepare builds for multiple architectures in one go, so we need to
* unconditonally replace the binary with the relevant one for the current
* unconditionally replace the binary with the relevant one for the current
* architecture being built (which might be different from the one we're running
* on). `beforeBuild.js` runs for each architecture being built.
*

View File

@@ -35,6 +35,7 @@ import log, { initLogging } from "./main/log";
import { createApplicationMenu, createTrayContextMenu } from "./main/menu";
import { setupAutoUpdater } from "./main/services/app-update";
import autoLauncher from "./main/services/auto-launcher";
import { shouldHideDockIcon } from "./main/services/store";
import { createWatcher } from "./main/services/watch";
import { userPreferences } from "./main/stores/user-preferences";
import { migrateLegacyWatchStoreIfNeeded } from "./main/stores/watch";
@@ -385,8 +386,8 @@ const createMainWindow = () => {
const wasAutoLaunched = autoLauncher.wasAutoLaunched();
if (wasAutoLaunched) {
// Don't automatically show the app's window if we were auto-launched.
// On macOS, also hide the dock icon on macOS.
if (process.platform == "darwin") app.dock.hide();
// On macOS, also hide the dock icon.
app.dock?.hide();
} else {
// Show our window otherwise, maximizing it if we're not asked to set it
// to a specific size.
@@ -421,14 +422,18 @@ const createMainWindow = () => {
window.on("hide", () => {
// On macOS, when hiding the window also hide the app's icon in the dock
// if the user has selected the Settings > Hide dock icon checkbox.
if (process.platform == "darwin" && userPreferences.get("hideDockIcon"))
app.dock.hide();
// unless the user has unchecked the Settings > Hide dock icon checkbox.
if (shouldHideDockIcon()) {
// macOS emits a window "hide" event when going fullscreen, and if
// we hide the dock icon there then the window disappears. So ignore
// this scenario.
if (!window.isFullScreen()) {
app.dock?.hide();
}
}
});
window.on("show", () => {
if (process.platform == "darwin") void app.dock.show();
});
window.on("show", () => void app.dock?.show());
// Let ipcRenderer know when mainWindow is in the foreground so that it can
// in turn inform the renderer process.
@@ -609,7 +614,7 @@ const handleBackOnStripeCheckout = (window: BrowserWindow) =>
*
* But this is an issue for uploads in the self hosted apps (or when we
* ourselves are trying to test things by with an arbitrary S3 bucket without
* going via a worker). During upload, theer is no redirection, so the request
* going via a worker). During upload, there is no redirection, so the request
* ACAO is "ente://app" but the response ACAO is `null` which don't match,
* causing the request to fail.
*

View File

@@ -13,6 +13,7 @@ import type { BrowserWindow } from "electron";
import { ipcMain } from "electron/main";
import type {
CollectionMapping,
FFmpegCommand,
FolderWatch,
PendingUploads,
ZipItem,
@@ -40,6 +41,7 @@ import {
fsRename,
fsRm,
fsRmdir,
fsStatMtime,
fsWriteFile,
fsWriteFileViaBackup,
} from "./services/fs";
@@ -55,8 +57,8 @@ import {
import {
clearPendingUploads,
listZipItems,
markUploadedFiles,
markUploadedZipItems,
markUploadedFile,
markUploadedZipItem,
pathOrZipItemSize,
pendingUploads,
setPendingUploads,
@@ -163,6 +165,8 @@ export const attachIPCHandlers = () => {
ipcMain.handle("fsIsDir", (_, dirPath: string) => fsIsDir(dirPath));
ipcMain.handle("fsStatMtime", (_, path: string) => fsStatMtime(path));
ipcMain.handle("fsFindFiles", (_, folderPath: string) =>
fsFindFiles(folderPath),
);
@@ -187,7 +191,7 @@ export const attachIPCHandlers = () => {
"ffmpegExec",
(
_,
command: string[],
command: FFmpegCommand,
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
outputFileExtension: string,
) => ffmpegExec(command, dataOrPathOrZipItem, outputFileExtension),
@@ -210,13 +214,15 @@ export const attachIPCHandlers = () => {
);
ipcMain.handle(
"markUploadedFiles",
(_, paths: PendingUploads["filePaths"]) => markUploadedFiles(paths),
"markUploadedFile",
(_, path: string, associatedPath: string | undefined) =>
markUploadedFile(path, associatedPath),
);
ipcMain.handle(
"markUploadedZipItems",
(_, items: PendingUploads["zipItems"]) => markUploadedZipItems(items),
"markUploadedZipItem",
(_, item: ZipItem, associatedItem: ZipItem | undefined) =>
markUploadedZipItem(item, associatedItem),
);
ipcMain.handle("clearPendingUploads", () => clearPendingUploads());

View File

@@ -7,7 +7,7 @@ import {
} from "electron";
import { allowWindowClose } from "../main";
import { forceCheckForAppUpdates } from "./services/app-update";
import { userPreferences } from "./stores/user-preferences";
import { setShouldHideDockIcon, shouldHideDockIcon } from "./services/store";
/** Create and return the entries in the app's main menu bar */
export const createApplicationMenu = (mainWindow: BrowserWindow) => {
@@ -15,7 +15,7 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
//
// Whenever the menu is redrawn the current value of these variables is used
// to set the checked state for the various settings checkboxes.
let shouldHideDockIcon = !!userPreferences.get("hideDockIcon");
let hideDockIcon = shouldHideDockIcon();
const macOSOnly = (options: MenuItemConstructorOptions[]) =>
process.platform == "darwin" ? options : [];
@@ -24,9 +24,9 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
const toggleHideDockIcon = () => {
// Persist
userPreferences.set("hideDockIcon", !shouldHideDockIcon);
setShouldHideDockIcon(!hideDockIcon);
// And update the in-memory state
shouldHideDockIcon = !shouldHideDockIcon;
hideDockIcon = !hideDockIcon;
};
const handleHelp = () =>
@@ -48,7 +48,7 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
{
label: "Hide Dock Icon",
type: "checkbox",
checked: shouldHideDockIcon,
checked: hideDockIcon,
click: toggleHideDockIcon,
},
],

View File

@@ -1,11 +1,13 @@
import pathToFfmpeg from "ffmpeg-static";
import { randomBytes } from "node:crypto";
import fs from "node:fs/promises";
import type { ZipItem } from "../../types/ipc";
import { ensure } from "../utils/common";
import path, { basename } from "node:path";
import type { FFmpegCommand, ZipItem } from "../../types/ipc";
import log from "../log";
import { execAsync } from "../utils/electron";
import {
deleteTempFileIgnoringErrors,
makeFileForDataOrPathOrZipItem,
makeFileForDataOrStreamOrPathOrZipItem,
makeTempFilePath,
} from "../utils/temp";
@@ -42,7 +44,7 @@ const outputPathPlaceholder = "OUTPUT";
* But I'm not sure if our code is supposed to be able to use it, and how.
*/
export const ffmpegExec = async (
command: string[],
command: FFmpegCommand,
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
outputFileExtension: string,
): Promise<Uint8Array> => {
@@ -50,14 +52,23 @@ export const ffmpegExec = async (
path: inputFilePath,
isFileTemporary: isInputFileTemporary,
writeToTemporaryFile: writeToTemporaryInputFile,
} = await makeFileForDataOrPathOrZipItem(dataOrPathOrZipItem);
} = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem);
const outputFilePath = await makeTempFilePath(outputFileExtension);
try {
await writeToTemporaryInputFile();
let resolvedCommand: string[];
if (Array.isArray(command)) {
resolvedCommand = command;
} else {
const isHDR = await isHDRVideo(inputFilePath);
log.debug(() => [basename(inputFilePath), { isHDR }]);
resolvedCommand = isHDR ? command.hdr : command.default;
}
const cmd = substitutePlaceholders(
command,
resolvedCommand,
inputFilePath,
outputFilePath,
);
@@ -99,18 +110,17 @@ const ffmpegBinaryPath = () => {
// This substitution of app.asar by app.asar.unpacked is suggested by the
// ffmpeg-static library author themselves:
// https://github.com/eugeneware/ffmpeg-static/issues/16
return ensure(pathToFfmpeg).replace("app.asar", "app.asar.unpacked");
return pathToFfmpeg!.replace("app.asar", "app.asar.unpacked");
};
/**
* A variant of {@link ffmpegExec} adapted to work with streams so that it can
* handle the MP4 conversion of large video files.
*
* See: [Note: Convert to MP4]
* @param inputFilePath The path to a file on the user's local file system. This
* is the video we want to convert.
* @param inputFilePath The path to a file on the user's local file system where
*
* @param outputFilePath The path to a file on the user's local file system where
* we should write the converted MP4 video.
*/
export const ffmpegConvertToMP4 = async (
@@ -130,3 +140,539 @@ export const ffmpegConvertToMP4 = async (
await execAsync(cmd);
};
export interface FFmpegGenerateHLSPlaylistAndSegmentsResult {
playlistPath: string;
videoPath: string;
dimensions: { width: number; height: number };
videoSize: number;
}
/**
* A bespoke variant of {@link ffmpegExec} for generation of HLS playlists for
* videos.
*
* Overview of the cases:
*
* H.264, <= 10 MB - Skip
* H.264, <= 4000 kb/s bitrate - Don't re-encode video stream
* BT.709, <= 2000 kb/s bitrate - Don't apply the scale+fps filter
* !BT.709 - Apply tonemap (zscale+tonemap+zscale)
*
* Example invocation:
*
* ffmpeg -i in.mov -vf 'scale=-2:720,fps=30,zscale=transfer=linear,tonemap=tonemap=hable:desat=0,zscale=primaries=709:transfer=709:matrix=709,format=yuv420p' -c:v libx264 -c:a aac -f hls -hls_key_info_file out.m3u8.info -hls_list_size 0 -hls_flags single_file out.m3u8
*
* See: [Note: Preview variant of videos]
*
* @param inputFilePath The path to a file on the user's local file system. This
* is the video we want to generate an streamable HLS playlist for.
*
* @param outputPathPrefix The path to unique, unused and temporary prefix on
* the user's local file system. This function will write the generated HLS
* playlist and video segments under this prefix.
*
* @returns The paths to two files on the user's local file system - one
* containing the generated HLS playlist, and the other containing the
* transcoded and encrypted video segments that the HLS playlist refers to.
*
* If the video is such that it doesn't require stream generation, then this
* function returns `undefined`.
*/
export const ffmpegGenerateHLSPlaylistAndSegments = async (
inputFilePath: string,
outputPathPrefix: string,
): Promise<FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined> => {
const { isH264, isBT709, bitrate } =
await detectVideoCharacteristics(inputFilePath);
log.debug(() => [basename(inputFilePath), { isH264, isBT709, bitrate }]);
// If the video is smaller than 10 MB, and already H.264 (the codec we are
// going to use for the conversion), then a streaming variant is not much
// use. Skip such cases.
//
// ---
//
// [Note: HEVC/H.265 issues]
//
// We've observed two issues out in the wild with HEVC videos:
//
// 1. On Linux, HEVC video streams don't play. However, since the audio
// stream plays, the browser tells us that the "video" itself is
// playable, but the user sees a blank screen with only audio.
//
// 2. HEVC + HDR videos taken on an iPhone have a rotation (`Side data:
// displaymatrix` in the ffmpeg output) that Chrome (and thus Electron)
// doesn't take into account, so these play upside down.
//
// Not fully related to this case, but mentioning here as to why both the
// size and codec need to be checked before skipping stream generation.
if (isH264) {
const inputVideoSize = await fs
.stat(inputFilePath)
.then((st) => st.size);
if (inputVideoSize <= 10 * 1024 * 1024 /* 10 MB */) {
return undefined;
}
}
// If the video is already H.264 with a bitrate less than 4000 kbps, then we
// do not need to reencode the video stream (by _far_ the costliest part of
// the HLS stream generation).
const reencodeVideo = !(isH264 && bitrate && bitrate <= 4000 * 1000);
// If the bitrate is not too high, then we don't need to rescale the video
// when generating the video stream. This is not a performance optimization,
// but more for avoiding making the video size smaller unnecessarily.
const rescaleVideo = !(bitrate && bitrate <= 2000 * 1000);
// [Note: Tonemapping HDR to HD]
//
// BT.709 ("HD") is a standard that describes things like how color is
// encoded, the range of values, and their "meaning" - i.e. how to map the
// values in the video to the pixels on the screen.
//
// It is not the only such standard, there are three common examples:
//
// - BT.601 ("Standard-Definition" or SD)
// - BT.709 ("High-Definition" or HD)
// - BT.2020 ("Ultra-High-Definition" or UHD, aka HDR^).
//
// ^ HDR ("High-Dynamic-Range") is an addendum to BT.2020, but for our
// purpose here we can treat it as as alias.
//
// BT.709 is the most common amongst these for older files out stored on
// computers, and they conform mostly to the standard (one notable exception
// is that the BT.709 standard also recommends using the yuv422p pixel
// format, but de facto yuv420p is used because many video players only
// support yuv420p).
//
// Since BT.709 is the most widely supported standard, we use it when
// generating the HLS playlist so to allow playback across the widest
// possible hardware/OS/browser combinations.
//
// If we convert HDR to HD without naively, then the colors look washed out
// compared to the original. To resolve this, we use a ffmpeg filterchain
// that uses the tonemap filter.
//
// However applying this tonemap to videos that are already HD leads to a
// brightness drop. So we conditionally apply this filter chain only if the
// colorspace is not already BT.709.
//
// See also: [Note: Alternative FFmpeg command for HDR videos], although
// that uses a allow-list based check (while here we use deny-list).
//
// Reference:
// - https://trac.ffmpeg.org/wiki/colorspace
const tonemap = !isBT709;
// We want the generated playlist to refer to the chunks as "output.ts".
//
// So we arrange things accordingly: We use the `outputPathPrefix` as our
// working directory, and then ask ffmpeg to generate a playlist with the
// name "output.m3u8".
//
// ffmpeg will automatically place the segments in a file with the same base
// name as the playlist, but with a ".ts" extension. And since we use the
// "single_file" option, all the segments will be placed in a file named
// "output.ts".
await fs.mkdir(outputPathPrefix);
const playlistPath = path.join(outputPathPrefix, "output.m3u8");
const videoPath = path.join(outputPathPrefix, "output.ts");
// Generate a cryptographically secure random key (16 bytes).
const keyBytes = randomBytes(16);
const keyB64 = keyBytes.toString("base64");
// Convert it to a data: URI that will be added to the playlist.
const keyURI = `data:text/plain;base64,${keyB64}`;
// Determine two paths - one where we will write the key itself, and where
// we will write the "key info" that provides ffmpeg the `keyURI` and the
// `keyPath;.
const keyPath = playlistPath + ".key";
const keyInfoPath = playlistPath + ".key-info";
// Generate a "key info":
//
// - the first line specifies the key URI that is written into the playlist.
// - the second line specifies the path to the local file system file from
// where ffmpeg should read the key.
const keyInfo = [keyURI, keyPath].join("\n");
// Overview:
//
// - Video H.264 HD 720p 30fps.
// - Audio AAC 128kbps.
// - Encrypted HLS playlist with a single file containing all the chunks.
//
// Reference:
// - `man ffmpeg-all`
// - https://trac.ffmpeg.org/wiki/Encode/H.264
//
const command = [
ffmpegBinaryPath(),
// Reduce the amount of output lines we have to parse.
["-hide_banner"],
// Input file. We don't need any extra options that apply to the input file.
"-i",
inputFilePath,
// The remaining options apply to the next output file (`playlistPath`).
reencodeVideo
? [
// `-vf` creates a filter graph for the video stream. It is a
// comma separated list of filters chained together, e.g.
// `filter1=key=value:key=value.filter2=key=value`.
"-vf",
[
// Do the rescaling to even number of pixels always if the
// tonemapping is going to be applied subsequently,
// otherwise the tonemapping will fail with "image
// dimensions must be divisible by subsampling factor".
//
// While we add the extra condition here for completeness,
// it won't usually matter since a non-BT.709 video is
// likely using a new codec, and as such would've a high
// enough bitrate to require rescaling anyways.
rescaleVideo || tonemap
? [
// Scales the video to maximum 720p height,
// keeping aspect ratio and the calculated
// dimension divisible by 2 (some of the other
// operations require an even pixel count).
"scale=-2:720",
// Convert the video to a constant 30 fps,
// duplicating or dropping frames as necessary.
"fps=30",
]
: [],
// Convert the colorspace if the video is not in the HD
// color space (bt709). Before conversion, tone map colors
// so that they work the same across the change in the
// dyamic range.
//
// 1. The tonemap filter only works linear light, so we
// first use zscale with transfer=linear to linearize
// the input.
//
// 2. Then we use the tonemap, with the hable option that
// is best for preserving details. desat=0 turns off
// the default desaturation.
//
// 3. Use zscale again to "convert to BT.709" by asking it
// to set the all three of color primaries, transfer
// characteristics and colorspace matrix to 709 (Note:
// the constants specified in the tonemap filter help
// do not include the "bt" prefix)
//
// See: https://ffmpeg.org/ffmpeg-filters.html#tonemap-1
//
// See: [Note: Tonemapping HDR to HD]
tonemap
? [
"zscale=transfer=linear",
"tonemap=tonemap=hable:desat=0",
"zscale=primaries=709:transfer=709:matrix=709",
]
: [],
// Output using the well supported pixel format: 8-bit YUV
// planar color space with 4:2:0 chroma subsampling.
"format=yuv420p",
]
.flat()
.join(","),
]
: [],
reencodeVideo
? // Video codec H.264
//
// - `-c:v libx264` converts the video stream to the H.264 codec.
//
// - We don't supply a bitrate, instead it uses the default CRF
// ("23") as recommended in the ffmpeg trac.
//
// - We don't supply a preset, it'll use the default ("medium").
["-c:v", "libx264"]
: // Keep the video stream unchanged
["-c:v", "copy"],
// Audio codec AAC
//
// - `-c:a aac` converts the audio stream to use the AAC codec
//
// - We don't supply a bitrate, it'll use the AAC default 128k bps.
["-c:a", "aac"],
// Generate a HLS playlist.
["-f", "hls"],
// Tell ffmpeg where to find the key, and the URI for the key to write
// into the generated playlist. Implies "-hls_enc 1".
["-hls_key_info_file", keyInfoPath],
// Generate as many playlist entries as needed (default limit is 5).
["-hls_list_size", "0"],
// Place all the video segments within the same .ts file (with the same
// path as the playlist file but with a ".ts" extension).
["-hls_flags", "single_file"],
// Output path where the playlist should be generated.
playlistPath,
].flat();
let dimensions: ReturnType<typeof detectVideoDimensions>;
let videoSize: number;
try {
// Write the key and the keyInfo to their desired paths.
await Promise.all([
fs.writeFile(keyPath, keyBytes),
fs.writeFile(keyInfoPath, keyInfo, { encoding: "utf8" }),
]);
// Run the ffmpeg command to generate the HLS playlist and segments.
//
// Note: Depending on the size of the input file, this may take long!
const { stderr: conversionStderr } = await execAsync(command);
// Determine the dimensions of the generated video from the stderr
// output produced by ffmpeg during the conversion.
dimensions = detectVideoDimensions(conversionStderr);
// Find the size of the generated video segments by reading the size of
// the generated .ts file.
videoSize = await fs.stat(videoPath).then((st) => st.size);
} catch (e) {
log.error("HLS generation failed", e);
await Promise.all([
deleteTempFileIgnoringErrors(playlistPath),
deleteTempFileIgnoringErrors(videoPath),
]);
throw e;
} finally {
await Promise.all([
deleteTempFileIgnoringErrors(keyInfoPath),
deleteTempFileIgnoringErrors(keyPath),
// ffmpeg writes a /path/output.ts.tmp, clear it out too.
deleteTempFileIgnoringErrors(videoPath + ".tmp"),
]);
}
return { playlistPath, videoPath, dimensions, videoSize };
};
/**
* A regex that matches the first line of the form
*
* Stream #0:0: Video: h264 (High 10) ([27][0][0][0] / 0x001B), yuv420p10le(tv, bt2020nc/bt2020/arib-std-b67), 1920x1080, 30 fps, 30 tbr, 90k tbn
*
* The part after Video: is the first capture group.
*
* Another example:
*
* Stream #0:1[0x2](und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(progressive), 480x270 [SAR 1:1 DAR 16:9], 539 kb/s, 29.97 fps, 29.97 tbr, 30k tbn (default)
*/
const videoStreamLineRegex = /Stream #.+: Video:(.+)\n/;
/** {@link videoStreamLineRegex}, but global. */
const videoStreamLinesRegex = /Stream #.+: Video:(.+)\n/g;
/**
* A regex that matches "<digits> kb/s" preceded by a space. See
* {@link videoStreamLineRegex} for the context in which it is used.
*/
const videoBitrateRegex = / ([1-9]\d*) kb\/s/;
/**
* A regex that matches <digits>x<digits> pair preceded by a space. See
* {@link videoStreamLineRegex} for the context in which it is used.
*
* We constrain the digit sequence not to begin with 0 to exclude hexadecimal
* representations of various constants that ffmpeg prints on this line (e.g.
* "avc1 / 0x31637661").
*/
const videoDimensionsRegex = / ([1-9]\d*)x([1-9]\d*)/;
interface VideoCharacteristics {
isH264: boolean;
isBT709: boolean;
bitrate: number | undefined;
}
/**
* Heuristically determine information about the video at the given
* {@link inputFilePath}:
*
* - If is encoded using H.264 codec.
* - If it uses the BT.709 colorspace.
* - Its bitrate.
*
* The defaults are tailored for the cases in which these conditions are used,
* so that even if we get the detection wrong we'll only end up encoding videos
* that could've possibly been skipped as an optimization.
*
* [Note: Parsing CLI output might break on ffmpeg updates]
*
* This function tries to determine the these bits of information about the
* given video by scanning the ffmpeg info output for the video stream line, and
* doing various string matches and regex extractions.
*
* Needless to say, while this works currently, this is liable to break in the
* future. So if something stops working after updating ffmpeg, look here!
*
* Ideally, we'd have done this using `ffprobe`, but we don't have the ffprobe
* binary at hand, so we make do by grepping the log output of ffmpeg.
*
* For reference,
*
* - codec and colorspace are printed by the `avcodec_string` function in the
* ffmpeg source:
* https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/avcodec.c
*
* - bitrate is printed by the `dump_stream_format` function in `dump.c`.
*/
const detectVideoCharacteristics = async (inputFilePath: string) => {
const videoInfo = await pseudoFFProbeVideo(inputFilePath);
const videoStreamLine = videoStreamLineRegex.exec(videoInfo)?.at(1)?.trim();
// Since the checks are heuristic, start with defaults that would cause the
// codec conversion to happen, even if it is unnecessary.
const res: VideoCharacteristics = {
isH264: false,
isBT709: false,
bitrate: undefined,
};
if (!videoStreamLine) return res;
res.isH264 = videoStreamLine.startsWith("h264 ");
res.isBT709 = videoStreamLine.includes("bt709");
// The regex matches "\d kb/s", but there can be other units for the
// bitrate. However, (a) "kb/s" is the most common for videos out in the
// wild, and (b) even if we guess wrong it we'll just do "-v:c x264" instead
// of "-v:c copy", so only unnecessary processing but no change in output.
const brs = videoBitrateRegex.exec(videoStreamLine)?.at(0);
if (brs) {
const br = parseInt(brs, 10);
if (br) res.bitrate = br;
}
return res;
};
/**
* Heuristically detect the dimensions of the given video from the log output of
* the ffmpeg invocation during the HLS playlist generation.
*
* This function tries to determine the width and height of the generated video
* from the output log written by ffmpeg on its stderr during the generation
* process, scanning it for the last video stream line, and trying to match a
* "<digits>x<digits>" regex.
*
* See: [Note: Parsing CLI output might break on ffmpeg updates].
*/
const detectVideoDimensions = (conversionStderr: string) => {
// There is a nicer way to do it - by running `pseudoFFProbeVideo` on the
// generated playlist. However, that playlist includes a data URL that
// specifies the encryption info, and ffmpeg refuses to read that unless we
// specify the "-allowed_extensions ALL" or something to that effect.
//
// Unfortunately, our current ffmpeg binary (5.x) does not support that
// option. So we instead parse the conversion output itself.
//
// This is also nice, since it saves on an extra ffmpeg invocation. But we
// now need to be careful to find the right video stream line, since the
// conversion output includes both the input and output video stream lines.
//
// To match the right (output) video stream line, we use a global regex, and
// use the last match since that'd correspond to the single video stream
// written in the output.
const videoStreamLine = Array.from(
conversionStderr.matchAll(videoStreamLinesRegex),
)
.at(-1) /* Last Stream...: Video: line in the output */
?.at(1); /* First capture group */
if (videoStreamLine) {
const [, ws, hs] = videoDimensionsRegex.exec(videoStreamLine) ?? [];
if (ws && hs) {
const w = parseInt(ws, 10);
const h = parseInt(hs, 10);
if (w && h) {
return { width: w, height: h };
}
}
}
throw new Error(
`Unable to detect video dimensions from stream line [${videoStreamLine ?? ""}]`,
);
};
/**
* Heuristically detect if the file at given path is a HDR video.
*
* This is similar to {@link detectVideoCharacteristics}, and see that
* function's documentation for all the caveats. However, this function uses an
* allow-list instead, and considers any file with color transfer "smpte2084" or
* "arib-std-b67" to be HDR. While this is in some sense a more exact check, it
* comes with different caveats:
*
* - These particular constants are not guaranteed to be correct; these are just
* what I saw on the internet as being used / recommended for detecting HDR.
*
* - Since we don't have ffprobe, we're not checking the color space value
* itself but a substring of the stream line in the ffmpeg stderr output.
*
* In particular, we use this more exact check for places where we have less
* leeway. e.g. when generating thumbnails, if we apply the tonemapping to any
* non-BT.709 file (as the HLS stream generation does), we start getting the
* "code 3074: no path between colorspaces" error during the JPEG conversion
* (this is not a problem in the H.264 conversion).
*
* - See: [Note: Alternative FFmpeg command for HDR videos]
* - See: [Note: Tonemapping HDR to HD]
*
* @param inputFilePath The path to a video file on the user's machine.
*
* @returns `true` if this file is likely a HDR video. Exceptions are treated as
* `false` to make this function safe to invoke without breaking the happy path.
*/
const isHDRVideo = async (inputFilePath: string) => {
try {
const videoInfo = await pseudoFFProbeVideo(inputFilePath);
const vs = videoStreamLineRegex.exec(videoInfo)?.at(1);
if (!vs) return false;
return vs.includes("smpte2084") || vs.includes("arib-std-b67");
} catch (e) {
log.warn(`Could not detect HDR status of ${inputFilePath}`, e);
return false;
}
};
/**
* Return the stderr of ffmpeg in an attempt to gain information about the video
* at the given {@link inputFilePath}.
*
* We don't have the ffprobe binary at hand, which is why we need to use this
* alternative. See: [Note: Parsing CLI output might break on ffmpeg updates]
*
* @returns the stderr of ffmpeg after running it on the input file. The exact
* command we run is:
*
* ffmpeg -i in.mov -an -frames:v 0 -f null - 2>info.txt
*
* And the returned string is the contents of the `info.txt` thus produced.
*/
const pseudoFFProbeVideo = async (inputFilePath: string) => {
const command = [
ffmpegPathPlaceholder,
// Reduce the amount of output lines we have to parse.
["-hide_banner"],
["-i", inputPathPlaceholder],
"-an",
["-frames:v", "0"],
["-f", "null"],
"-",
].flat();
const cmd = substitutePlaceholders(command, inputFilePath, /* NA */ "");
const { stderr } = await execAsync(cmd);
return stderr;
};

View File

@@ -36,6 +36,17 @@ export const fsIsDir = async (dirPath: string) => {
return stat.isDirectory();
};
export const fsStatMtime = (path: string) =>
// [Note: Integral last modified time]
//
// Whenever we need to find the modified time of a file, use the
// `mtime.getTime()` instead of `mtimeMs` of the stat; this way, it is
// guaranteed that the times are integral (we persist these values to remote
// in some cases, and the contract is for them to be integral; mtimeMs is a
// float with sub-millisecond precision), and that all places use the same
// value so that they're comparable.
fs.stat(path).then((st) => st.mtime.getTime());
export const fsFindFiles = async (dirPath: string) => {
const items = await fs.readdir(dirPath, { withFileTypes: true });
let paths: string[] = [];

View File

@@ -6,7 +6,7 @@ import { type ZipItem } from "../../types/ipc";
import { execAsync, isDev } from "../utils/electron";
import {
deleteTempFileIgnoringErrors,
makeFileForDataOrPathOrZipItem,
makeFileForDataOrStreamOrPathOrZipItem,
makeTempFilePath,
} from "../utils/temp";
@@ -69,7 +69,7 @@ export const generateImageThumbnail = async (
path: inputFilePath,
isFileTemporary: isInputFileTemporary,
writeToTemporaryFile: writeToTemporaryInputFile,
} = await makeFileForDataOrPathOrZipItem(dataOrPathOrZipItem);
} = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem);
const outputFilePath = await makeTempFilePath("jpeg");

View File

@@ -1,6 +1,6 @@
import type { FSWatcher } from "chokidar";
import log from "../log";
import { clearConvertToMP4Results } from "../stream";
import { clearPendingVideoResults } from "../stream";
import { clearStores } from "./store";
import { watchReset } from "./watch";
import { clearOpenZipCache } from "./zip";
@@ -22,9 +22,9 @@ export const logout = (watcher: FSWatcher) => {
ignoreError("FS watch", e);
}
try {
clearConvertToMP4Results();
clearPendingVideoResults();
} catch (e) {
ignoreError("convert-to-mp4", e);
ignoreError("video", e);
}
try {
clearStores();

View File

@@ -16,8 +16,9 @@ import fs from "node:fs/promises";
import path from "node:path";
import * as ort from "onnxruntime-node";
import { messagePortMainEndpoint } from "../utils/comlink";
import { ensure, wait } from "../utils/common";
import { wait } from "../utils/common";
import { writeStream } from "../utils/stream";
import { fsStatMtime } from "./fs";
/**
* We cannot do
@@ -63,12 +64,13 @@ process.parentPort.once("message", (e) => {
// parent.
expose(
{
fsStatMtime,
computeCLIPImageEmbedding,
computeCLIPTextEmbeddingIfAvailable,
detectFaces,
computeFaceEmbeddings,
},
messagePortMainEndpoint(ensure(e.ports[0])),
messagePortMainEndpoint(e.ports[0]!),
);
});
@@ -80,7 +82,7 @@ process.parentPort.once("message", (e) => {
let _userDataPath: string | undefined;
/** Equivalent to app.getPath("userData") */
const userDataPath = () => ensure(_userDataPath);
const userDataPath = () => _userDataPath!;
const parseInitData = (data: unknown) => {
if (
@@ -131,10 +133,8 @@ const makeCachedInferenceSession = (
const createSession = (modelPath: string) =>
createInferenceSession(modelPath);
const cachedInferenceSession = () => {
if (!session) session = download().then(createSession);
return session;
};
const cachedInferenceSession = () =>
(session ??= download().then(createSession));
return cachedInferenceSession;
};
@@ -252,7 +252,7 @@ export const computeCLIPImageEmbedding = async (
const results = await session.run(feeds);
log.debugString(`ONNX/CLIP image embedding took ${Date.now() - t} ms`);
/* Need these model specific casts to type the result */
return ensure(results.output).data as Float32Array;
return results.output!.data as Float32Array;
};
const cachedCLIPTextSession = makeCachedInferenceSession(
@@ -261,10 +261,7 @@ const cachedCLIPTextSession = makeCachedInferenceSession(
);
let _tokenizer: Tokenizer | undefined;
const getTokenizer = () => {
if (!_tokenizer) _tokenizer = new Tokenizer();
return _tokenizer;
};
const getTokenizer = () => (_tokenizer ??= new Tokenizer());
/**
* Compute CLIP embeddings for an text snippet.
@@ -295,7 +292,7 @@ export const computeCLIPTextEmbeddingIfAvailable = async (text: string) => {
const t = Date.now();
const results = await session.run(feeds);
log.debugString(`ONNX/CLIP text embedding took ${Date.now() - t} ms`);
return ensure(results.output).data as Float32Array;
return results.output!.data as Float32Array;
};
const cachedFaceDetectionSession = makeCachedInferenceSession(
@@ -316,7 +313,7 @@ export const detectFaces = async (
const t = Date.now();
const results = await session.run(feeds);
log.debugString(`ONNX/YOLO face detection took ${Date.now() - t} ms`);
return ensure(results.output).data;
return results.output!.data;
};
const cachedFaceEmbeddingSession = makeCachedInferenceSession(

View File

@@ -42,3 +42,31 @@ export const lastShownChangelogVersion = (): number | undefined =>
export const setLastShownChangelogVersion = (version: number) =>
userPreferences.set("lastShownChangelogVersion", version);
/**
* Return true if the dock icon should be hidden when the window is closed
* [macOS only].
*
* On macOS, if this function returns true then when hiding ("closing" it with
* the x traffic light) the window we also hide the app's icon in the dock. The
* user can modify their preference using the Menu bar > ente > Settings > Hide
* dock icon checkbox.
*
* If the user has not set a value for this preference (i.e., the value is
* `undefined`), we use the default `true`. This is confusing, but this way we
* can retain the preexisting preference key instead of doing a migration.
*
* Value | Behaviour
* ----------|--------------
* undefined | default (hide)
* false | show
* true | hide
*
* On non-macOS platforms, it always returns false.
*/
export const shouldHideDockIcon = (): boolean =>
process.platform == "darwin" &&
userPreferences.get("hideDockIcon") !== false;
export const setShouldHideDockIcon = (hide: boolean) =>
userPreferences.set("hideDockIcon", hide);

View File

@@ -135,22 +135,35 @@ export const setPendingUploads = ({
});
};
export const markUploadedFiles = (paths: string[]) => {
export const markUploadedFile = (
path: string,
associatedPath: string | undefined,
) => {
const existing = uploadStatusStore.get("filePaths") ?? [];
const updated = existing.filter((p) => !paths.includes(p));
const updated = existing.filter((p) => p != path && p != associatedPath);
uploadStatusStore.set("filePaths", updated);
// See: [Note: Integral last modified time]
return fs.stat(path).then((st) => st.mtime.getTime());
};
export const markUploadedZipItems = (
items: [zipPath: string, entryName: string][],
export const markUploadedZipItem = (
item: ZipItem,
associatedItem: ZipItem | undefined,
) => {
const existing = uploadStatusStore.get("zipItems") ?? [];
const updated = existing.filter(
(z) => !items.some((e) => z[0] == e[0] && z[1] == e[1]),
const updated = exceptZipItem(
exceptZipItem(existing, item),
associatedItem,
);
uploadStatusStore.set("zipItems", updated);
return fs.stat(item[0]).then((st) => st.mtime.getTime());
};
const exceptZipItem = (items: ZipItem[], item: ZipItem | undefined) =>
item
? items.filter((zi) => !(zi[0] == item[0] && zi[1] == item[1]))
: items;
export const clearPendingUploads = () => {
uploadStatusStore.clear();
clearOpenZipCache();

View File

@@ -3,17 +3,23 @@
*/
import { net, protocol } from "electron/main";
import { randomUUID } from "node:crypto";
import fs_ from "node:fs";
import fs from "node:fs/promises";
import { Writable } from "node:stream";
import { Readable, Writable } from "node:stream";
import { pathToFileURL } from "node:url";
import log from "./log";
import { ffmpegConvertToMP4 } from "./services/ffmpeg";
import {
ffmpegConvertToMP4,
ffmpegGenerateHLSPlaylistAndSegments,
type FFmpegGenerateHLSPlaylistAndSegmentsResult,
} from "./services/ffmpeg";
import { markClosableZip, openZip } from "./services/zip";
import { ensure } from "./utils/common";
import { wait } from "./utils/common";
import { writeStream } from "./utils/stream";
import {
deleteTempFile,
deleteTempFileIgnoringErrors,
makeFileForDataOrStreamOrPathOrZipItem,
makeTempFilePath,
} from "./utils/temp";
@@ -57,25 +63,39 @@ const handleStreamRequest = async (request: Request): Promise<Response> => {
const { host, searchParams } = new URL(url);
switch (host) {
case "read":
return handleRead(ensure(searchParams.get("path")));
return handleRead(searchParams.get("path")!);
case "read-zip":
return handleReadZip(
ensure(searchParams.get("zipPath")),
ensure(searchParams.get("entryName")),
searchParams.get("zipPath")!,
searchParams.get("entryName")!,
);
case "write":
return handleWrite(ensure(searchParams.get("path")), request);
return handleWrite(searchParams.get("path")!, request);
case "video": {
const op = searchParams.get("op");
if (op) {
switch (op) {
case "convert-to-mp4":
return handleConvertToMP4Write(request);
case "generate-hls":
return handleGenerateHLSWrite(request, searchParams);
default:
return new Response(`Unknown op ${op}`, {
status: 404,
});
}
}
case "convert-to-mp4": {
const token = searchParams.get("token");
const done = searchParams.get("done") !== null;
return token
? done
? handleConvertToMP4ReadDone(token)
: handleConvertToMP4Read(token)
: handleConvertToMP4Write(request);
if (!token) {
return new Response("Missing token", { status: 404 });
}
return done ? handleVideoDone(token) : handleVideoRead(token);
}
default:
@@ -105,6 +125,7 @@ const handleRead = async (path: string) => {
res.headers.set("Content-Length", `${fileSize}`);
// Add the file's last modified time (as epoch milliseconds).
// See: [Note: Integral last modified time]
const mtimeMs = stat.mtime.getTime();
res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`);
}
@@ -166,21 +187,21 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
};
const handleWrite = async (path: string, request: Request) => {
await writeStream(path, ensure(request.body));
await writeStream(path, request.body!);
return new Response("", { status: 200 });
};
/**
* A map from token to file paths for convert-to-mp4 requests that we have
* received.
* A map from token to file paths generated as a result of stream://video
* requests we have received.
*/
const convertToMP4Results = new Map<string, string>();
const pendingVideoResults = new Map<string, string>();
/**
* Clear any in-memory state for in-flight convert-to-mp4 requests. Meant to be
* called during logout.
* Clear any in-memory state for in-flight streamed video processing requests.
* Meant to be called during logout.
*/
export const clearConvertToMP4Results = () => convertToMP4Results.clear();
export const clearPendingVideoResults = () => pendingVideoResults.clear();
/**
* [Note: Convert to MP4]
@@ -195,26 +216,26 @@ export const clearConvertToMP4Results = () => convertToMP4Results.clear();
* mode for the Web fetch API). So we need to simulate that using two different
* streaming requests.
*
* renderer → main stream://convert-to-mp4
* renderer → main stream://video?op=convert-to-mp4
* → request.body is the original video
* ← response is a token
* ← response is [token]
*
* renderer → main stream://convert-to-mp4?token=<token>
* renderer → main stream://video?token=<token>
* ← response.body is the converted video
*
* renderer → main stream://convert-to-mp4?token=<token>&done
* renderer → main stream://video?token=<token>&done
* ← 200 OK
*
* Note that the conversion itself is not streaming. The conversion still
* happens in a single shot, we are just streaming the data across the IPC
* boundary to allow us to pass large amounts of data without running out of
* memory.
* happens in a single invocation of ffmpeg, we are just streaming the data
* across the IPC boundary to allow us to pass large amounts of data without
* running out of memory.
*
* See also: [Note: IPC streams]
*/
const handleConvertToMP4Write = async (request: Request) => {
const inputTempFilePath = await makeTempFilePath();
await writeStream(inputTempFilePath, ensure(request.body));
await writeStream(inputTempFilePath, request.body!);
const outputTempFilePath = await makeTempFilePath("mp4");
try {
@@ -228,25 +249,177 @@ const handleConvertToMP4Write = async (request: Request) => {
}
const token = randomUUID();
convertToMP4Results.set(token, outputTempFilePath);
pendingVideoResults.set(token, outputTempFilePath);
return new Response(token, { status: 200 });
};
const handleConvertToMP4Read = async (token: string) => {
const filePath = convertToMP4Results.get(token);
const handleVideoRead = async (token: string) => {
const filePath = pendingVideoResults.get(token);
if (!filePath)
return new Response(`Unknown token ${token}`, { status: 404 });
return net.fetch(pathToFileURL(filePath).toString());
};
const handleConvertToMP4ReadDone = async (token: string) => {
const filePath = convertToMP4Results.get(token);
const handleVideoDone = async (token: string) => {
const filePath = pendingVideoResults.get(token);
if (!filePath)
return new Response(`Unknown token ${token}`, { status: 404 });
await deleteTempFile(filePath);
convertToMP4Results.delete(token);
pendingVideoResults.delete(token);
return new Response("", { status: 200 });
};
/**
* Generate a HLS playlist for the given video.
*
* See: [Note: Convert to MP4] for the general architecture of commands that do
* renderer <-> main I/O using streams.
*
* The difference here is that we the conversion generates two streams^ - one
* for the HLS playlist itself, and one for the file containing the encrypted
* and transcoded video chunks. The video stream we write to the objectUploadURL
* (provided via {@link params}), and then we return a JSON object containing
* the token for the playlist, and other metadata for use by the renderer.
*
* ^ if the video doesn't require a stream to be generated (e.g. it is very
* small and already uses a compatible codec) then a HTT 204 is returned and
* no stream is generated.
*/
const handleGenerateHLSWrite = async (
request: Request,
params: URLSearchParams,
) => {
const objectUploadURL = params.get("objectUploadURL");
if (!objectUploadURL) throw new Error("Missing objectUploadURL");
let inputItem: Parameters<typeof makeFileForDataOrStreamOrPathOrZipItem>[0];
const path = params.get("path");
if (path) {
inputItem = path;
} else {
const zipPath = params.get("zipPath");
const entryName = params.get("entryName");
if (zipPath && entryName) {
inputItem = [zipPath, entryName];
} else {
const body = request.body;
if (!body) throw new Error("Missing body");
inputItem = body;
}
}
const {
path: inputFilePath,
isFileTemporary: isInputFileTemporary,
writeToTemporaryFile: writeToTemporaryInputFile,
} = await makeFileForDataOrStreamOrPathOrZipItem(inputItem);
const outputFilePathPrefix = await makeTempFilePath();
let result: FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined;
try {
await writeToTemporaryInputFile();
result = await ffmpegGenerateHLSPlaylistAndSegments(
inputFilePath,
outputFilePathPrefix,
);
if (!result) {
// This video doesn't require stream generation.
return new Response(null, { status: 204 });
}
const { playlistPath, videoPath } = result;
try {
await uploadVideoSegments(videoPath, objectUploadURL);
const playlistToken = randomUUID();
pendingVideoResults.set(playlistToken, playlistPath);
const { dimensions, videoSize } = result;
return new Response(
JSON.stringify({ playlistToken, dimensions, videoSize }),
{ status: 200 },
);
} catch (e) {
await deleteTempFileIgnoringErrors(playlistPath);
throw e;
} finally {
await deleteTempFileIgnoringErrors(videoPath);
}
} finally {
if (isInputFileTemporary)
await deleteTempFileIgnoringErrors(inputFilePath);
}
};
/**
* Upload the file at the given {@link videoFilePath} to the provided presigned
* {@link objectUploadURL} using a HTTP PUT request.
*
* In case on non-HTTP-4xx errors, retry up to 3 times with exponential backoff.
*
* See: [Note: Upload HLS video segment from node side].
*
* ---
*
* This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOkOr4xx`
* from `web/packages/base/http.ts` (we don't have the rest of the scaffolding
* used by that function, which is why it is inlined bespoked).
*
* It handles the specific use case of uploading videos since generating the HLS
* stream is a fairly expensive operation, so a retry to discount transient
* network issues is called for. There are only 2 retries for a total of 3
* attempts, and the retry gaps are more spaced out.
*/
export const uploadVideoSegments = async (
videoFilePath: string,
objectUploadURL: string,
) => {
const waitTimeBeforeNextTry = [5000, 20000];
while (true) {
let abort = false;
try {
const nodeStream = fs_.createReadStream(videoFilePath);
const webStream = Readable.toWeb(nodeStream);
const res = await net.fetch(objectUploadURL, {
method: "PUT",
// The duplex option is required since we're passing a stream.
//
// @ts-expect-error TypeScript's libdom.d.ts does not include
// the "duplex" parameter, e.g. see
// https://github.com/node-fetch/node-fetch/issues/1769.
duplex: "half",
body: webStream,
});
if (res.ok) {
// Success.
return;
}
if (res.status >= 400 && res.status < 500) {
// HTTP 4xx.
abort = true;
}
throw new Error(
`Failed to upload generated HLS video: HTTP ${res.status} ${res.statusText}`,
);
} catch (e) {
if (abort) {
throw e;
}
const t = waitTimeBeforeNextTry.shift();
if (!t) {
throw e;
} else {
log.warn("Will retry potentially transient request failure", e);
}
await wait(t);
}
}
};

View File

@@ -12,7 +12,7 @@
*
* yields (on POSIX shells):
*
* curl -v -H 'Location;' -H 'User-Agent: FoorBar'"'"'s so-called "Browser"' 'http://www.daveeddy.com/?name=dave&age=24'
* curl -v -H 'Location;' -H 'User-Agent: FooBar'"'"'s so-called "Browser"' 'http://www.daveeddy.com/?name=dave&age=24'
*
* or (on Windows):
*

View File

@@ -5,20 +5,13 @@
* currently a common package that both of them share.
*/
/**
* Throw an exception if the given value is `null` or `undefined`.
*/
export const ensure = <T>(v: T | null | undefined): T => {
if (v === null) throw new Error("Required value was null");
if (v === undefined) throw new Error("Required value was not found");
return v;
};
/**
* Wait for {@link ms} milliseconds
*
* This function is a promisified `setTimeout`. It returns a promise that
* resolves after {@link ms} milliseconds.
*
* Duplicated from `web/packages/utils/promise.ts`.
*/
export const wait = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));

View File

@@ -27,7 +27,7 @@ const writeNodeStream = async (filePath: string, fileStream: Readable) => {
fileStream.pipe(writeable);
await new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
writeable.on("finish", resolve);
writeable.on("error", (err) => {
if (existsSync(filePath)) {

View File

@@ -5,7 +5,7 @@ import path from "node:path";
import type { ZipItem } from "../../types/ipc";
import log from "../log";
import { markClosableZip, openZip } from "../services/zip";
import { ensure } from "./common";
import { writeStream } from "./stream";
/**
* Our very own directory within the system temp directory. Go crazy, but
@@ -20,17 +20,21 @@ const enteTempDirPath = async () => {
/** Generate a random string suitable for being used as a file name prefix */
const randomPrefix = () => {
const ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const randomChar = () => ensure(ch[Math.floor(Math.random() * ch.length)]);
const randomChar = () => ch[Math.floor(Math.random() * ch.length)]!;
return Array(10).fill("").map(randomChar).join("");
};
/**
* Return the path to a temporary file with the given {@link suffix}.
* Return the path to a temporary file with an optional {@link extension}.
*
* The function returns the path to a file in the system temp directory (in an
* Ente specific folder therin) with a random prefix and an (optional)
* {@link extension}.
* {@link extension}. The parent directory is guaranteed to exist.
*
* @param extension A string, if provided, is used as the extension for the
* generated path. It will be automatically prefixed by a dot, so don't include
* the dot in the provided string.
*
* It ensures that there is no existing item with the same name already.
*
@@ -64,7 +68,7 @@ export const deleteTempFile = async (tempFilePath: string) => {
};
/**
* A variant of {@link deleteTempFile} that supresses any errors, making it
* A variant of {@link deleteTempFile} that suppresses any errors, making it
* safe to call them in a sequence without needing to handle the scenario where
* one of them failing causes the rest to be skipped.
*/
@@ -76,7 +80,7 @@ export const deleteTempFileIgnoringErrors = async (tempFilePath: string) => {
}
};
/** The result of {@link makeFileForDataOrPathOrZipItem}. */
/** The result of {@link makeFileForDataOrStreamOrPathOrZipItem}. */
interface FileForDataOrPathOrZipItem {
/**
* The path to the file (possibly temporary).
@@ -101,14 +105,14 @@ interface FileForDataOrPathOrZipItem {
/**
* Return the path to a file, a boolean indicating if this is a temporary path
* that needs to be deleted after processing, and a function to write the given
* {@link dataOrPathOrZipItem} into that temporary file if needed.
* {@link item} into that temporary file if needed.
*
* @param dataOrPathOrZipItem The contents of the file, or the path to an
* existing file, or a (path to a zip file, name of an entry within that zip
* file) tuple.
* @param item The contents of the file (bytes), or a {@link ReadableStream}
* with the contents of the file, or the path to an existing file, or a (path to
* a zip file, name of an entry within that zip file) tuple.
*/
export const makeFileForDataOrPathOrZipItem = async (
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
export const makeFileForDataOrStreamOrPathOrZipItem = async (
item: Uint8Array | ReadableStream | string | ZipItem,
): Promise<FileForDataOrPathOrZipItem> => {
let path: string;
let isFileTemporary: boolean;
@@ -116,18 +120,19 @@ export const makeFileForDataOrPathOrZipItem = async (
/* no-op */
};
if (typeof dataOrPathOrZipItem == "string") {
path = dataOrPathOrZipItem;
if (typeof item == "string") {
path = item;
isFileTemporary = false;
} else {
path = await makeTempFilePath();
isFileTemporary = true;
if (dataOrPathOrZipItem instanceof Uint8Array) {
writeToTemporaryFile = () =>
fs.writeFile(path, dataOrPathOrZipItem);
if (item instanceof Uint8Array) {
writeToTemporaryFile = () => fs.writeFile(path, item);
} else if (item instanceof ReadableStream) {
writeToTemporaryFile = () => writeStream(path, item);
} else {
writeToTemporaryFile = async () => {
const [zipPath, entryName] = dataOrPathOrZipItem;
const [zipPath, entryName] = item;
const zip = openZip(zipPath);
try {
await zip.extract(entryName, path);

View File

@@ -66,6 +66,7 @@ import type { IpcRendererEvent } from "electron";
import type {
AppUpdate,
CollectionMapping,
FFmpegCommand,
FolderWatch,
PendingUploads,
ZipItem,
@@ -183,6 +184,8 @@ const fsWriteFileViaBackup = (path: string, contents: string) =>
const fsIsDir = (dirPath: string) => ipcRenderer.invoke("fsIsDir", dirPath);
const fsStatMtime = (path: string) => ipcRenderer.invoke("fsStatMtime", path);
// - Conversion
const convertToJPEG = (imageData: Uint8Array) =>
@@ -201,7 +204,7 @@ const generateImageThumbnail = (
);
const ffmpegExec = (
command: string[],
command: FFmpegCommand,
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
outputFileExtension: string,
) =>
@@ -289,11 +292,11 @@ const pendingUploads = () => ipcRenderer.invoke("pendingUploads");
const setPendingUploads = (pendingUploads: PendingUploads) =>
ipcRenderer.invoke("setPendingUploads", pendingUploads);
const markUploadedFiles = (paths: PendingUploads["filePaths"]) =>
ipcRenderer.invoke("markUploadedFiles", paths);
const markUploadedFile = (path: string, associatedPath?: string) =>
ipcRenderer.invoke("markUploadedFile", path, associatedPath);
const markUploadedZipItems = (items: PendingUploads["zipItems"]) =>
ipcRenderer.invoke("markUploadedZipItems", items);
const markUploadedZipItem = (item: ZipItem, associatedItem?: ZipItem) =>
ipcRenderer.invoke("markUploadedZipItem", item, associatedItem);
const clearPendingUploads = () => ipcRenderer.invoke("clearPendingUploads");
@@ -335,7 +338,7 @@ const clearPendingUploads = () => ipcRenderer.invoke("clearPendingUploads");
* >
* > https://www.electronjs.org/docs/latest/api/context-bridge#methods
*
* The copy itself is relatively fast, but the problem with transfering large
* The copy itself is relatively fast, but the problem with transferring large
* amounts of data is potentially running out of memory during the copy.
*
* For an alternative, see [Note: IPC streams].
@@ -378,6 +381,7 @@ contextBridge.exposeInMainWorld("electron", {
writeFile: fsWriteFile,
writeFileViaBackup: fsWriteFileViaBackup,
isDir: fsIsDir,
statMtime: fsStatMtime,
findFiles: fsFindFiles,
},
@@ -410,7 +414,7 @@ contextBridge.exposeInMainWorld("electron", {
pathOrZipItemSize,
pendingUploads,
setPendingUploads,
markUploadedFiles,
markUploadedZipItems,
markUploadedFile,
markUploadedZipItem,
clearPendingUploads,
});

View File

@@ -32,3 +32,5 @@ export interface PendingUploads {
filePaths: string[];
zipItems: ZipItem[];
}
export type FFmpegCommand = string[] | { default: string[]; hdr: string[] };

View File

@@ -7,10 +7,10 @@
* Recommended target, lib and other settings for code running in the
* version of Node.js bundled with Electron.
*
* Currently, with Electron 30, this is Node.js 20.11.1.
* https://www.electronjs.org/blog/electron-30-0
* Currently, with Electron 35, this is Node.js 22.14.0.
* https://www.electronjs.org/blog/electron-35-0
*/
"extends": "@tsconfig/node20/tsconfig.json",
"extends": "@tsconfig/node22/tsconfig.json",
/* TSConfig docs: https://aka.ms/tsconfig.json */
"compilerOptions": {

View File

@@ -25,19 +25,19 @@
ajv "^6.12.0"
ajv-keywords "^3.4.1"
"@electron/asar@3.2.18":
version "3.2.18"
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.18.tgz#fa607f829209bab8b9e0ce6658d3fe81b2cba517"
integrity sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==
"@electron/asar@3.4.1":
version "3.4.1"
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.4.1.tgz#4e9196a4b54fba18c56cd8d5cac67c5bdc588065"
integrity sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==
dependencies:
commander "^5.0.0"
glob "^7.1.6"
minimatch "^3.0.4"
"@electron/asar@^3.2.7":
version "3.2.10"
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.10.tgz#615cf346b734b23cafa4e0603551010bd0e50aa8"
integrity sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw==
version "3.2.18"
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.18.tgz#fa607f829209bab8b9e0ce6658d3fe81b2cba517"
integrity sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==
dependencies:
commander "^5.0.0"
glob "^7.1.6"
@@ -103,10 +103,10 @@
minimist "^1.2.6"
plist "^3.0.5"
"@electron/rebuild@3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.7.0.tgz#82e20c467ddedbb295d7f641592c52e68c141e9f"
integrity sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==
"@electron/rebuild@3.7.2":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.7.2.tgz#8d808b29159c50086d27a5dec72b40bf16b4b582"
integrity sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==
dependencies:
"@electron/node-gyp" "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2"
"@malept/cross-spawn-promise" "^2.0.0"
@@ -177,10 +177,10 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.1.tgz#4a97e85e982099d6c7ee8410aacb55adaa576f06"
integrity sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==
"@eslint/js@^9.22.0":
version "9.22.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.22.0.tgz#4ff53649ded7cbce90b444b494c234137fa1aa3d"
integrity sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==
"@eslint/js@^9.25.1":
version "9.25.1"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.25.1.tgz#25f5c930c2b68b5ebe7ac857f754cbd61ef6d117"
integrity sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==
"@eslint/object-schema@^2.1.4":
version "2.1.4"
@@ -290,10 +290,10 @@
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
"@tsconfig/node20@^20.1.4":
version "20.1.4"
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928"
integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==
"@tsconfig/node22@^22.0.1":
version "22.0.1"
resolved "https://registry.yarnpkg.com/@tsconfig/node22/-/node22-22.0.1.tgz#27e3ee9b359e31e5b94690bf2bad5a923c1d57d0"
integrity sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg==
"@types/auto-launch@^5.0.5":
version "5.0.5"
@@ -346,25 +346,18 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
"@types/node@*":
version "22.5.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.4.tgz#83f7d1f65bc2ed223bdbf57c7884f1d5a4fa84e8"
integrity sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==
"@types/node@*", "@types/node@^22.7.7":
version "22.14.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.0.tgz#d3bfa3936fef0dbacd79ea3eb17d521c628bb47e"
integrity sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==
dependencies:
undici-types "~6.19.2"
undici-types "~6.21.0"
"@types/node@^10.0.3":
version "10.17.60"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
"@types/node@^20.9.0":
version "20.16.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.16.5.tgz#d43c7f973b32ffdf9aa7bd4f80e1072310fd7a53"
integrity sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==
dependencies:
undici-types "~6.19.2"
"@types/plist@^3.0.1":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.5.tgz#9a0c49c0f9886c8c8696a7904dd703f6284036e0"
@@ -392,62 +385,62 @@
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@8.26.1":
version "8.26.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz#3e48eb847924161843b092c87a9b65176b53782f"
integrity sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==
"@typescript-eslint/eslint-plugin@8.31.1":
version "8.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz#62f1befe59647524994e89de4516d8dcba7a850a"
integrity sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.26.1"
"@typescript-eslint/type-utils" "8.26.1"
"@typescript-eslint/utils" "8.26.1"
"@typescript-eslint/visitor-keys" "8.26.1"
"@typescript-eslint/scope-manager" "8.31.1"
"@typescript-eslint/type-utils" "8.31.1"
"@typescript-eslint/utils" "8.31.1"
"@typescript-eslint/visitor-keys" "8.31.1"
graphemer "^1.4.0"
ignore "^5.3.1"
natural-compare "^1.4.0"
ts-api-utils "^2.0.1"
"@typescript-eslint/parser@8.26.1":
version "8.26.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.1.tgz#0e2f915a497519fc43f52cf2ecbfa607ff56f72e"
integrity sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==
"@typescript-eslint/parser@8.31.1":
version "8.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.31.1.tgz#e9b0ccf30d37dde724ee4d15f4dbc195995cce1b"
integrity sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==
dependencies:
"@typescript-eslint/scope-manager" "8.26.1"
"@typescript-eslint/types" "8.26.1"
"@typescript-eslint/typescript-estree" "8.26.1"
"@typescript-eslint/visitor-keys" "8.26.1"
"@typescript-eslint/scope-manager" "8.31.1"
"@typescript-eslint/types" "8.31.1"
"@typescript-eslint/typescript-estree" "8.31.1"
"@typescript-eslint/visitor-keys" "8.31.1"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.26.1":
version "8.26.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz#5e6ad0ac258ccf79462e91c3f43a3f1f7f31a6cc"
integrity sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==
"@typescript-eslint/scope-manager@8.31.1":
version "8.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz#1eb52e76878f545e4add142e0d8e3e97e7aa443b"
integrity sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==
dependencies:
"@typescript-eslint/types" "8.26.1"
"@typescript-eslint/visitor-keys" "8.26.1"
"@typescript-eslint/types" "8.31.1"
"@typescript-eslint/visitor-keys" "8.31.1"
"@typescript-eslint/type-utils@8.26.1":
version "8.26.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz#462f0bae09de72ac6e8e1af2ebe588c23224d7f8"
integrity sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==
"@typescript-eslint/type-utils@8.31.1":
version "8.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz#be0f438fb24b03568e282a0aed85f776409f970c"
integrity sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==
dependencies:
"@typescript-eslint/typescript-estree" "8.26.1"
"@typescript-eslint/utils" "8.26.1"
"@typescript-eslint/typescript-estree" "8.31.1"
"@typescript-eslint/utils" "8.31.1"
debug "^4.3.4"
ts-api-utils "^2.0.1"
"@typescript-eslint/types@8.26.1":
version "8.26.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.26.1.tgz#d5978721670cff263348d5062773389231a64132"
integrity sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==
"@typescript-eslint/types@8.31.1":
version "8.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.31.1.tgz#478ed6f7e8aee1be7b63a60212b6bffe1423b5d4"
integrity sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==
"@typescript-eslint/typescript-estree@8.26.1":
version "8.26.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz#eb0e4ce31753683d83be53441a409fd5f0b34afd"
integrity sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==
"@typescript-eslint/typescript-estree@8.31.1":
version "8.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz#37792fe7ef4d3021c7580067c8f1ae66daabacdf"
integrity sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==
dependencies:
"@typescript-eslint/types" "8.26.1"
"@typescript-eslint/visitor-keys" "8.26.1"
"@typescript-eslint/types" "8.31.1"
"@typescript-eslint/visitor-keys" "8.31.1"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -455,22 +448,22 @@
semver "^7.6.0"
ts-api-utils "^2.0.1"
"@typescript-eslint/utils@8.26.1":
version "8.26.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.26.1.tgz#54cc58469955f25577f659753b71a0e117a0539f"
integrity sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==
"@typescript-eslint/utils@8.31.1":
version "8.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.31.1.tgz#5628ea0393598a0b2f143d0fc6d019f0dee9dd14"
integrity sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@typescript-eslint/scope-manager" "8.26.1"
"@typescript-eslint/types" "8.26.1"
"@typescript-eslint/typescript-estree" "8.26.1"
"@typescript-eslint/scope-manager" "8.31.1"
"@typescript-eslint/types" "8.31.1"
"@typescript-eslint/typescript-estree" "8.31.1"
"@typescript-eslint/visitor-keys@8.26.1":
version "8.26.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz#c5267fcc82795cf10280363023837deacad2647c"
integrity sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==
"@typescript-eslint/visitor-keys@8.31.1":
version "8.31.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz#6742b0e3ba1e0c1e35bdaf78c03e759eb8dd8e75"
integrity sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==
dependencies:
"@typescript-eslint/types" "8.26.1"
"@typescript-eslint/types" "8.31.1"
eslint-visitor-keys "^4.2.0"
"@xmldom/xmldom@^0.8.8":
@@ -571,44 +564,35 @@ any-shell-escape@^0.1.1:
resolved "https://registry.yarnpkg.com/any-shell-escape/-/any-shell-escape-0.1.1.tgz#d55ab972244c71a9a5e1ab0879f30bf110806959"
integrity sha512-36j4l5HVkboyRhIWgtMh1I9i8LTdFqVwDEHy1cp+QioJyKgAUG40X0W8s7jakWRta/Sjvm8mUG1fU6Tj8mWagQ==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
app-builder-bin@5.0.0-alpha.12:
version "5.0.0-alpha.12"
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz#2daf82f8badc698e0adcc95ba36af4ff0650dc80"
integrity sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==
app-builder-lib@26.0.0:
version "26.0.0"
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.0.0.tgz#726b8b2c058f04f854aba70c7245d48454905980"
integrity sha512-vZTt6Nc401IHBHISqspcO9tUF80ddOP5ehh2B4goLefM+zdT75CvuQUuqz7yzRgW16pHLqCvKMws0FbJQfLB5w==
app-builder-lib@26.0.14:
version "26.0.14"
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.0.14.tgz#a28fbefb600cf052d1259932f32289e043573f61"
integrity sha512-nc/A9MUd95MCc7bR4yVW7Lhs9FZTA/l8QdV8PE1vZZOOiogK4dupBfCCJG0UqLU81JS62f078/bwAeuMjt3hfQ==
dependencies:
"@develar/schema-utils" "~2.6.5"
"@electron/asar" "3.2.18"
"@electron/asar" "3.4.1"
"@electron/fuses" "^1.8.0"
"@electron/notarize" "2.5.0"
"@electron/osx-sign" "1.3.1"
"@electron/rebuild" "3.7.0"
"@electron/rebuild" "3.7.2"
"@electron/universal" "2.0.1"
"@malept/flatpak-bundler" "^0.4.0"
"@types/fs-extra" "9.0.13"
async-exit-hook "^2.0.1"
bluebird-lst "^1.0.9"
builder-util "26.0.0"
builder-util-runtime "9.3.0"
builder-util "26.0.13"
builder-util-runtime "9.3.2"
chromium-pickle-js "^0.2.0"
config-file-ts "0.2.8-rc1"
debug "^4.3.4"
dotenv "^16.4.5"
dotenv-expand "^11.0.6"
ejs "^3.1.8"
electron-publish "26.0.0"
electron-publish "26.0.13"
fs-extra "^10.1.0"
hosted-git-info "^4.1.0"
is-ci "^3.0.0"
@@ -617,10 +601,12 @@ app-builder-lib@26.0.0:
json5 "^2.2.3"
lazy-val "^1.0.5"
minimatch "^10.0.0"
plist "3.1.0"
resedit "^1.7.0"
semver "^7.3.8"
tar "^6.1.12"
temp-file "^3.4.0"
tiny-async-pool "1.3.0"
applescript@^1.0.0:
version "1.0.0"
@@ -688,11 +674,6 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
bl@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
@@ -702,18 +683,6 @@ bl@^4.1.0:
inherits "^2.0.4"
readable-stream "^3.4.0"
bluebird-lst@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz#a64a0e4365658b9ab5fe875eb9dfb694189bb41c"
integrity sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==
dependencies:
bluebird "^3.5.5"
bluebird@^3.5.5:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
boolean@^3.0.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b"
@@ -734,7 +703,7 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
braces@^3.0.3, braces@~3.0.2:
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
@@ -759,24 +728,23 @@ buffer@^5.1.0, buffer@^5.5.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
builder-util-runtime@9.3.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.3.0.tgz#58d97c279bb8626a10d779e1cf22fea0eb25f5e8"
integrity sha512-wR81YIybr41JITLSltwtTsZXkgTwcpBol7LGOyB5A8fKCcZaYLDWgUqDwmsjhlgADD6sGD5ieyzS/5wbL2l/qQ==
builder-util-runtime@9.3.2:
version "9.3.2"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.3.2.tgz#2a69a239b50e26accf4ed4ea1730406a3117213c"
integrity sha512-7QDXJ1FwT6d9ZhG4kuObUUPY8/ENBS/Ky26O4hR5vbeoRGavgekS2Jxv+8sCn/v23aPGU2DXRWEeJuijN2ooYA==
dependencies:
debug "^4.3.4"
sax "^1.2.4"
builder-util@26.0.0:
version "26.0.0"
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.0.0.tgz#e5bc122f3e9e94fa2e3afd2e837e81ce9f177359"
integrity sha512-iLN4R0UAzSz4MxPmz+6vnXMqaY5BMd2FSNlM9f7eHSRYqZrsoTHCSrd7W4Kr4qFIdUxLlRz3X2npATapXzomIg==
builder-util@26.0.13:
version "26.0.13"
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.0.13.tgz#a2c11f8e89e5392719e540d610d70d8413943d74"
integrity sha512-6b64uHzywaL2KAG+rVcqk/Prta1m3I2Jo1d4d2CrApb6EeSk2V384tmSL0EniH+P8jaNbMp6qhg7cIALw32zRA==
dependencies:
"7zip-bin" "~5.2.0"
"@types/debug" "^4.1.6"
app-builder-bin "5.0.0-alpha.12"
bluebird-lst "^1.0.9"
builder-util-runtime "9.3.0"
builder-util-runtime "9.3.2"
chalk "^4.1.2"
cross-spawn "^7.0.6"
debug "^4.3.4"
@@ -789,6 +757,7 @@ builder-util@26.0.0:
source-map-support "^0.5.19"
stat-mode "^1.0.0"
temp-file "^3.4.0"
tiny-async-pool "1.3.0"
cacache@^16.1.0:
version "16.1.3"
@@ -850,20 +819,12 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chokidar@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
chokidar@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
readdirp "^4.0.1"
chownr@^2.0.0:
version "2.0.0"
@@ -1046,16 +1007,7 @@ cross-env@^7.0.3:
dependencies:
cross-spawn "^7.0.1"
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
cross-spawn@^7.0.6:
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -1153,14 +1105,14 @@ dir-compare@^4.2.0:
minimatch "^3.0.5"
p-limit "^3.1.0 "
dmg-builder@26.0.0:
version "26.0.0"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.0.0.tgz#6dc81be31f7ffe9f8a5fc77ce7f83eba33ae9aa7"
integrity sha512-NyyTgm1U56ytpFvuGjj63PYug3v+oYYbPc8e08EiHP6G4TvMwYabmSzFtanfoO1iI7xOljpW/fW7SNCHHElsfw==
dmg-builder@26.0.14:
version "26.0.14"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.0.14.tgz#ce8180da319cf3ee05d42cd460a7509207ad474b"
integrity sha512-0l7oEj175hee7NfnaUpb0zf7fsgh1SyHeLjDA0AtOMnBUfTGxPPwrifbUxfd73qzamrGNcyeqza+m/EJx3QUug==
dependencies:
app-builder-lib "26.0.0"
builder-util "26.0.0"
builder-util-runtime "9.3.0"
app-builder-lib "26.0.14"
builder-util "26.0.13"
builder-util-runtime "9.3.2"
fs-extra "^10.1.0"
iconv-lite "^0.6.2"
js-yaml "^4.1.0"
@@ -1207,35 +1159,35 @@ ejs@^3.1.8:
dependencies:
jake "^10.8.5"
electron-builder@^26.0.0:
version "26.0.0"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.0.0.tgz#f5c03b049e3e9b0d3da7737f93bf1a830d1f17bb"
integrity sha512-R1ZNTCtwjApiELkE4LTNIF6UkV+FMgyDIjILlsVvFak8Jr60cI9gu4q3lVALYzGBcFyzHKn2RPp6j0gl5kuVow==
electron-builder@^26.0.14:
version "26.0.14"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.0.14.tgz#8927c6da42a69425d15e08f351e944ea0e7866da"
integrity sha512-YBxpWLMGj0oS7fbS3LVingeZqFunU0F8s+uB9QTd5+wN4qgrf/rSGRkqoImbWg2+F2yHq11wmaA/Xr9xzvgQ0w==
dependencies:
app-builder-lib "26.0.0"
builder-util "26.0.0"
builder-util-runtime "9.3.0"
app-builder-lib "26.0.14"
builder-util "26.0.13"
builder-util-runtime "9.3.2"
chalk "^4.1.2"
dmg-builder "26.0.0"
dmg-builder "26.0.14"
fs-extra "^10.1.0"
is-ci "^3.0.0"
lazy-val "^1.0.5"
simple-update-notifier "2.0.0"
yargs "^17.6.2"
electron-log@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.3.2.tgz#76aa0091f9cbf0d304546ca6f271ebb6ad953bf4"
integrity sha512-EFI5MFFEzFJU5gyhJNpKQhfGfrRP9IWzSu0sSxrWXasWKvVAOFgBySafX8W1pbPKa/w8/DDPu2bBBtVZJdDsnw==
electron-log@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.4.0.tgz#3180bf5194b2e2efacb62ec1392f8150faf4de6b"
integrity sha512-AXI5OVppskrWxEAmCxuv8ovX+s2Br39CpCAgkGMNHQtjYT3IiVbSQTncEjFVGPgoH35ZygRm/mvUMBDWwhRxgg==
electron-publish@26.0.0:
version "26.0.0"
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.0.0.tgz#f37b9babe62885c2ee552af2817bf2048b566637"
integrity sha512-0MOeYp1IRDj+jdkWluEVMer8fpc/htwWJZdGQUMbbbhgMnX8AvDdwHuR0UdaPFLDJ076E9YHd2urkeFyLm7PUQ==
electron-publish@26.0.13:
version "26.0.13"
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.0.13.tgz#04340520e6e9de5262fecfa011658cfcc3fc8917"
integrity sha512-O5hfHSwli5cegQ4JS3Dp0dZcheex6UCRE/qYyRQvhB6DhSwojiwTnAGEuQCJXc8K8Zxz2lku5Du3VwYHf8d5Lw==
dependencies:
"@types/fs-extra" "^9.0.11"
builder-util "26.0.0"
builder-util-runtime "9.3.0"
builder-util "26.0.13"
builder-util-runtime "9.3.2"
chalk "^4.1.2"
form-data "^4.0.0"
fs-extra "^10.1.0"
@@ -1250,12 +1202,12 @@ electron-store@^8.2.0:
conf "^10.2.0"
type-fest "^2.17.0"
electron-updater@^6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.4.0.tgz#f413e5d1cb4fadde451eb7daa585ddb6332e2409"
integrity sha512-E2fqL3GrVaXGZm2w95s4kJuPIF633pi5GhEy1/ReOHjDW9h/C0mZ1LXcLq0LsyQ4vyVj9UsdRb4E+Zy/d7RKUw==
electron-updater@^6.6.3:
version "6.6.3"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.6.3.tgz#a1f53671ffbb08a475d495d48f0c0d971e665d5d"
integrity sha512-i448/SwMtqxy5wqAcXScnWjiFxZp+hmWA2jZCmojcdfodEGhi/DWTdRP01mE3lCILb8hmdE28SBaHf1oQW3+kw==
dependencies:
builder-util-runtime "9.3.0"
builder-util-runtime "9.3.2"
fs-extra "^10.1.0"
js-yaml "^4.1.0"
lazy-val "^1.0.5"
@@ -1264,13 +1216,13 @@ electron-updater@^6.4.0:
semver "^7.6.3"
tiny-typed-emitter "^2.1.0"
electron@^34.3.4:
version "34.3.4"
resolved "https://registry.yarnpkg.com/electron/-/electron-34.3.4.tgz#3acbcb5fe1aabfa926dddce23de28da9e6023cc5"
integrity sha512-bIsjBh5EN229K4orJpVfjLb7JEoCYkUVg3tS981E1elEjvhBITqwm+K1j9goEqMIlxqYWgogREYpSZRQ+aLWgQ==
electron@^36.1.0:
version "36.1.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-36.1.0.tgz#9919b77e61cd1400acc6dd24f9db8451fba5f8eb"
integrity sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"
"@types/node" "^22.7.7"
extract-zip "^2.0.1"
emoji-regex@^8.0.0:
@@ -1342,12 +1294,7 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint-visitor-keys@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb"
integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==
eslint-visitor-keys@^4.2.0:
eslint-visitor-keys@^4.0.0, eslint-visitor-keys@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
@@ -1621,11 +1568,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
@@ -1664,7 +1606,7 @@ git-hooks-list@^3.0.0:
resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.1.0.tgz#386dc531dcc17474cf094743ff30987a3d3e70fc"
integrity sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==
glob-parent@^5.1.2, glob-parent@~5.1.2:
glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -1942,13 +1884,6 @@ ip-address@^9.0.5:
jsbn "1.1.0"
sprintf-js "^1.1.3"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-ci@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867"
@@ -1973,7 +1908,7 @@ is-fullwidth-code-point@^3.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@@ -2185,10 +2120,10 @@ lru-cache@^10.2.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lru-cache@^11.0.2:
version "11.0.2"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39"
integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==
lru-cache@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.1.0.tgz#afafb060607108132dbc1cf8ae661afb69486117"
integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==
lru-cache@^6.0.0:
version "6.0.0"
@@ -2459,11 +2394,6 @@ nopt@^6.0.0:
dependencies:
abbrev "^1.0.0"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-url@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
@@ -2633,7 +2563,7 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@@ -2650,7 +2580,7 @@ pkg-up@^3.1.0:
dependencies:
find-up "^3.0.0"
plist@^3.0.4, plist@^3.0.5, plist@^3.1.0:
plist@3.1.0, plist@^3.0.4, plist@^3.0.5, plist@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9"
integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==
@@ -2744,12 +2674,10 @@ readable-stream@^3.0.2, readable-stream@^3.4.0:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
readdirp@^4.0.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
rechoir@^0.6.2:
version "0.6.2"
@@ -2886,6 +2814,11 @@ semver-compare@^1.0.0:
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==
semver@^5.5.0:
version "5.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
semver@^6.2.0:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
@@ -3133,6 +3066,13 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
tiny-async-pool@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz#c013e1b369095e7005db5595f95e646cca6ef8a5"
integrity sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==
dependencies:
semver "^5.5.0"
tiny-typed-emitter@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5"
@@ -3209,29 +3149,24 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript-eslint@^8.26.1:
version "8.26.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.26.1.tgz#d17a638a7543bc535157b83cdf5876513c71493b"
integrity sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==
typescript-eslint@^8.31.1:
version "8.31.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.31.1.tgz#b77ab1e48ced2daab9225ff94bab54391a4af69b"
integrity sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==
dependencies:
"@typescript-eslint/eslint-plugin" "8.26.1"
"@typescript-eslint/parser" "8.26.1"
"@typescript-eslint/utils" "8.26.1"
"@typescript-eslint/eslint-plugin" "8.31.1"
"@typescript-eslint/parser" "8.31.1"
"@typescript-eslint/utils" "8.31.1"
typescript@^5.4.3:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
typescript@^5.4.3, typescript@^5.8.3:
version "5.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
typescript@^5.8.2:
version "5.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
undici-types@~6.19.2:
version "6.19.8"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
undici-types@~6.21.0:
version "6.21.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
unique-filename@^2.0.0:
version "2.0.1"

View File

@@ -182,6 +182,7 @@ export const sidebar = [
text: "Auth",
items: [
{ text: "Introduction", link: "/auth/" },
{ text: "Features", link: "/auth/features/" },
{
text: "FAQ",
collapsed: true,
@@ -223,6 +224,7 @@ export const sidebar = [
},
{
text: "Troubleshooting",
collapsed: true,
items: [
{
text: "Windows login",
@@ -238,58 +240,91 @@ export const sidebar = [
items: [
{ text: "Getting started", link: "/self-hosting/" },
{
text: "System requirements",
link: "/self-hosting/guides/system-requirements",
text: "Connecting to custom server",
link: "/self-hosting/guides/custom-server/",
},
{
text: "Creating accounts",
link: "/self-hosting/creating-accounts",
},
{
text: "Configuring your server",
link: "/self-hosting/museum",
},
{
text: "Configuring S3",
link: "/self-hosting/guides/configuring-s3",
},
{
text: "Reverse proxy",
link: "/self-hosting/reverse-proxy",
},
{
text: "Guides",
collapsed: true,
items: [
{ text: "Introduction", link: "/self-hosting/guides/" },
{
text: "Connect to custom server",
link: "/self-hosting/guides/custom-server/",
},
{
text: "Hosting the web app",
link: "/self-hosting/guides/web-app",
},
{
text: "Configuring S3",
link: "/self-hosting/guides/configuring-s3",
},
{
text: "Hosting Ente with external S3 (Community)",
link: "/self-hosting/guides/external-s3",
},
{
text: "DB migration",
link: "/self-hosting/guides/db-migration",
},
{
text: "Hosting Ente without Docker",
link: "/self-hosting/guides/standalone-ente",
},
{
text: "Ente via Tailscale (Community)",
link: "/self-hosting/guides/Tailscale.md",
},
{
text: "Configure CLI for Self Hosted Instance",
link: "/self-hosting/guides/selfhost-cli",
},
{
text: "Administering your server",
link: "/self-hosting/guides/admin",
},
{
text: "Mobile build",
link: "/self-hosting/guides/mobile-build",
text: "Configuring CLI for your instance",
link: "/self-hosting/guides/selfhost-cli",
},
{
text: "Running Ente from source",
link: "/self-hosting/guides/from-source",
},
{
text: "Running Ente without Docker",
link: "/self-hosting/guides/standalone-ente",
},
],
},
{
text: "Troubleshooting",
collapsed: true,
items: [
{
text: "General",
link: "/self-hosting/troubleshooting/misc",
},
{
text: "Bucket CORS",
link: '/self-hosting/troubleshooting/bucket-cors'
},
{
text: "Uploads",
link: "/self-hosting/troubleshooting/uploads",
},
{
text: "Docker / quickstart",
link: "/self-hosting/troubleshooting/docker",
},
{
text: "Ente CLI secrets",
link: "/self-hosting/troubleshooting/keyring",
},
],
},
{
text: "Community Guides",
collapsed: true,
items :[
{
text: "Ente via Tailscale",
link: "/self-hosting/guides/Tailscale",
},
{
text: "Ente with External S3",
link: "/self-hosting/guides/external-s3",
}
]
},
{
text: "FAQ",
collapsed: true,
items: [
{ text: "General", link: "/self-hosting/faq/" },
{
@@ -306,27 +341,6 @@ export const sidebar = [
},
],
},
{
text: "Troubleshooting",
items: [
{
text: "Uploads",
link: "/self-hosting/troubleshooting/uploads",
},
{
text: "Docker",
link: "/self-hosting/troubleshooting/docker",
},
{
text: "Yarn",
link: "/self-hosting/troubleshooting/yarn",
},
{
text: "Ente CLI Secrets",
link: "/self-hosting/troubleshooting/keyring",
},
],
},
],
},
{

View File

@@ -0,0 +1,139 @@
---
title: Features - Auth
description: Features available in Ente Auth
---
# Features
This page outlines the key features available in Ente Auth.
### Icons
Ente Auth supports the icon pack provided by
[simple-icons](https://github.com/simple-icons/simple-icons). If an icon you
need is missing, please refer to the
[docs/adding-icons](https://github.com/ente-io/ente/blob/main/auth/docs/adding-icons.md)
guide for instructions on how to contribute.
### Search
Quickly find your codes by searching based on issuer or account name. You can
also configure the app to focus the search bar automatically on app start by
going to **Settings → General → Focus search on app start**.
### Tags
Organize and filter your codes with ease using tags.
- **Creating a Tag:** When adding or editing a code, tap the orange (+) icon to
create a new tag.
- **Adding an existing Tag:** When adding or editing a code, select the desired
tag from the list.
### Pinning
Highlight your frequently used services by pinning them to the top of your code
list. To pin a code, long-press (mobile) or right-click (desktop) the code and
select "Pin".
### Notes
Add additional information to your codes using notes. Notes can be added during
the process of creating or modifying a code.
### Sharing
Securely share codes temporarily with others.
- Long-press (mobile) or right-click (desktop) on a code and choose "Share".
- Select a duration for the shared link: 2 minutes, 5 minutes, or 10 minutes.
- This generates a unique, time-limited link. Recipients can view the codes for
the specified duration without gaining access to the underlying secret key.
After the link expires, the recipients will no longer be able to view new
codes.
### Custom sorting
Customize the order in which your codes are displayed. Ente Auth provides
several sorting options:
- Issuer name
- Account name
- Frequently used
- Recently used
- Manual (custom drag-and-drop order)
Access the sort menu in the top-right corner (next to the search icon) to change
your sorting preference.
### Offline mode
Ente Auth can be used entirely offline. Choose "Use without backups" on the
login screen. In this mode, your codes are stored locally on your device.
Unlike when using an account, data is not synced or backed up to the cloud. You
are responsible for manually backing up your codes.
### Display options
Customize how your codes are displayed for optimal usability.
- **Show large icons:** Display codes with larger icons for enhanced visibility.
- **Compact mode:** Switch to a more compact layout to view more codes on the
screen simultaneously.
- **Hide codes:** Hide the actual code values for extra privacy. Double-tap a
code to reveal it when needed.
### App lock
Add an additional layer of protection using the app lock. Choose from the
following lock methods:
- **Device lock:** Use the existing lock configured on your device (e.g., Face
ID, Touch ID, system password).
- **PIN lock:** Set up a 4-digit PIN code to unlock the app.
- **Password lock:** Set up a password to unlock the app.
Additionally, configure **Auto lock** to automatically lock the app after a
specified period of time (options: Immediately, 5s, 15s, 1m, 5m, 30m).
### Import / Export
Ente Auth offers various import and export options for your codes.
- **Export:** Export your codes in plain text, as an encrypted file, or
automatically via the CLI.
- **Import:** Import codes from various other authentication apps.
For detailed instructions, refer to the
[migration guides](../migration-guides/).
### Deduplicate codes
If you import codes and end up with duplicates, you can easily remove them. Go
to **Settings → Data → Duplicate codes** to find and remove duplicate codes.
### Trash
Manage unwanted codes by moving them to the Trash. The Trash is not cleared
automatically, giving you the flexibility to restore or permanently delete codes
at any time.
- **Trashing a code:** Long-press (mobile) or right-click (desktop) on a code
and select "Trash" to move it to the Trash.
- **Viewing trashed codes:** If you have trashed codes, you can view them by
selecting the Trash tag.
- **Managing trashed codes:** In the Trash view, you can either permanently
delete codes or restore them back to your main list.
### Scan QR
Easily add or share entries using QR codes:
- **Add by scanning (mobile):** On mobile, you can add a new entry by scanning
the QR code provided by the service. This quickly adds the entry to Ente Auth.
- **Show entry as QR code:** On all apps, you can long-press (mobile) or
right-click (desktop) a code and select "QR". This allows you to easily share
the complete entry (including the secret) with others by letting them scan the
displayed QR code. This can also be used to easily add the same entry to
another authenticatior app or service.

View File

@@ -10,8 +10,8 @@ A guide written by Green, an ente.io lover
> [!WARNING]
>
> Authy has dropped all support for its desktop apps. It is no longer possible
> to export data from Authy using methods 1 and 2. You will either need a rooted
> android phone or you will need to reconfigure 2FA for each of your accounts.
> to export data from Authy using methods 1 and 2. You will need either an iOS device
> and computer (method 4) or a rooted Android phone (method 3) to follow this guide.
---
@@ -202,7 +202,15 @@ This uses the tool [Aegis Authenticator](https://getaegis.app/) from
6. Then export the codes from Aegis Authenticator to `json` or `txt` using the
"Export to file" option in the "Import & Export" menu.
## Importing to Ente Authenticator (Method 1, method 2.1)
## Method 4: Authy-iOS-MiTM
**Who should use this?** Technical iOS users of Authy that cannot export their tokens with methods 1 or 2 (due to those methods being patched) or method 3 (due to that method requiring a rooted Android device).
This method works by intercepting the data the Authy app receives while logging in for the first time, which contains your encrypted authenticator tokens. After the encrypted authenticator tokens are dumped, you can decrypt them using your backup password and convert them to an Ente token file.
For an up-to-date guide of how to retrieve the encrypted authenticator tokens and decrypt them, please see [Authy-iOS-MiTM](https://github.com/AlexTech01/Authy-iOS-MiTM). To convert the `decrypted_tokens.json` file from that guide into a format Ente Authenticator can recognize, use [this](https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93?permalink_comment_id=5317087#gistcomment-5317087) Python script. Once you have the `ente_auth_import.plain` file from that script, transfer it to your device and follow the instructions below to import it into Ente Authenticator.
## Importing to Ente Authenticator (Method 1, method 2.1, method 4)
1. Copy the TXT file to one of your devices with Ente Authenticator.
2. Log in to your account (if you haven't already), or press "Use without

View File

@@ -7,10 +7,9 @@ description: Frequently asked questions about keeping extra backups of your data
## How can I backup my data in a local drive outside Ente?
Yes! You can use our CLI tool or our desktop app to set up exports of your data
to your local drive. This way, you can use Ente in your day to day use, but will
have an additional guarantee that a copy of your original photos and videos are
always available in normal directories and files.
You can use our CLI tool or our desktop app to set up exports of your data
to your local drive. This way, you can use Ente in your day to day use, with an additional guarantee that a copy of your original photos and videos are
always available on your machine.
- You can use [Ente's CLI](https://github.com/ente-io/ente/tree/main/cli#export)
to export your data in a cron job to a location of your choice. The exports

View File

@@ -24,7 +24,7 @@ In brief,
## Storage Limits
If you're an admin of a family, you will be able to set storage limits for the
If you're an admin of a family, you will be able to set storage limits for the
members in your family plan.
In brief,
@@ -38,15 +38,5 @@ In brief,
- If the admin has set a limit for any user, that limit value will be prefilled
in the input box.
- Incase, if you want to remove any storage limit from a members account, you
- If you want to remove any storage limit from a members account, you
can click on the "Remove Limit" and they can upload photos without any limit.
## FAQ
- **Can you assign a storage quota for each individual member in the family
plan?**
Unfortunately, at this moment, assigning a storage quota for each individual
member in the family plan is not supported. For updates on this feature
request, please follow
[this thread](https://github.com/ente-io/ente/discussions/857).

View File

@@ -6,7 +6,7 @@ description: Deleting items and trash
# Trash
Whenever you delete an item from Ente, it is moved to Trash. These items will be
automatically deleted from Trash after 30 days. You can manaully select photos
automatically deleted from Trash after 30 days. You can manually select photos
to permanently delete or completely empty the trash if you wish.
Items in trash are included in your used storage calculation.

View File

@@ -14,9 +14,21 @@ directly stream chunks of Google Takeout zips that are stored on network drives.
In particular, the folder watch functionality suffers a lot since the app needs
access to file system events to detect changes to the users files so that they
can be uploaded whenever there are changes.
can be uploaded whenever there are changes. Network drives are less reliable in
providing these file change events correctly.
Since are high chances of the user having a subpar experience, we request
customers to avoid using the desktop app directly with network attached storage
and instead temporarily copy the files to their local storage for uploads, and
avoid watching folders that live on a network drive.
## Exporting to UNC paths
Generally, exports are likely to work better than imports, since the interaction
with the file system is relatively simpler (Note that the app still needs to
scan the folder to find existing files, esp. if the continuous export option is
enabled).
A special case is when exporting to a UNC path. In this case, the file
separators will not work as expected and the export will not start. As a
workaround, you can map your UNC path to a network drive and use that instead.

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Some files were not shown because too many files have changed in this diff Show More