Merge remote-tracking branch 'origin/flutter-upgrade' into isolated-ffmpeg
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.24.3"
|
||||
FLUTTER_VERSION: "3.32.5"
|
||||
RUST_VERSION: "1.85.1"
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.24.3"
|
||||
FLUTTER_VERSION: "3.32.5"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
2
.github/workflows/mobile-lint.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
- ".github/workflows/mobile-lint.yml"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.24.3"
|
||||
FLUTTER_VERSION: "3.32.5"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/mobile-release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
- "photos-v*"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.24.3"
|
||||
FLUTTER_VERSION: "3.32.5"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -1038,6 +1038,30 @@
|
||||
{
|
||||
"title": "Proton"
|
||||
},
|
||||
{
|
||||
"title": "Proton Calendar",
|
||||
"slug": "proton_calendar"
|
||||
},
|
||||
{
|
||||
"title": "Proton Drive",
|
||||
"slug": "proton_drive"
|
||||
},
|
||||
{
|
||||
"title": "Proton Mail",
|
||||
"slug": "proton_mail"
|
||||
},
|
||||
{
|
||||
"title": "Proton Pass",
|
||||
"slug": "proton_pass"
|
||||
},
|
||||
{
|
||||
"title": "Proton VPN",
|
||||
"slug": "proton_vpn"
|
||||
},
|
||||
{
|
||||
"title": "Proton Wallet",
|
||||
"slug": "proton_wallet"
|
||||
},
|
||||
{
|
||||
"title": "Proxmox"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<svg width="918" height="754" viewBox="0 0 918 754" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_11864_192587)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M649.27 113.046H0.226562V659.944C0.226562 711.978 42.7217 754.117 95.1872 754.117H580.638H417.047V710.245C417.047 687.879 425.081 666.184 439.699 649.192L606.969 454.509V454.606C606.871 454.701 606.774 454.893 606.581 454.989C625.941 433.006 654.4 419.182 686.054 419.182H723.032V186.292C723.128 145.877 690.12 113.046 649.27 113.046Z" fill="url(#paint0_radial_11864_192587)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M607.065 454.609C590.608 473.04 580.639 497.327 580.639 523.823L580.638 754.117H417.047V710.245C417.047 687.879 425.081 666.184 439.699 649.192L606.969 454.509L607.065 454.609Z" fill="#B8D7FF"/>
|
||||
<path d="M673.082 634.504H706.478C707.833 640.264 711.221 645.352 716.061 648.903C720.804 652.455 726.709 654.279 732.711 653.991C747.812 653.991 757.782 645.735 757.782 633.64C757.782 621.544 747.425 614.824 726.903 614.824H713.641V587.561H725.161C745.005 587.561 753.135 580.265 753.135 569.418C753.135 558.569 744.327 551.082 732.131 551.082C726.709 550.794 721.289 552.522 717.223 556.074C713.061 559.625 710.544 564.617 710.06 569.994H677.826C678.987 550.986 694.088 523.723 731.937 523.723C762.332 523.723 783.337 540.906 783.337 565.097C783.337 572.873 780.917 580.457 776.271 586.697C771.625 592.937 765.139 597.64 757.685 599.944V600.424C766.397 601.96 774.238 606.568 779.756 613.384C785.273 620.2 788.177 628.743 787.887 637.576C787.887 663.783 763.687 681.063 732.421 681.063C699.219 681.254 675.503 662.535 673.082 634.504Z" fill="#8F69FF"/>
|
||||
<path d="M846.257 526.328H870.552V678.868H839.191V561.848L808.989 582.199V551.383L846.257 526.328Z" fill="#8F69FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M95.1872 0.543945H823.413C875.879 0.543945 918.374 42.6864 918.374 94.7173V419.189H723.128V186.299C723.128 145.788 690.023 113.046 649.27 113.046H0.226562V94.7173C0.226562 42.6864 42.7217 0.543945 95.1872 0.543945Z" fill="url(#paint1_linear_11864_192587)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_11864_192587" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(883.262 -628.134) scale(1874.62 1592.06)">
|
||||
<stop offset="0.5563" stop-color="#6D4AFF"/>
|
||||
<stop offset="0.9944" stop-color="#06B8FF"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_11864_192587" x1="771.551" y1="515.649" x2="203.726" y2="-273.993" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BFE8FF"/>
|
||||
<stop offset="0.3075" stop-color="#BFABFF"/>
|
||||
<stop offset="1" stop-color="#7341FF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_11864_192587">
|
||||
<rect width="918" height="754" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
20
mobile/apps/auth/assets/custom-icons/icons/proton_drive.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="918" height="753" viewBox="0 0 918 753" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_11864_192589)">
|
||||
<path d="M648.843 188.37L289.21 190.386C273.725 190.482 258.628 185.778 245.95 176.851L165.816 120.405C153.332 111.573 138.331 106.869 123.04 106.869H0.226562V658.946C0.226562 710.976 42.7128 753.118 95.1673 753.118H723.169V261.616C723.169 221.009 689.877 188.082 648.843 188.37Z" fill="url(#paint0_radial_11864_192589)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M432.928 75.3823H823.433C875.887 75.3823 918.374 117.525 918.374 169.555V658.85C918.374 710.88 875.887 753.023 823.433 753.023H723.169V261.616C723.169 221.009 689.877 188.082 648.843 188.37L289.21 190.386C273.725 190.482 258.628 185.778 245.95 176.851L165.816 120.405C153.332 111.573 138.331 106.869 123.04 106.869H0.226562V93.7176C0.226562 41.6875 42.7128 -0.455078 95.1673 -0.455078H273.338C291.339 -0.455078 308.856 5.20872 323.277 15.7684L382.893 59.1588C397.41 69.7185 414.927 75.3823 432.928 75.3823Z" fill="url(#paint1_linear_11864_192589)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_11864_192589" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(995.275 -518.146) scale(1713.25 1560.31)">
|
||||
<stop offset="0.5561" stop-color="#6D4AFF"/>
|
||||
<stop offset="1" stop-color="#FF50C3"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_11864_192589" x1="-11.8774" y1="-393.727" x2="1125.4" y2="866.473" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#7341FF"/>
|
||||
<stop offset="0.3593" stop-color="#B487FF"/>
|
||||
<stop offset="1" stop-color="#FFC8FF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_11864_192589">
|
||||
<rect width="918" height="753" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
24
mobile/apps/auth/assets/custom-icons/icons/proton_mail.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg width="918" height="745" viewBox="0 0 918 745" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_11864_192585)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M723.149 140.476V745.456H823.435C875.876 745.456 918.374 702.372 918.374 649.323V21.9472C918.374 3.82154 897.547 -6.04793 883.757 5.52972L723.149 140.476Z" fill="url(#paint0_linear_11864_192585)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M583.274 258.151L400.057 422.041C368.818 449.941 322.193 450.605 290.202 423.654L0.226562 179.574V22.0424C0.226562 3.91675 21.053 -6.04762 34.8435 5.53003L398.65 311.294C433.83 340.902 484.864 340.902 520.044 311.294L583.274 258.151Z" fill="url(#paint1_linear_11864_192585)"/>
|
||||
<path d="M723.149 140.571L583.274 258.151L583.368 258.15L400.057 422.041C368.818 449.941 322.193 450.605 290.202 423.654L0.226562 179.574V649.323C0.226562 702.372 42.7238 745.456 95.1652 745.456L723.149 745.456V140.571Z" fill="url(#paint2_radial_11864_192585)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_11864_192585" x1="2304.04" y1="1228.39" x2="2116.29" y2="-635.265" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.271" stop-color="#E3D9FF"/>
|
||||
<stop offset="1" stop-color="#7341FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_11864_192585" x1="542.753" y1="752.371" x2="160.929" y2="-755.454" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E3D9FF"/>
|
||||
<stop offset="1" stop-color="#7341FF"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint2_radial_11864_192585" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(914.374 91.7284) scale(1070.72 1083.11)">
|
||||
<stop offset="0.5561" stop-color="#6D4AFF"/>
|
||||
<stop offset="0.9944" stop-color="#AA8EFF"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_11864_192585">
|
||||
<rect width="918" height="745" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
67
mobile/apps/auth/assets/custom-icons/icons/proton_pass.svg
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="500px" height="500px" viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{clip-path:url(#SVGID_00000160880367675937928300000013114190071515040655_);}
|
||||
.st1{fill:url(#SVGID_00000158020781677298841590000014386279406831169436_);}
|
||||
.st2{fill:url(#SVGID_00000038389245457700614160000015930090567954731184_);}
|
||||
.st3{fill:url(#SVGID_00000074402669088272121800000007567021010918974869_);}
|
||||
</style>
|
||||
<g>
|
||||
<defs>
|
||||
<rect id="SVGID_1_" width="500" height="500"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_00000097476224578929584770000017210037084984045718_">
|
||||
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
|
||||
</clipPath>
|
||||
<g style="clip-path:url(#SVGID_00000097476224578929584770000017210037084984045718_);">
|
||||
|
||||
<radialGradient id="SVGID_00000047755579261974386440000006872451468367864226_" cx="148.4036" cy="350.2411" r="4.717" gradientTransform="matrix(46.7033 -75.1155 -117.4926 -73.0513 34370.6797 37242.4727)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#FFD580"/>
|
||||
<stop offset="9.375000e-02" style="stop-color:#F6C592"/>
|
||||
<stop offset="0.205" style="stop-color:#EBB6A2"/>
|
||||
<stop offset="0.3245" style="stop-color:#DFA5AF"/>
|
||||
<stop offset="0.4288" style="stop-color:#D397BE"/>
|
||||
<stop offset="0.5337" style="stop-color:#C486CB"/>
|
||||
<stop offset="0.6488" style="stop-color:#B578D9"/>
|
||||
<stop offset="0.7713" style="stop-color:#A166E5"/>
|
||||
<stop offset="0.8913" style="stop-color:#8B57F2"/>
|
||||
<stop offset="1" style="stop-color:#704CFF"/>
|
||||
</radialGradient>
|
||||
<path style="fill:url(#SVGID_00000047755579261974386440000006872451468367864226_);" d="M150.4,63.1
|
||||
c34.9-34.9,52.3-52.3,72.4-58.8c17.7-5.7,36.7-5.7,54.4,0c20.1,6.5,37.5,24,72.4,58.8l87.2,87.1c34.9,34.9,52.3,52.3,58.9,72.4
|
||||
c5.8,17.7,5.8,36.7,0,54.4c-6.5,20.1-24,37.5-58.9,72.4l-87.2,87.1c-34.9,34.9-52.3,52.3-72.4,58.8c-17.7,5.7-36.7,5.7-54.4,0
|
||||
c-20.1-6.5-37.5-24-72.4-58.8L134,418.2c-9.9-11.1-14.9-16.7-18.4-23c-3.1-5.6-5.4-11.6-6.8-17.9c-1.6-7.1-1.6-14.5-1.6-29.4
|
||||
V151.8c0-14.9,0-22.3,1.6-29.4c1.4-6.3,3.7-12.3,6.8-17.9c3.5-6.3,8.5-11.9,18.4-23L150.4,63.1z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000017511077749203986220000003166735930388103090_" gradientUnits="userSpaceOnUse" x1="234.6024" y1="617.7536" x2="331.7387" y2="24.506" gradientTransform="matrix(1 0 0 -1 0 502)">
|
||||
<stop offset="0" style="stop-color:#6D4AFF"/>
|
||||
<stop offset="0.392" style="stop-color:#B39FFB;stop-opacity:0.978"/>
|
||||
<stop offset="1" style="stop-color:#FFE8DB;stop-opacity:0.8"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000017511077749203986220000003166735930388103090_);" d="M150.4,63.1
|
||||
c34.9-34.9,52.3-52.3,72.4-58.8c17.7-5.7,36.7-5.7,54.4,0c20.1,6.5,37.5,24,72.4,58.8l87.2,87.1c34.9,34.9,52.3,52.3,58.9,72.4
|
||||
c5.8,17.7,5.8,36.7,0,54.4c-6.5,20.1-24,37.5-58.9,72.4l-87.2,87.1c-34.9,34.9-52.3,52.3-72.4,58.8c-17.7,5.7-36.7,5.7-54.4,0
|
||||
c-20.1-6.5-37.5-24-72.4-58.8L134,418.2c-9.9-11.1-14.9-16.7-18.4-23c-3.1-5.6-5.4-11.6-6.8-17.9c-1.6-7.1-1.6-14.5-1.6-29.4
|
||||
V151.8c0-14.9,0-22.3,1.6-29.4c1.4-6.3,3.7-12.3,6.8-17.9c3.5-6.3,8.5-11.9,18.4-23L150.4,63.1z"/>
|
||||
|
||||
<radialGradient id="SVGID_00000089555210451034675070000005649055941905357209_" cx="148.0355" cy="350.4669" r="4.717" gradientTransform="matrix(37.5657 -60.419 -94.5046 -58.7585 27673.916 29995.748)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#FFD580"/>
|
||||
<stop offset="9.375000e-02" style="stop-color:#F6C592"/>
|
||||
<stop offset="0.205" style="stop-color:#EBB6A2"/>
|
||||
<stop offset="0.3245" style="stop-color:#DFA5AF"/>
|
||||
<stop offset="0.4288" style="stop-color:#D397BE"/>
|
||||
<stop offset="0.5337" style="stop-color:#C486CB"/>
|
||||
<stop offset="0.6488" style="stop-color:#B578D9"/>
|
||||
<stop offset="0.7713" style="stop-color:#A166E5"/>
|
||||
<stop offset="0.8913" style="stop-color:#8B57F2"/>
|
||||
<stop offset="1" style="stop-color:#704CFF"/>
|
||||
</radialGradient>
|
||||
<path style="fill:url(#SVGID_00000089555210451034675070000005649055941905357209_);" d="M144.1,69.4
|
||||
c17.4-17.4,26.2-26.1,36.2-29.4c8.8-2.9,18.4-2.9,27.2,0c10.1,3.3,18.8,12,36.2,29.4l130.8,130.7c17.4,17.4,26.2,26.1,29.4,36.2
|
||||
c2.9,8.8,2.9,18.4,0,27.2c-3.3,10.1-12,18.8-29.4,36.2L243.8,430.4c-17.4,17.4-26.2,26.1-36.2,29.4c-8.8,2.9-18.4,2.9-27.2,0
|
||||
c-10.1-3.3-18.8-12-36.2-29.4l-81-80.9c-34.9-34.9-52.3-52.3-58.9-72.4c-5.7-17.7-5.7-36.7,0-54.4c6.5-20.1,24-37.5,58.9-72.4
|
||||
L144.1,69.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
25
mobile/apps/auth/assets/custom-icons/icons/proton_vpn.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<svg width="918" height="833" viewBox="0 0 918 833" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_11864_192591)">
|
||||
<g clip-path="url(#clip1_11864_192591)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M369.025 783.591C403.919 846.321 493.528 849.718 533.145 789.813L902.547 231.232C941.721 171.996 904.24 92.5773 833.301 84.5058L107.232 1.89232C29.8225 -6.91551 -25.2938 74.7083 12.3576 142.396L369.025 783.591Z" fill="url(#paint0_linear_11864_192591)"/>
|
||||
<path d="M389.854 725.08L422.638 676.182L671.66 300.021C693.433 267.133 672.652 223.014 633.259 218.491L15.25 147.539L335.396 723.082C347.08 743.681 376.623 744.814 389.854 725.08Z" fill="url(#paint1_linear_11864_192591)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_11864_192591" x1="830.496" y1="783.548" x2="239.715" y2="-235.266" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0660125" stop-color="#8EFFEE"/>
|
||||
<stop offset="0.4499" stop-color="#C9C7FF"/>
|
||||
<stop offset="1" stop-color="#7341FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_11864_192591" x1="884.537" y1="-908.44" x2="57.2415" y2="861.083" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.4799" stop-color="#6D4AFF"/>
|
||||
<stop offset="0.9944" stop-color="#00F0C3"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_11864_192591">
|
||||
<rect width="918" height="833" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_11864_192591">
|
||||
<rect width="918.147" height="831.529" fill="white" transform="translate(0.226562 1.23438)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
24
mobile/apps/auth/assets/custom-icons/icons/proton_wallet.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg width="1683" height="1461" viewBox="0 0 1683 1461" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M333.319 1113.48C261.409 1113.48 203.111 1055.18 203.111 983.274C203.111 911.364 261.409 853.066 333.319 853.066C405.229 853.066 463.537 911.364 463.537 983.274C463.537 1055.18 405.239 1113.48 333.319 1113.48Z" fill="url(#paint0_linear_3766_630)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1682.53 713.397V1307.45C1682.53 1391.84 1614.11 1460.26 1529.72 1460.26H248.509L251.519 1456.79L512.455 1156C513.745 1154.64 515.016 1153.26 516.266 1151.86H516.276C555.741 1107.89 580.005 1048.21 580.005 982.484C580.005 847.346 477.45 737.77 350.973 737.77H-0.00585938V153.63C-0.00585938 69.2378 68.4136 0.818359 152.805 0.818359H969.949C1363.49 0.818359 1682.53 319.853 1682.53 713.397Z" fill="url(#paint1_linear_3766_630)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1327.42 774.325V1460.26H248.509L251.519 1456.79L512.455 1156C513.745 1154.64 515.016 1153.26 516.266 1151.86H516.276C555.741 1107.89 580.005 1048.21 580.005 982.484C580.005 847.346 477.45 737.77 350.973 737.77H-0.00585938V241.191H794.284C1088.73 241.191 1327.42 479.885 1327.42 774.325Z" fill="#6D4AFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1327.42 774.325V1460.26H248.509L251.519 1456.79L512.455 1156C513.745 1154.64 515.016 1153.26 516.266 1151.86H516.276C555.741 1107.89 580.005 1048.21 580.005 982.484C580.005 847.346 477.45 737.77 350.973 737.77H-0.00585938V241.191H794.284C1088.73 241.191 1327.42 479.885 1327.42 774.325Z" fill="url(#paint2_linear_3766_630)" fill-opacity="0.9"/>
|
||||
<path d="M516.046 1151.86L512.455 1156L251.519 1456.79L248.509 1460.26H152.805C68.4136 1460.26 -0.00585938 1391.84 -0.00585938 1307.45V1224.48L297.845 1226.79L350.973 1227.2C352.923 1227.2 354.873 1227.17 356.814 1227.12C358.714 1227.07 360.604 1226.99 362.484 1226.89C362.544 1226.89 362.594 1226.89 362.654 1226.88C363.595 1226.83 364.535 1226.77 365.475 1226.71C424.543 1222.7 477.53 1194.75 516.036 1151.86H516.046Z" fill="#FFBB93"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3766_630" x1="248.534" y1="1075.2" x2="432.469" y2="913.224" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EA777D"/>
|
||||
<stop offset="0.479" stop-color="#DC6E8E"/>
|
||||
<stop offset="0.764" stop-color="#D2699A"/>
|
||||
<stop offset="1" stop-color="#C864A6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_3766_630" x1="-210.798" y1="170.667" x2="1871.67" y2="979.674" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#957AFD"/>
|
||||
<stop offset="1" stop-color="#FFC6C6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_3766_630" x1="1252.32" y1="426.167" x2="619.247" y2="1730.93" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.149916" stop-color="#FA528E" stop-opacity="0"/>
|
||||
<stop offset="0.720842" stop-color="#FF8065"/>
|
||||
<stop offset="1" stop-color="#FFA51F"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,4 +1,206 @@
|
||||
{
|
||||
"account": "Kasutajakonto",
|
||||
"unlock": "Ava",
|
||||
"recoveryKey": "Taastevõti",
|
||||
"counterAppBarTitle": "Loendur",
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
},
|
||||
"onBoardingBody": "Sinu kaheastmelise autentimise koodide turvaline varundus",
|
||||
"onBoardingGetStarted": "Alusta",
|
||||
"setupFirstAccount": "Lisa oma esimene kasutajakonto",
|
||||
"importScanQrCode": "Skanneeri QR-koodi",
|
||||
"qrCode": "QR-kood",
|
||||
"importAccountPageTitle": "Sisesta kasutajakonto üksikasjad",
|
||||
"secretCanNotBeEmpty": "Saladus ei tohi jääda tühjaks",
|
||||
"bothIssuerAndAccountCanNotBeEmpty": "Nii kasutajakonto kui väljaandja ei tohi tühjaks jääda",
|
||||
"incorrectDetails": "Viga üksikasjades",
|
||||
"pleaseVerifyDetails": "Palun kontrolli andmeid ja proovi uuesti",
|
||||
"codeIssuerHint": "Väljaandja",
|
||||
"codeSecretKeyHint": "Salajane võti",
|
||||
"secret": "Saladus",
|
||||
"all": "Kõik",
|
||||
"notes": "Märkmed",
|
||||
"notesLengthLimit": "Märkmete pikkus võib olla kuni {count} tähemärki",
|
||||
"@notesLengthLimit": {
|
||||
"description": "Text to indicate the maximum number of characters allowed for notes",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"description": "The maximum number of characters allowed for notes",
|
||||
"type": "int",
|
||||
"example": "100"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codeTagHint": "Silt",
|
||||
"accountKeyType": "Võtme tüüp",
|
||||
"sessionExpired": "Sessioon on aegunud",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
},
|
||||
"pleaseLoginAgain": "Palun logi uuesti sisse",
|
||||
"loggingOut": "Väljalogimine...",
|
||||
"useRecoveryKey": "Kasuta taastevõtit"
|
||||
"saveAction": "Salvesta",
|
||||
"trash": "Prügikast",
|
||||
"viewLogsAction": "Vaata logisid",
|
||||
"preparingLogsTitle": "Valmistan logisid ette...",
|
||||
"emailLogsTitle": "Logide saatmine e-postiga",
|
||||
"emailLogsMessage": "Palun saada logid e-posti aadressile {email}",
|
||||
"@emailLogsMessage": {
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"copyEmailAction": "Kopeeri e-posti aadress",
|
||||
"exportLogsAction": "Ekspordi logid",
|
||||
"reportABug": "Teata veast",
|
||||
"contactSupport": "Võtke ühendust klienditoega",
|
||||
"blog": "Blogi",
|
||||
"verifyPassword": "Korda salasõna",
|
||||
"pleaseWait": "Palun oota...",
|
||||
"generatingEncryptionKeysTitle": "Loon krüptovõtmeid...",
|
||||
"recreatePassword": "Loo salasõna uuesti",
|
||||
"useRecoveryKey": "Kasuta taastevõtit",
|
||||
"incorrectPasswordTitle": "Vale salasõna",
|
||||
"welcomeBack": "Tere tulemast tagasi!",
|
||||
"emailAlreadyRegistered": "E-posti aadress on juba registreeritud.",
|
||||
"emailNotRegistered": "E-posti aadress pole registreeritud.",
|
||||
"changeEmail": "Muuda e-posti aadressi",
|
||||
"changePassword": "Muuda salasõna",
|
||||
"data": "Andmed",
|
||||
"passwordForDecryptingExport": "Salasõna eksporditud andmete dekrüptimiseks",
|
||||
"passwordEmptyError": "Salasõna väli ei saa olla tühi",
|
||||
"ok": "Sobib",
|
||||
"cancel": "Katkesta",
|
||||
"yes": "Jah",
|
||||
"no": "Ei",
|
||||
"email": "E-post",
|
||||
"support": "Kasutajatugi",
|
||||
"settings": "Seadistused",
|
||||
"copied": "Kopeeritud",
|
||||
"pleaseTryAgain": "Palun proovi uuesti",
|
||||
"existingUser": "Olemasolev kasutaja",
|
||||
"newUser": "Uus kasutaja Ente jaoks",
|
||||
"delete": "Kustuta",
|
||||
"enterYourPasswordHint": "Sisesta oma salasõna",
|
||||
"forgotPassword": "Unustasin salasõna",
|
||||
"oops": "Vaat kus lops!",
|
||||
"faq": "KKK",
|
||||
"somethingWentWrongMessage": "Midagi läks valesti, palun proovi uuesti",
|
||||
"scan": "Skanneeri",
|
||||
"scanACode": "Skanneeri QR-koodi",
|
||||
"verify": "Kinnita",
|
||||
"verifyEmail": "Kinnita e-posti aadress",
|
||||
"enterCodeHint": "Sisesta oma autentimisrakendusest\n6-numbriline kood",
|
||||
"lostDeviceTitle": "Kas kaotasid oma seadme?",
|
||||
"recoverAccount": "Taasta oma kasutajakonto",
|
||||
"enterRecoveryKeyHint": "Sisesta oma taastevõti",
|
||||
"recover": "Taasta",
|
||||
"invalidQRCode": "Vigane QR-kood",
|
||||
"noRecoveryKeyTitle": "Sul pole taastevõtit?",
|
||||
"enterEmailHint": "Sisesta oma e-posti aadress",
|
||||
"enterNewEmailHint": "Sisesta oma uus e-posti aadress",
|
||||
"invalidEmailTitle": "Vigane e-posti aadress",
|
||||
"invalidEmailMessage": "Palun sisesta korrektne e-posti aadress.",
|
||||
"deleteAccount": "Kustuta kasutajakonto",
|
||||
"deleteAccountQuery": "Meil on kahju, et soovid lahkuda. Kas sul tekkis mõni viga või probleem?",
|
||||
"yesSendFeedbackAction": "Jah, saadan tagasisidet",
|
||||
"noDeleteAccountAction": "Ei, kustuta kasutajakonto",
|
||||
"initiateAccountDeleteTitle": "Kasutajakonto kustutamiseks palun tuvasta end",
|
||||
"sendEmail": "Saada e-kiri",
|
||||
"createNewAccount": "Loo uus kasutajakonto",
|
||||
"weakStrength": "Nõrk",
|
||||
"strongStrength": "Tugev",
|
||||
"moderateStrength": "Keskmine",
|
||||
"confirmPassword": "Korda salasõna",
|
||||
"close": "Sulge",
|
||||
"oopsSomethingWentWrong": "Vaat kus lops! Midagi läks valesti.",
|
||||
"selectLanguage": "Vali keel",
|
||||
"language": "Keel",
|
||||
"security": "Turvalisus",
|
||||
"lockscreen": "Lukustusvaade",
|
||||
"search": "Otsi",
|
||||
"noResult": "Tulemusi pole",
|
||||
"addCode": "Lisa kood",
|
||||
"scanAQrCode": "Skanneeri QR-koodi",
|
||||
"enterDetailsManually": "Sisesta üksikasjad käsitsi",
|
||||
"edit": "Muuda",
|
||||
"share": "Jaga",
|
||||
"shareCodes": "Jaga koodi",
|
||||
"restore": "Taasta",
|
||||
"copiedToClipboard": "Kopeeritud lõikelauale",
|
||||
"copiedNextToClipboard": "Järgmine kood on kopeeritud lõikelauale",
|
||||
"error": "Viga",
|
||||
"recoveryKeyCopiedToClipboard": "Taastevõti on kopeeritud lõikelauale",
|
||||
"recoveryKeyOnForgotPassword": "Kui unustad oma salasõna, siis see krüptovõti on ainus võimalus sinu andmete taastamiseks.",
|
||||
"saveKey": "Salvesta võti",
|
||||
"save": "Salvesta",
|
||||
"send": "Saada",
|
||||
"back": "Tagasi",
|
||||
"passwordStrength": "Salasõna tugevus: {passwordStrengthValue}",
|
||||
"@passwordStrength": {
|
||||
"description": "Text to indicate the password strength",
|
||||
"placeholders": {
|
||||
"passwordStrengthValue": {
|
||||
"description": "The strength of the password as a string",
|
||||
"type": "String",
|
||||
"example": "Weak or Moderate or Strong"
|
||||
}
|
||||
},
|
||||
"message": "Password Strength: {passwordStrengthText}"
|
||||
},
|
||||
"password": "Salasõna",
|
||||
"privacyPolicyTitle": "Privaatsusreeglid",
|
||||
"termsOfServicesTitle": "Kasutustingimused",
|
||||
"encryption": "Krüptimine",
|
||||
"setPasswordTitle": "Sisesta salasõna",
|
||||
"changePasswordTitle": "Muuda salasõna",
|
||||
"resetPasswordTitle": "Lähtesta salasõna",
|
||||
"encryptionKeys": "Krüptovõtmed",
|
||||
"passwordChangedSuccessfully": "Salasõna muutmine õnnestus",
|
||||
"howItWorks": "Kuidas see töötab",
|
||||
"ackPasswordLostWarning": "Ma saan aru, et salasõna kaotamisel kaotan ka ligipääsu oma andmetele - minu andmed on ju <underline>läbivalt krüptitud</underline>.",
|
||||
"loginTerms": "Sisselogdes nõustun <u-terms>kasutustingimustega</u-terms> ja <u-policy>privaatsusreeglitega</u-policy>",
|
||||
"logInLabel": "Logi sisse",
|
||||
"logout": "Logi välja",
|
||||
"areYouSureYouWantToLogout": "Kas oled kindel, et soovid välja logida?",
|
||||
"yesLogout": "Jah, logi välja",
|
||||
"theme": "Kujundus",
|
||||
"lightTheme": "Hele kujundus",
|
||||
"darkTheme": "Tume kujundus",
|
||||
"systemTheme": "Süsteemi kujundus",
|
||||
"verifyingRecoveryKey": "Kontrollin taastevõtit...",
|
||||
"recoveryKeyVerified": "Taastevõti on kontrollitud",
|
||||
"recreatePasswordTitle": "Loo salasõna uuesti",
|
||||
"tryAgain": "Proovi uuesti",
|
||||
"privacy": "Privaatsus",
|
||||
"terms": "Kasutustingimused",
|
||||
"checkForUpdates": "Kontrolli uuendusi",
|
||||
"checkStatus": "Kontrolli olekut",
|
||||
"downloadUpdate": "Laadi alla",
|
||||
"criticalUpdateAvailable": "Saadaval on kriitiline uuendus",
|
||||
"updateAvailable": "Saadaval on uuendus",
|
||||
"update": "Uuenda",
|
||||
"checking": "Kontrollin...",
|
||||
"youAreOnTheLatestVersion": "Kasutad viimast versiooni",
|
||||
"warning": "Hoiatus",
|
||||
"exportWarningDesc": "Eksporditud failis on privaatsed andmed. Palun hoia seda turvaliselt.",
|
||||
"iUnderStand": "Sain aru",
|
||||
"@iUnderStand": {
|
||||
"description": "Text for the button to confirm the user understands the warning"
|
||||
},
|
||||
"enterPassword": "Sisesta salasõna",
|
||||
"createNewTag": "Lisa uus silt",
|
||||
"tag": "Silt",
|
||||
"create": "Loo",
|
||||
"editTag": "Muuda silti",
|
||||
"deleteTagTitle": "Kas kustutame sildi?",
|
||||
"deleteTagMessage": "Kas sa oled kindel, et soovid selle sildi kustutada? Seda tegevust ei saa tagasi pöörata.",
|
||||
"updateNotAvailable": "Uuendust pole saadaval",
|
||||
"reEnterPassword": "Sisesta salasõna uuesti",
|
||||
"setNewPassword": "Sisesta uus salasõna",
|
||||
"enterPin": "Sisesta PIN-kood",
|
||||
"setNewPin": "Määra uus PIN-kood"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"incorrectDetails": "Helytelen adatok",
|
||||
"pleaseVerifyDetails": "Kérjük, ellenőrizd az adataid, majd próbáld meg újra",
|
||||
"codeIssuerHint": "Kibocsátó",
|
||||
"codeSecretKeyHint": "Titkos (Secret) kulcs",
|
||||
"codeSecretKeyHint": "Titkos kulcs",
|
||||
"secret": "Titkos kód",
|
||||
"all": "Minden",
|
||||
"notes": "Megjegyzések",
|
||||
@@ -223,7 +223,7 @@
|
||||
"saveOrSendDescription": "El szeretné menteni ezt a tárhelyére (alapértelmezés szerint a Letöltések mappába), vagy elküldi más alkalmazásoknak?",
|
||||
"saveOnlyDescription": "El szeretné menteni ezt a tárhelyére (alapértelmezés szerint a Letöltések mappába)?",
|
||||
"back": "Vissza",
|
||||
"createAccount": "Jelszó erőssége:",
|
||||
"createAccount": "Felhasználó létrehozás",
|
||||
"passwordStrength": "Jelszó erőssége: {passwordStrengthValue}",
|
||||
"@passwordStrength": {
|
||||
"description": "Text to indicate the password strength",
|
||||
@@ -381,7 +381,7 @@
|
||||
"deleteCodeAuthMessage": "Hitelesítés a kód törléséhez",
|
||||
"showQRAuthMessage": "Hitelesítés a QR kód megjelenítéséhez",
|
||||
"confirmAccountDeleteTitle": "Fiók törlésének megerősítése",
|
||||
"confirmAccountDeleteMessage": "",
|
||||
"confirmAccountDeleteMessage": "Ez a fiók össze van kapcsolva más Ente-alkalmazásokkal, ha használ ilyet.\n\nA feltöltött adataid törlését ütemezzük az összes Ente alkalmazásban, és a fiókod véglegesen törlésre kerül.",
|
||||
"androidBiometricHint": "Személyazonosság ellenőrzése",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
@@ -515,5 +515,9 @@
|
||||
"loginWithAuthAccount": "Jelentkezzen be Auth fiókjával",
|
||||
"freeStorageOffer": "10% kedvezmény on <bold-green>ente<bold-green> photos",
|
||||
"freeStorageOfferDescription": "Használja az \"AUTH\" kódot, hogy 10% kedvezményt kapjon az első évben",
|
||||
"type": "Típus"
|
||||
"advanced": "Haladó",
|
||||
"algorithm": "Algoritmus",
|
||||
"type": "Típus",
|
||||
"period": "Időszak",
|
||||
"digits": "Számjegyek"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "Неверный QR-код",
|
||||
"noRecoveryKeyTitle": "Нет ключа восстановления?",
|
||||
"enterEmailHint": "Введите адрес электронной почты",
|
||||
"enterNewEmailHint": "Введите ваш новый адрес электронной почты",
|
||||
"invalidEmailTitle": "Неверный адрес электронной почты",
|
||||
"invalidEmailMessage": "Пожалуйста, введите действительный адрес электронной почты.",
|
||||
"deleteAccount": "Удалить аккаунт",
|
||||
@@ -334,10 +335,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Ручная",
|
||||
"manualSort": "Пользовательская",
|
||||
"editOrder": "Изменить порядок",
|
||||
"mostFrequentlyUsed": "Частота использования",
|
||||
"mostRecentlyUsed": "Недавно использованные",
|
||||
"mostFrequentlyUsed": "Часто используемые",
|
||||
"mostRecentlyUsed": "Недавно используемые",
|
||||
"activeSessions": "Активные сеансы",
|
||||
"somethingWentWrongPleaseTryAgain": "Что-то пошло не так. Попробуйте еще раз",
|
||||
"thisWillLogYouOutOfThisDevice": "Вы выйдете из этого устройства!",
|
||||
@@ -513,5 +514,10 @@
|
||||
"free5GB": "5Гб бесплатного пространства на <bold-green>ente</bold-green> Фото",
|
||||
"loginWithAuthAccount": "Войти с помощью учетной записи Auth",
|
||||
"freeStorageOffer": "Скидка 10% на <bold-green>ente</bold-green> фото",
|
||||
"freeStorageOfferDescription": "Используйте код \"AUTH\", чтобы получить скидку 10% в первый год"
|
||||
"freeStorageOfferDescription": "Используйте код \"AUTH\", чтобы получить скидку 10% в первый год",
|
||||
"advanced": "Расширенные",
|
||||
"algorithm": "Алгоритм",
|
||||
"type": "Тип",
|
||||
"period": "Период",
|
||||
"digits": "Цифр"
|
||||
}
|
||||
@@ -46,7 +46,7 @@ You can alternatively install the build from PlayStore or F-Droid.
|
||||
|
||||
## 🧑💻 Building from source
|
||||
|
||||
1. [Install Flutter v3.24.3](https://flutter.dev/docs/get-started/install).
|
||||
1. [Install Flutter v3.32.5](https://flutter.dev/docs/get-started/install).
|
||||
|
||||
2. Pull in all submodules with `git submodule update --init --recursive`
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
source 'https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git'
|
||||
platform :ios, '13.0'
|
||||
|
||||
source 'https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git'
|
||||
source 'https://cdn.cocoapods.org/'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
|
||||
@@ -75,8 +75,6 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_timezone (0.0.1):
|
||||
- Flutter
|
||||
- flutter_timezone (0.0.1):
|
||||
- Flutter
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- GoogleDataTransport (10.1.0):
|
||||
@@ -209,6 +207,8 @@ PODS:
|
||||
- sqlite3/common
|
||||
- sqlite3/fts5 (3.49.2):
|
||||
- sqlite3/common
|
||||
- sqlite3/math (3.49.2):
|
||||
- sqlite3/common
|
||||
- sqlite3/perf-threadsafe (3.49.2):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.49.2):
|
||||
@@ -240,7 +240,7 @@ PODS:
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- workmanager (0.0.1):
|
||||
- workmanager_apple (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -263,7 +263,6 @@ DEPENDENCIES:
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`)
|
||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_editor_common (from `.symlinks/plugins/image_editor_common/ios`)
|
||||
@@ -301,7 +300,7 @@ DEPENDENCIES:
|
||||
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
||||
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios:
|
||||
@@ -365,8 +364,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_sodium/ios"
|
||||
flutter_timezone:
|
||||
:path: ".symlinks/plugins/flutter_timezone/ios"
|
||||
flutter_timezone:
|
||||
:path: ".symlinks/plugins/flutter_timezone/ios"
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
home_widget:
|
||||
@@ -441,8 +438,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
workmanager:
|
||||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
workmanager_apple:
|
||||
:path: ".symlinks/plugins/workmanager_apple/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
|
||||
@@ -450,37 +447,26 @@ SPEC CHECKSUMS:
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
|
||||
background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57
|
||||
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99
|
||||
ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
|
||||
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
|
||||
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
|
||||
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
|
||||
firebase_core: ba71b44041571da878cb624ce0d80250bcbe58ad
|
||||
firebase_messaging: 13129fe2ca166d1ed2d095062d76cee88943d067
|
||||
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
|
||||
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
|
||||
FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3
|
||||
FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145
|
||||
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
|
||||
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
|
||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
@@ -489,13 +475,7 @@ SPEC CHECKSUMS:
|
||||
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
|
||||
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
|
||||
@@ -505,34 +485,18 @@ SPEC CHECKSUMS:
|
||||
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
|
||||
motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
|
||||
move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
|
||||
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
|
||||
media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
|
||||
motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
|
||||
move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
native_video_player: 6809dec117e8997161dbfb42a6f90d6df71a504d
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
|
||||
native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
|
||||
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
|
||||
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
|
||||
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
|
||||
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
@@ -544,16 +508,17 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
|
||||
sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832
|
||||
sqlite3_flutter_libs: 74334e3ef2dbdb7d37e50859bb45da43935779c4
|
||||
system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
|
||||
thermal: d4c48be750d1ddbab36b0e2dcb2471531bc8df41
|
||||
ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
workmanager_apple: f540d652595dfe5c8b8200c4c85ba622d6fb5c5b
|
||||
|
||||
PODFILE CHECKSUM: a8ef88ad74ba499756207e7592c6071a96756d18
|
||||
PODFILE CHECKSUM: cce2cd3351d3488dca65b151118552b680e23635
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -579,7 +579,7 @@
|
||||
"${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/volume_controller/volume_controller.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/wakelock_plus/wakelock_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/workmanager/workmanager.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/workmanager_apple/workmanager_apple.framework",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg_kit_custom/ffmpegkit.framework/ffmpegkit",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg_kit_custom/libavcodec.framework/libavcodec",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg_kit_custom/libavdevice.framework/libavdevice",
|
||||
@@ -674,7 +674,7 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/volume_controller.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/workmanager.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/workmanager_apple.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ffmpegkit.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavcodec.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavdevice.framework",
|
||||
|
||||
@@ -422,44 +422,7 @@ class _FileSelectionActionsWidgetState
|
||||
),
|
||||
labelText: S.of(context).editLocation,
|
||||
icon: Icons.edit_location_alt_outlined,
|
||||
onTap: () async {
|
||||
await showBarModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
backgroundColor: getEnteColorScheme(context).backgroundElevated,
|
||||
barrierColor: backdropFaintDark,
|
||||
topControl: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
// This container is for increasing the tap area
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 36,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
Container(
|
||||
height: 5,
|
||||
width: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: backgroundElevated2Light,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return UpdateLocationDataWidget(
|
||||
widget.selectedFiles.files.toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
onTap: _editLocation,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -479,11 +442,7 @@ class _FileSelectionActionsWidgetState
|
||||
labelText: S.of(context).share,
|
||||
icon: Icons.adaptive.share_outlined,
|
||||
key: shareButtonKey,
|
||||
onTap: () => shareSelected(
|
||||
context,
|
||||
shareButtonKey,
|
||||
widget.selectedFiles.files.toList(),
|
||||
),
|
||||
onTap: _shareSelectedFiles,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -523,6 +482,54 @@ class _FileSelectionActionsWidgetState
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
Future<void> _editLocation() async {
|
||||
await showBarModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
backgroundColor: getEnteColorScheme(context).backgroundElevated,
|
||||
barrierColor: backdropFaintDark,
|
||||
topControl: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
// This container is for increasing the tap area
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 36,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
Container(
|
||||
height: 5,
|
||||
width: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: backgroundElevated2Light,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return UpdateLocationDataWidget(
|
||||
widget.selectedFiles.files.toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _shareSelectedFiles() async {
|
||||
shareSelected(
|
||||
context,
|
||||
shareButtonKey,
|
||||
widget.selectedFiles.files.toList(),
|
||||
);
|
||||
widget.selectedFiles.clearAll();
|
||||
}
|
||||
|
||||
Future<void> _moveFiles() async {
|
||||
if (split.pendingUploads.isNotEmpty || split.ownedByOtherUsers.isNotEmpty) {
|
||||
widget.selectedFiles
|
||||
|
||||
BIN
web/apps/photos/public/images/preview.jpg
Normal file
|
After Width: | Height: | Size: 465 KiB |
@@ -25,6 +25,9 @@ import {
|
||||
import { SingleInputDialog } from "ente-base/components/SingleInputDialog";
|
||||
import { useModalVisibility } from "ente-base/components/utils/modal";
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import type { AddSaveGroup } from "ente-gallery/components/utils/save-groups";
|
||||
import { downloadAndSaveCollectionFiles } from "ente-gallery/services/save";
|
||||
import { uniqueFilesByID } from "ente-gallery/utils/file";
|
||||
import { CollectionOrder, type Collection } from "ente-media/collection";
|
||||
import { ItemVisibility } from "ente-media/file-metadata";
|
||||
import type { RemotePullOpts } from "ente-new/photos/components/gallery";
|
||||
@@ -33,7 +36,9 @@ import {
|
||||
GalleryItemsSummary,
|
||||
} from "ente-new/photos/components/gallery/ListHeader";
|
||||
import {
|
||||
defaultHiddenCollectionUserFacingName,
|
||||
deleteCollection,
|
||||
findDefaultHiddenCollectionIDs,
|
||||
isHiddenCollection,
|
||||
leaveSharedCollection,
|
||||
renameCollection,
|
||||
@@ -46,16 +51,15 @@ import {
|
||||
type CollectionSummary,
|
||||
type CollectionSummaryType,
|
||||
} from "ente-new/photos/services/collection-summary";
|
||||
import {
|
||||
savedCollectionFiles,
|
||||
savedCollections,
|
||||
} from "ente-new/photos/services/photos-fdb";
|
||||
import { emptyTrash } from "ente-new/photos/services/trash";
|
||||
import { usePhotosAppContext } from "ente-new/photos/types/context";
|
||||
import { t } from "i18next";
|
||||
import React, { useCallback, useRef } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import type { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
|
||||
import {
|
||||
downloadCollectionHelper,
|
||||
downloadDefaultHiddenCollectionHelper,
|
||||
} from "utils/collection";
|
||||
|
||||
export interface CollectionHeaderProps {
|
||||
collectionSummary: CollectionSummary;
|
||||
@@ -69,7 +73,11 @@ export interface CollectionHeaderProps {
|
||||
onRemotePull: (opts?: RemotePullOpts) => Promise<void>;
|
||||
onCollectionShare: () => void;
|
||||
onCollectionCast: () => void;
|
||||
setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator;
|
||||
/**
|
||||
* A function that can be used to create a UI notification to track the
|
||||
* progress of user-initiated download, and to cancel it if needed.
|
||||
*/
|
||||
onAddSaveGroup: AddSaveGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +127,7 @@ const CollectionHeaderOptions: React.FC<CollectionHeaderProps> = ({
|
||||
onRemotePull,
|
||||
onCollectionShare,
|
||||
onCollectionCast,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
onAddSaveGroup,
|
||||
isActiveCollectionDownloadInProgress,
|
||||
}) => {
|
||||
const { showMiniDialog, onGenericError } = useBaseContext();
|
||||
@@ -225,17 +233,31 @@ const CollectionHeaderOptions: React.FC<CollectionHeaderProps> = ({
|
||||
if (isActiveCollectionDownloadInProgress()) return;
|
||||
|
||||
if (collectionSummaryType == "hiddenItems") {
|
||||
await downloadDefaultHiddenCollectionHelper(
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
const defaultHiddenCollectionsIDs = findDefaultHiddenCollectionIDs(
|
||||
await savedCollections(),
|
||||
);
|
||||
const collectionFiles = await savedCollectionFiles();
|
||||
const defaultHiddenCollectionFiles = uniqueFilesByID(
|
||||
collectionFiles.filter((file) =>
|
||||
defaultHiddenCollectionsIDs.has(file.collectionID),
|
||||
),
|
||||
);
|
||||
await downloadAndSaveCollectionFiles(
|
||||
defaultHiddenCollectionUserFacingName,
|
||||
PseudoCollectionID.hiddenItems,
|
||||
defaultHiddenCollectionFiles,
|
||||
true,
|
||||
onAddSaveGroup,
|
||||
);
|
||||
} else {
|
||||
await downloadCollectionHelper(
|
||||
await downloadAndSaveCollectionFiles(
|
||||
activeCollection.name,
|
||||
activeCollection.id,
|
||||
setFilesDownloadProgressAttributesCreator(
|
||||
activeCollection.name,
|
||||
activeCollection.id,
|
||||
isHiddenCollection(activeCollection),
|
||||
(await savedCollectionFiles()).filter(
|
||||
(file) => file.collectionID == activeCollection.id,
|
||||
),
|
||||
isHiddenCollection(activeCollection),
|
||||
onAddSaveGroup,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
} from "components/Collections/CollectionShare";
|
||||
import type { TimeStampListItem } from "components/FileList";
|
||||
import { useModalVisibility } from "ente-base/components/utils/modal";
|
||||
import {
|
||||
isSaveCancelled,
|
||||
isSaveComplete,
|
||||
type SaveGroup,
|
||||
} from "ente-gallery/components/utils/save-groups";
|
||||
import type { Collection } from "ente-media/collection";
|
||||
import {
|
||||
GalleryBarImpl,
|
||||
@@ -21,17 +26,11 @@ import {
|
||||
import { includes } from "ente-utils/type-guards";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { sortCollectionSummaries } from "services/collectionService";
|
||||
import {
|
||||
isFilesDownloadCancelled,
|
||||
isFilesDownloadCompleted,
|
||||
type FilesDownloadProgressAttributes,
|
||||
} from "../FilesDownloadProgress";
|
||||
import { AlbumCastDialog } from "./AlbumCastDialog";
|
||||
import {
|
||||
CollectionHeader,
|
||||
type CollectionHeaderProps,
|
||||
} from "./CollectionHeader";
|
||||
|
||||
type GalleryBarAndListHeaderProps = Omit<
|
||||
GalleryBarImplProps,
|
||||
| "collectionSummaries"
|
||||
@@ -48,11 +47,8 @@ type GalleryBarAndListHeaderProps = Omit<
|
||||
activeCollection: Collection;
|
||||
setActiveCollectionID: (collectionID: number) => void;
|
||||
setPhotoListHeader: (value: TimeStampListItem) => void;
|
||||
filesDownloadProgressAttributesList: FilesDownloadProgressAttributes[];
|
||||
} & Pick<
|
||||
CollectionHeaderProps,
|
||||
"setFilesDownloadProgressAttributesCreator" | "onRemotePull"
|
||||
> &
|
||||
saveGroups: SaveGroup[];
|
||||
} & Pick<CollectionHeaderProps, "onRemotePull" | "onAddSaveGroup"> &
|
||||
Pick<
|
||||
CollectionShareProps,
|
||||
"user" | "emailByUserID" | "shareSuggestionEmails" | "setBlockingLoad"
|
||||
@@ -89,14 +85,14 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
setActiveCollectionID,
|
||||
setBlockingLoad,
|
||||
people,
|
||||
saveGroups,
|
||||
activePerson,
|
||||
emailByUserID,
|
||||
shareSuggestionEmails,
|
||||
onRemotePull,
|
||||
onAddSaveGroup,
|
||||
onSelectPerson,
|
||||
setPhotoListHeader,
|
||||
filesDownloadProgressAttributesList,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
}) => {
|
||||
const { show: showAllAlbums, props: allAlbumsVisibilityProps } =
|
||||
useModalVisibility();
|
||||
@@ -126,15 +122,11 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
);
|
||||
|
||||
const isActiveCollectionDownloadInProgress = useCallback(() => {
|
||||
const attributes = filesDownloadProgressAttributesList.find(
|
||||
(attr) => attr.collectionID === activeCollectionID,
|
||||
const group = saveGroups.find(
|
||||
(g) => g.collectionSummaryID === activeCollectionID,
|
||||
);
|
||||
return (
|
||||
attributes &&
|
||||
!isFilesDownloadCancelled(attributes) &&
|
||||
!isFilesDownloadCompleted(attributes)
|
||||
);
|
||||
}, [activeCollectionID, filesDownloadProgressAttributesList]);
|
||||
return group && !isSaveComplete(group) && !isSaveCancelled(group);
|
||||
}, [saveGroups, activeCollectionID]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldHide) return;
|
||||
@@ -146,9 +138,9 @@ export const GalleryBarAndListHeader: React.FC<
|
||||
{...{
|
||||
activeCollection,
|
||||
setActiveCollectionID,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
isActiveCollectionDownloadInProgress,
|
||||
onRemotePull,
|
||||
onAddSaveGroup,
|
||||
}}
|
||||
collectionSummary={toShowCollectionSummaries.get(
|
||||
activeCollectionID,
|
||||
|
||||
142
web/apps/photos/src/components/DownloadStatusNotifications.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import {
|
||||
isSaveComplete,
|
||||
isSaveCompleteWithErrors,
|
||||
isSaveStarted,
|
||||
type SaveGroup,
|
||||
} from "ente-gallery/components/utils/save-groups";
|
||||
import { Notification } from "ente-new/photos/components/Notification";
|
||||
import { t } from "i18next";
|
||||
|
||||
interface DownloadStatusNotificationsProps {
|
||||
/**
|
||||
* A list of user-initiated downloads for which a status should be shown.
|
||||
*
|
||||
* An entry is added to this list when the user initiates the download, and
|
||||
* remains here until the user explicitly closes the corresponding
|
||||
* {@link Notification} component that was showing the save group's status.
|
||||
*/
|
||||
saveGroups: SaveGroup[];
|
||||
/**
|
||||
* Called when the user closes the download status associated with the given
|
||||
* {@link saveGroup}.
|
||||
*/
|
||||
onRemoveSaveGroup: (saveGroup: SaveGroup) => void;
|
||||
/**
|
||||
* Called when the hidden section should be shown.
|
||||
*
|
||||
* This triggers the display of the dialog to authenticate the user, and the
|
||||
* returned promise when (and only if) the user successfully reauthenticates.
|
||||
*
|
||||
* Since the hidden section is only relevant in the context of the photos
|
||||
* app where there is a logged in user, this callback can be omitted in the
|
||||
* context of the public albums app.
|
||||
*/
|
||||
onShowHiddenSection?: () => Promise<void>;
|
||||
/**
|
||||
* Called when the collection with the given {@link collectionID} should be
|
||||
* shown.
|
||||
*
|
||||
* This is only relevant in the context of the photos app, and can be
|
||||
* omitted by the public albums app.
|
||||
*/
|
||||
onShowCollection?: (collectionID: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that shows a list of notifications, one each for an active
|
||||
* user-initiated download.
|
||||
*/
|
||||
export const DownloadStatusNotifications: React.FC<
|
||||
DownloadStatusNotificationsProps
|
||||
> = ({
|
||||
saveGroups,
|
||||
onRemoveSaveGroup,
|
||||
onShowHiddenSection,
|
||||
onShowCollection,
|
||||
}) => {
|
||||
const { showMiniDialog } = useBaseContext();
|
||||
|
||||
const confirmCancelDownload = (group: SaveGroup) =>
|
||||
showMiniDialog({
|
||||
title: t("stop_downloads_title"),
|
||||
message: t("stop_downloads_message"),
|
||||
continue: {
|
||||
text: t("yes_stop_downloads"),
|
||||
color: "critical",
|
||||
action: () => {
|
||||
group?.canceller.abort();
|
||||
onRemoveSaveGroup(group);
|
||||
},
|
||||
},
|
||||
cancel: t("no"),
|
||||
});
|
||||
|
||||
const createOnClose = (group: SaveGroup) => () => {
|
||||
if (isSaveComplete(group)) {
|
||||
onRemoveSaveGroup(group);
|
||||
} else {
|
||||
confirmCancelDownload(group);
|
||||
}
|
||||
};
|
||||
|
||||
const createOnClick = (group: SaveGroup) => () => {
|
||||
const electron = globalThis.electron;
|
||||
if (electron) {
|
||||
electron.openDirectory(group.downloadDirPath);
|
||||
} else if (onShowCollection) {
|
||||
if (group.isHiddenCollectionSummary) {
|
||||
void onShowHiddenSection().then(() => {
|
||||
onShowCollection(group.collectionSummaryID);
|
||||
});
|
||||
} else {
|
||||
onShowCollection(group.collectionSummaryID);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (!saveGroups) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const notifications: React.ReactNode[] = [];
|
||||
|
||||
let visibleIndex = 0;
|
||||
for (const group of saveGroups) {
|
||||
// Skip attempted downloads of empty albums, which had no effect.
|
||||
if (!isSaveStarted(group)) continue;
|
||||
|
||||
const index = visibleIndex++;
|
||||
notifications.push(
|
||||
<Notification
|
||||
key={group.id}
|
||||
horizontal="left"
|
||||
sx={{ "&&": { bottom: `${index * 80 + 20}px` } }}
|
||||
open={isSaveStarted(group)}
|
||||
onClose={createOnClose(group)}
|
||||
keepOpenOnClick
|
||||
attributes={{
|
||||
color: isSaveCompleteWithErrors(group)
|
||||
? "critical"
|
||||
: "secondary",
|
||||
title: isSaveCompleteWithErrors(group)
|
||||
? t("download_failed")
|
||||
: isSaveComplete(group)
|
||||
? t("download_complete")
|
||||
: t("downloading_album", { name: group.title }),
|
||||
caption: isSaveComplete(group)
|
||||
? group.title
|
||||
: t("download_progress", {
|
||||
count: group.success + group.failed,
|
||||
total: group.total,
|
||||
}),
|
||||
onClick: createOnClick(group),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return notifications;
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { styled } from "@mui/material";
|
||||
import { isSameDay } from "ente-base/date";
|
||||
import { formattedDate } from "ente-base/i18n-date";
|
||||
import type { AddSaveGroup } from "ente-gallery/components/utils/save-groups";
|
||||
import {
|
||||
FileViewer,
|
||||
type FileViewerProps,
|
||||
} from "ente-gallery/components/viewer/FileViewer";
|
||||
import { downloadAndSaveFiles } from "ente-gallery/services/save";
|
||||
import type { Collection } from "ente-media/collection";
|
||||
import type { EnteFile } from "ente-media/file";
|
||||
import { fileCreationTime, fileFileName } from "ente-media/file-metadata";
|
||||
@@ -14,8 +16,6 @@ import { t } from "i18next";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
import { uploadManager } from "services/upload-manager";
|
||||
import type { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
|
||||
import { downloadSingleFile } from "utils/file";
|
||||
import {
|
||||
FileList,
|
||||
type FileListAnnotatedFile,
|
||||
@@ -38,7 +38,6 @@ export type FileListWithViewerProps = {
|
||||
* Not set in the context of the shared albums app.
|
||||
*/
|
||||
onMarkTempDeleted?: (files: EnteFile[]) => void;
|
||||
setFilesDownloadProgressAttributesCreator?: SetFilesDownloadProgressAttributesCreator;
|
||||
/**
|
||||
* Called when the visibility of the file viewer dialog changes.
|
||||
*/
|
||||
@@ -48,6 +47,11 @@ export type FileListWithViewerProps = {
|
||||
* pull from remote.
|
||||
*/
|
||||
onRemotePull: () => Promise<void>;
|
||||
/**
|
||||
* A function that can be used to create a UI notification to track the
|
||||
* progress of user-initiated download, and to cancel it if needed.
|
||||
*/
|
||||
onAddSaveGroup: AddSaveGroup;
|
||||
} & Pick<
|
||||
FileListProps,
|
||||
| "mode"
|
||||
@@ -107,11 +111,11 @@ export const FileListWithViewer: React.FC<FileListWithViewerProps> = ({
|
||||
collectionNameByID,
|
||||
pendingFavoriteUpdates,
|
||||
pendingVisibilityUpdates,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
onSetOpenFileViewer,
|
||||
onRemotePull,
|
||||
onRemoteFilesPull,
|
||||
onVisualFeedback,
|
||||
onAddSaveGroup,
|
||||
onToggleFavorite,
|
||||
onFileVisibilityUpdate,
|
||||
onMarkTempDeleted,
|
||||
@@ -147,12 +151,9 @@ export const FileListWithViewer: React.FC<FileListWithViewerProps> = ({
|
||||
);
|
||||
|
||||
const handleDownload = useCallback(
|
||||
(file: EnteFile) => {
|
||||
const setSingleFileDownloadProgress =
|
||||
setFilesDownloadProgressAttributesCreator!(fileFileName(file));
|
||||
void downloadSingleFile(file, setSingleFileDownloadProgress);
|
||||
},
|
||||
[setFilesDownloadProgressAttributesCreator],
|
||||
(file: EnteFile) =>
|
||||
downloadAndSaveFiles([file], fileFileName(file), onAddSaveGroup),
|
||||
[onAddSaveGroup],
|
||||
);
|
||||
|
||||
const handleDelete = useMemo(() => {
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
// TODO: Audit this file
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import { Notification } from "ente-new/photos/components/Notification";
|
||||
import { t } from "i18next";
|
||||
|
||||
export interface FilesDownloadProgressAttributes {
|
||||
id: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
total: number;
|
||||
folderName: string;
|
||||
collectionID: number;
|
||||
isHidden: boolean;
|
||||
downloadDirPath: string;
|
||||
canceller: AbortController;
|
||||
}
|
||||
|
||||
interface FilesDownloadProgressProps {
|
||||
attributesList: FilesDownloadProgressAttributes[];
|
||||
setAttributesList: (value: FilesDownloadProgressAttributes[]) => void;
|
||||
/**
|
||||
* Called when the hidden section should be shown.
|
||||
*
|
||||
* This triggers the display of the dialog to authenticate the user, and the
|
||||
* returned promise when (and only if) the user successfully reauthenticates.
|
||||
*
|
||||
* Since the hidden section is only relevant in the context of the photos
|
||||
* app where there is a logged in user, this callback can be omitted in the
|
||||
* context of the public albums app.
|
||||
*/
|
||||
onShowHiddenSection?: () => Promise<void>;
|
||||
/**
|
||||
* Called when the collection with the given {@link collectionID} should be
|
||||
* shown.
|
||||
*
|
||||
* This is only relevant in the context of the photos app, and can be
|
||||
* omitted by the public albums app.
|
||||
*/
|
||||
onShowCollection?: (collectionID: number) => void;
|
||||
}
|
||||
|
||||
export const isFilesDownloadStarted = (
|
||||
attributes: FilesDownloadProgressAttributes,
|
||||
) => {
|
||||
return attributes && attributes.total > 0;
|
||||
};
|
||||
|
||||
export const isFilesDownloadCompleted = (
|
||||
attributes: FilesDownloadProgressAttributes,
|
||||
) => {
|
||||
return (
|
||||
attributes &&
|
||||
attributes.success + attributes.failed === attributes.total
|
||||
);
|
||||
};
|
||||
|
||||
const isFilesDownloadCompletedWithErrors = (
|
||||
attributes: FilesDownloadProgressAttributes,
|
||||
) => {
|
||||
return (
|
||||
attributes &&
|
||||
attributes.failed > 0 &&
|
||||
isFilesDownloadCompleted(attributes)
|
||||
);
|
||||
};
|
||||
|
||||
export const isFilesDownloadCancelled = (
|
||||
attributes: FilesDownloadProgressAttributes,
|
||||
) => {
|
||||
return attributes?.canceller?.signal?.aborted;
|
||||
};
|
||||
|
||||
export const FilesDownloadProgress: React.FC<FilesDownloadProgressProps> = ({
|
||||
attributesList,
|
||||
setAttributesList,
|
||||
onShowHiddenSection,
|
||||
onShowCollection,
|
||||
}) => {
|
||||
const { showMiniDialog } = useBaseContext();
|
||||
|
||||
if (!attributesList) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const onClose = (id: number) => {
|
||||
setAttributesList(attributesList.filter((attr) => attr.id !== id));
|
||||
};
|
||||
|
||||
const confirmCancelDownload = (
|
||||
attributes: FilesDownloadProgressAttributes,
|
||||
) => {
|
||||
showMiniDialog({
|
||||
title: t("stop_downloads_title"),
|
||||
message: t("stop_downloads_message"),
|
||||
continue: {
|
||||
text: t("yes_stop_downloads"),
|
||||
color: "critical",
|
||||
action: () => {
|
||||
attributes?.canceller.abort();
|
||||
onClose(attributes.id);
|
||||
},
|
||||
},
|
||||
cancel: t("no"),
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = (attributes: FilesDownloadProgressAttributes) => () => {
|
||||
if (isFilesDownloadCompleted(attributes)) {
|
||||
onClose(attributes.id);
|
||||
} else {
|
||||
confirmCancelDownload(attributes);
|
||||
}
|
||||
};
|
||||
|
||||
const createHandleOnClick =
|
||||
(id: number, onShowCollection: (collectionID: number) => void) =>
|
||||
() => {
|
||||
const attributes = attributesList.find((attr) => attr.id === id);
|
||||
const electron = globalThis.electron;
|
||||
if (electron) {
|
||||
electron.openDirectory(attributes.downloadDirPath);
|
||||
} else if (onShowCollection) {
|
||||
if (attributes.isHidden) {
|
||||
void onShowHiddenSection().then(() => {
|
||||
onShowCollection(attributes.collectionID);
|
||||
});
|
||||
} else {
|
||||
onShowCollection(attributes.collectionID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const notifications: React.ReactNode[] = [];
|
||||
let visibleIndex = 0;
|
||||
for (const attributes of attributesList) {
|
||||
// Skip attempted downloads of empty albums, which had no effect.
|
||||
if (!isFilesDownloadStarted(attributes)) continue;
|
||||
|
||||
const index = visibleIndex++;
|
||||
notifications.push(
|
||||
<Notification
|
||||
key={attributes.id}
|
||||
horizontal="left"
|
||||
sx={{ "&&": { bottom: `${index * 80 + 20}px` } }}
|
||||
open={isFilesDownloadStarted(attributes)}
|
||||
onClose={handleClose(attributes)}
|
||||
keepOpenOnClick
|
||||
attributes={{
|
||||
color: isFilesDownloadCompletedWithErrors(attributes)
|
||||
? "critical"
|
||||
: "secondary",
|
||||
title: isFilesDownloadCompletedWithErrors(attributes)
|
||||
? t("download_failed")
|
||||
: isFilesDownloadCompleted(attributes)
|
||||
? t("download_complete")
|
||||
: t("downloading_album", {
|
||||
name: attributes.folderName,
|
||||
}),
|
||||
caption: isFilesDownloadCompleted(attributes)
|
||||
? attributes.folderName
|
||||
: t("download_progress", {
|
||||
count: attributes.success + attributes.failed,
|
||||
total: attributes.total,
|
||||
}),
|
||||
onClick: onShowCollection
|
||||
? createHandleOnClick(attributes.id, onShowCollection)
|
||||
: undefined,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return notifications;
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import { isDesktop, staticAppTitle } from "ente-base/app";
|
||||
import { CenteredRow } from "ente-base/components/containers";
|
||||
import { CustomHead } from "ente-base/components/Head";
|
||||
import { CustomHeadPhotosOrAlbums } from "ente-base/components/Head";
|
||||
import {
|
||||
LoadingIndicator,
|
||||
TranslucentLoadingOverlay,
|
||||
@@ -170,7 +170,7 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={photosTheme}>
|
||||
<CustomHead {...{ title }} />
|
||||
<CustomHeadPhotosOrAlbums {...{ title }} />
|
||||
<CssBaseline enableColorScheme />
|
||||
|
||||
<ThemedLoadingBar ref={loadingBarRef} />
|
||||
|
||||
@@ -4,12 +4,9 @@ import MenuIcon from "@mui/icons-material/Menu";
|
||||
import { IconButton, Stack, Typography } from "@mui/material";
|
||||
import { AuthenticateUser } from "components/AuthenticateUser";
|
||||
import { GalleryBarAndListHeader } from "components/Collections/GalleryBarAndListHeader";
|
||||
import { DownloadStatusNotifications } from "components/DownloadStatusNotifications";
|
||||
import { type TimeStampListItem } from "components/FileList";
|
||||
import { FileListWithViewer } from "components/FileListWithViewer";
|
||||
import {
|
||||
FilesDownloadProgress,
|
||||
type FilesDownloadProgressAttributes,
|
||||
} from "components/FilesDownloadProgress";
|
||||
import { FixCreationTime } from "components/FixCreationTime";
|
||||
import { Sidebar } from "components/Sidebar";
|
||||
import { Upload } from "components/Upload";
|
||||
@@ -41,6 +38,7 @@ import {
|
||||
import { savedAuthToken } from "ente-base/token";
|
||||
import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone";
|
||||
import { type UploadTypeSelectorIntent } from "ente-gallery/components/Upload";
|
||||
import { useSaveGroups } from "ente-gallery/components/utils/save-groups";
|
||||
import { type Collection } from "ente-media/collection";
|
||||
import { type EnteFile } from "ente-media/file";
|
||||
import { type ItemVisibility } from "ente-media/file-metadata";
|
||||
@@ -123,11 +121,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { FileWithPath } from "react-dropzone";
|
||||
import { Trans } from "react-i18next";
|
||||
import { uploadManager } from "services/upload-manager";
|
||||
import type {
|
||||
SelectedState,
|
||||
SetFilesDownloadProgressAttributes,
|
||||
SetFilesDownloadProgressAttributesCreator,
|
||||
} from "types/gallery";
|
||||
import type { SelectedState } from "types/gallery";
|
||||
import { getSelectedFiles, performFileOp } from "utils/file";
|
||||
|
||||
/**
|
||||
@@ -191,10 +185,7 @@ const Page: React.FC = () => {
|
||||
const [photoListHeader, setPhotoListHeader] =
|
||||
useState<TimeStampListItem>(null);
|
||||
|
||||
const [
|
||||
filesDownloadProgressAttributesList,
|
||||
setFilesDownloadProgressAttributesList,
|
||||
] = useState<FilesDownloadProgressAttributes[]>([]);
|
||||
const { saveGroups, onAddSaveGroup, onRemoveSaveGroup } = useSaveGroups();
|
||||
const [, setPostCreateAlbumOp] = useState<CollectionOp | undefined>(
|
||||
undefined,
|
||||
);
|
||||
@@ -631,39 +622,6 @@ const Page: React.FC = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator =
|
||||
useCallback((folderName, collectionID, isHidden) => {
|
||||
const id = Math.random();
|
||||
const updater: SetFilesDownloadProgressAttributes = (value) => {
|
||||
setFilesDownloadProgressAttributesList((prev) => {
|
||||
const attributes = prev?.find((attr) => attr.id === id);
|
||||
const updatedAttributes =
|
||||
typeof value == "function"
|
||||
? value(attributes)
|
||||
: { ...attributes, ...value };
|
||||
const updatedAttributesList = attributes
|
||||
? prev.map((attr) =>
|
||||
attr.id === id ? updatedAttributes : attr,
|
||||
)
|
||||
: [...prev, updatedAttributes];
|
||||
|
||||
return updatedAttributesList;
|
||||
});
|
||||
};
|
||||
updater({
|
||||
id,
|
||||
folderName,
|
||||
collectionID,
|
||||
isHidden,
|
||||
canceller: null,
|
||||
total: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
downloadDirPath: null,
|
||||
});
|
||||
return updater;
|
||||
}, []);
|
||||
|
||||
const handleRemoveFilesFromCollection = (collection: Collection) => {
|
||||
void (async () => {
|
||||
showLoadingBar();
|
||||
@@ -771,6 +729,7 @@ const Page: React.FC = () => {
|
||||
await performFileOp(
|
||||
op,
|
||||
toProcessFiles,
|
||||
onAddSaveGroup,
|
||||
handleMarkTempDeleted,
|
||||
() => dispatch({ type: "clearTempDeleted" }),
|
||||
(files) => dispatch({ type: "markTempHidden", files }),
|
||||
@@ -779,7 +738,6 @@ const Page: React.FC = () => {
|
||||
setFixCreationTimeFiles(files);
|
||||
showFixCreationTime();
|
||||
},
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
);
|
||||
}
|
||||
// Apart from download, the other operations currently only work
|
||||
@@ -995,9 +953,8 @@ const Page: React.FC = () => {
|
||||
)!
|
||||
}
|
||||
/>
|
||||
<FilesDownloadProgress
|
||||
attributesList={filesDownloadProgressAttributesList}
|
||||
setAttributesList={setFilesDownloadProgressAttributesList}
|
||||
<DownloadStatusNotifications
|
||||
{...{ saveGroups, onRemoveSaveGroup }}
|
||||
onShowHiddenSection={handleShowHiddenSection}
|
||||
onShowCollection={handleShowCollection}
|
||||
/>
|
||||
@@ -1067,8 +1024,8 @@ const Page: React.FC = () => {
|
||||
activeCollectionID,
|
||||
activePerson,
|
||||
setPhotoListHeader,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
filesDownloadProgressAttributesList,
|
||||
saveGroups,
|
||||
onAddSaveGroup,
|
||||
}}
|
||||
mode={barMode}
|
||||
shouldHide={isInSearchMode}
|
||||
@@ -1165,11 +1122,9 @@ const Page: React.FC = () => {
|
||||
fileNormalCollectionIDs,
|
||||
pendingFavoriteUpdates,
|
||||
pendingVisibilityUpdates,
|
||||
onAddSaveGroup,
|
||||
}}
|
||||
emailByUserID={state.emailByUserID}
|
||||
setFilesDownloadProgressAttributesCreator={
|
||||
setFilesDownloadProgressAttributesCreator
|
||||
}
|
||||
onToggleFavorite={handleFileViewerToggleFavorite}
|
||||
onFileVisibilityUpdate={
|
||||
handleFileViewerFileVisibilityUpdate
|
||||
|
||||
@@ -6,12 +6,9 @@ import DownloadIcon from "@mui/icons-material/Download";
|
||||
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||
import { Box, Button, IconButton, Stack, styled, Tooltip } from "@mui/material";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { DownloadStatusNotifications } from "components/DownloadStatusNotifications";
|
||||
import type { TimeStampListItem } from "components/FileList";
|
||||
import { FileListWithViewer } from "components/FileListWithViewer";
|
||||
import {
|
||||
FilesDownloadProgress,
|
||||
type FilesDownloadProgressAttributes,
|
||||
} from "components/FilesDownloadProgress";
|
||||
import { Upload } from "components/Upload";
|
||||
import {
|
||||
AccountsPageContents,
|
||||
@@ -50,7 +47,15 @@ import {
|
||||
} from "ente-base/http";
|
||||
import log from "ente-base/log";
|
||||
import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone";
|
||||
import {
|
||||
useSaveGroups,
|
||||
type AddSaveGroup,
|
||||
} from "ente-gallery/components/utils/save-groups";
|
||||
import { downloadManager } from "ente-gallery/services/download";
|
||||
import {
|
||||
downloadAndSaveCollectionFiles,
|
||||
downloadAndSaveFiles,
|
||||
} from "ente-gallery/services/save";
|
||||
import { extractCollectionKeyFromShareURL } from "ente-gallery/services/share";
|
||||
import { updateShouldDisableCFUploadProxy } from "ente-gallery/services/upload";
|
||||
import { sortFiles } from "ente-gallery/utils/file";
|
||||
@@ -83,13 +88,8 @@ import { useRouter } from "next/router";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { type FileWithPath } from "react-dropzone";
|
||||
import { uploadManager } from "services/upload-manager";
|
||||
import type {
|
||||
SelectedState,
|
||||
SetFilesDownloadProgressAttributes,
|
||||
SetFilesDownloadProgressAttributesCreator,
|
||||
} from "types/gallery";
|
||||
import { downloadCollectionFiles } from "utils/collection";
|
||||
import { downloadSelectedFiles, getSelectedFiles } from "utils/file";
|
||||
import type { SelectedState } from "types/gallery";
|
||||
import { getSelectedFiles } from "utils/file";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
|
||||
export default function PublicCollectionGallery() {
|
||||
@@ -130,44 +130,7 @@ export default function PublicCollectionGallery() {
|
||||
collectionID: 0,
|
||||
context: undefined,
|
||||
});
|
||||
|
||||
const [
|
||||
filesDownloadProgressAttributesList,
|
||||
setFilesDownloadProgressAttributesList,
|
||||
] = useState<FilesDownloadProgressAttributes[]>([]);
|
||||
|
||||
const setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator =
|
||||
useCallback((folderName, collectionID, isHidden) => {
|
||||
const id = Math.random();
|
||||
const updater: SetFilesDownloadProgressAttributes = (value) => {
|
||||
setFilesDownloadProgressAttributesList((prev) => {
|
||||
const attributes = prev?.find((attr) => attr.id === id);
|
||||
const updatedAttributes =
|
||||
typeof value == "function"
|
||||
? value(attributes)
|
||||
: { ...attributes, ...value };
|
||||
const updatedAttributesList = attributes
|
||||
? prev.map((attr) =>
|
||||
attr.id === id ? updatedAttributes : attr,
|
||||
)
|
||||
: [...prev, updatedAttributes];
|
||||
|
||||
return updatedAttributesList;
|
||||
});
|
||||
};
|
||||
updater({
|
||||
id,
|
||||
folderName,
|
||||
collectionID,
|
||||
isHidden,
|
||||
canceller: null,
|
||||
total: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
downloadDirPath: null,
|
||||
});
|
||||
return updater;
|
||||
}, []);
|
||||
const { saveGroups, onAddSaveGroup, onRemoveSaveGroup } = useSaveGroups();
|
||||
|
||||
const onAddPhotos = useMemo(() => {
|
||||
return publicCollection?.publicURLs[0]?.enableCollect
|
||||
@@ -274,11 +237,7 @@ export default function PublicCollectionGallery() {
|
||||
setPhotoListHeader({
|
||||
item: (
|
||||
<ListHeader
|
||||
{...{
|
||||
publicCollection,
|
||||
publicFiles,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
}}
|
||||
{...{ publicCollection, publicFiles, onAddSaveGroup }}
|
||||
/>
|
||||
),
|
||||
tag: "header",
|
||||
@@ -453,13 +412,10 @@ export default function PublicCollectionGallery() {
|
||||
const downloadFilesHelper = async () => {
|
||||
try {
|
||||
const selectedFiles = getSelectedFiles(selected, publicFiles);
|
||||
const setFilesDownloadProgressAttributes =
|
||||
setFilesDownloadProgressAttributesCreator(
|
||||
t("files_count", { count: selectedFiles.length }),
|
||||
);
|
||||
await downloadSelectedFiles(
|
||||
await downloadAndSaveFiles(
|
||||
selectedFiles,
|
||||
setFilesDownloadProgressAttributes,
|
||||
t("files_count", { count: selectedFiles.length }),
|
||||
onAddSaveGroup,
|
||||
);
|
||||
clearSelection();
|
||||
} catch (e) {
|
||||
@@ -554,11 +510,9 @@ export default function PublicCollectionGallery() {
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
activeCollectionID={PseudoCollectionID.all}
|
||||
setFilesDownloadProgressAttributesCreator={
|
||||
setFilesDownloadProgressAttributesCreator
|
||||
}
|
||||
onRemotePull={publicAlbumsRemotePull}
|
||||
onVisualFeedback={handleVisualFeedback}
|
||||
onAddSaveGroup={onAddSaveGroup}
|
||||
/>
|
||||
{blockingLoad && <TranslucentLoadingOverlay />}
|
||||
<Upload
|
||||
@@ -573,9 +527,8 @@ export default function PublicCollectionGallery() {
|
||||
onShowSessionExpiredDialog={showPublicLinkExpiredMessage}
|
||||
{...{ dragAndDropFiles }}
|
||||
/>
|
||||
<FilesDownloadProgress
|
||||
attributesList={filesDownloadProgressAttributesList}
|
||||
setAttributesList={setFilesDownloadProgressAttributesList}
|
||||
<DownloadStatusNotifications
|
||||
{...{ saveGroups, onRemoveSaveGroup }}
|
||||
/>
|
||||
</FullScreenDropZone>
|
||||
</PublicCollectionGalleryContext.Provider>
|
||||
@@ -680,30 +633,25 @@ const SelectedFileOptions: React.FC<SelectedFileOptionsProps> = ({
|
||||
interface ListHeaderProps {
|
||||
publicCollection: Collection;
|
||||
publicFiles: EnteFile[];
|
||||
setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator;
|
||||
onAddSaveGroup: AddSaveGroup;
|
||||
}
|
||||
|
||||
const ListHeader: React.FC<ListHeaderProps> = ({
|
||||
publicCollection,
|
||||
publicFiles,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
onAddSaveGroup,
|
||||
}) => {
|
||||
const downloadEnabled =
|
||||
publicCollection.publicURLs?.[0]?.enableDownload ?? true;
|
||||
|
||||
const downloadAllFiles = async () => {
|
||||
const setFilesDownloadProgressAttributes =
|
||||
setFilesDownloadProgressAttributesCreator(
|
||||
publicCollection.name,
|
||||
publicCollection.id,
|
||||
isHiddenCollection(publicCollection),
|
||||
);
|
||||
await downloadCollectionFiles(
|
||||
const downloadAllFiles = () =>
|
||||
downloadAndSaveCollectionFiles(
|
||||
publicCollection.name,
|
||||
publicCollection.id,
|
||||
publicFiles,
|
||||
setFilesDownloadProgressAttributes,
|
||||
isHiddenCollection(publicCollection),
|
||||
onAddSaveGroup,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<GalleryItemsHeaderAdapter>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { type FilesDownloadProgressAttributes } from "components/FilesDownloadProgress";
|
||||
import { type SelectionContext } from "ente-new/photos/components/gallery";
|
||||
|
||||
export interface SelectedState {
|
||||
@@ -17,19 +16,6 @@ export type SetSelectedState = React.Dispatch<
|
||||
React.SetStateAction<SelectedState>
|
||||
>;
|
||||
export type SetLoading = React.Dispatch<React.SetStateAction<boolean>>;
|
||||
export type SetFilesDownloadProgressAttributes = (
|
||||
value:
|
||||
| Partial<FilesDownloadProgressAttributes>
|
||||
| ((
|
||||
prev: FilesDownloadProgressAttributes,
|
||||
) => FilesDownloadProgressAttributes),
|
||||
) => void;
|
||||
|
||||
export type SetFilesDownloadProgressAttributesCreator = (
|
||||
folderName: string,
|
||||
collectionID?: number,
|
||||
isHidden?: boolean,
|
||||
) => SetFilesDownloadProgressAttributes;
|
||||
|
||||
export interface MergedSourceURL {
|
||||
original: string;
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { ensureElectron } from "ente-base/electron";
|
||||
import { joinPath } from "ente-base/file-name";
|
||||
import log from "ente-base/log";
|
||||
import { uniqueFilesByID } from "ente-gallery/utils/file";
|
||||
import type { EnteFile } from "ente-media/file";
|
||||
import {
|
||||
defaultHiddenCollectionUserFacingName,
|
||||
findDefaultHiddenCollectionIDs,
|
||||
} from "ente-new/photos/services/collection";
|
||||
import { PseudoCollectionID } from "ente-new/photos/services/collection-summary";
|
||||
import {
|
||||
savedCollectionFiles,
|
||||
savedCollections,
|
||||
} from "ente-new/photos/services/photos-fdb";
|
||||
import { safeDirectoryName } from "ente-new/photos/utils/native-fs";
|
||||
import type {
|
||||
SetFilesDownloadProgressAttributes,
|
||||
SetFilesDownloadProgressAttributesCreator,
|
||||
} from "types/gallery";
|
||||
import { downloadFilesWithProgress } from "utils/file";
|
||||
|
||||
export async function downloadCollectionHelper(
|
||||
collectionID: number,
|
||||
setFilesDownloadProgressAttributes: SetFilesDownloadProgressAttributes,
|
||||
) {
|
||||
try {
|
||||
const allFiles = await savedCollectionFiles();
|
||||
const collectionFiles = allFiles.filter(
|
||||
(file) => file.collectionID == collectionID,
|
||||
);
|
||||
const allCollections = await savedCollections();
|
||||
const collection = allCollections.find(
|
||||
(collection) => collection.id == collectionID,
|
||||
);
|
||||
if (!collection) {
|
||||
throw Error("collection not found");
|
||||
}
|
||||
await downloadCollectionFiles(
|
||||
collection.name,
|
||||
collectionFiles,
|
||||
setFilesDownloadProgressAttributes,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("download collection failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadDefaultHiddenCollectionHelper(
|
||||
setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator,
|
||||
) {
|
||||
try {
|
||||
const defaultHiddenCollectionsIDs = findDefaultHiddenCollectionIDs(
|
||||
await savedCollections(),
|
||||
);
|
||||
const collectionFiles = await savedCollectionFiles();
|
||||
const defaultHiddenCollectionFiles = uniqueFilesByID(
|
||||
collectionFiles.filter((file) =>
|
||||
defaultHiddenCollectionsIDs.has(file.collectionID),
|
||||
),
|
||||
);
|
||||
const setFilesDownloadProgressAttributes =
|
||||
setFilesDownloadProgressAttributesCreator(
|
||||
defaultHiddenCollectionUserFacingName,
|
||||
PseudoCollectionID.hiddenItems,
|
||||
true,
|
||||
);
|
||||
|
||||
await downloadCollectionFiles(
|
||||
defaultHiddenCollectionUserFacingName,
|
||||
defaultHiddenCollectionFiles,
|
||||
setFilesDownloadProgressAttributes,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("download hidden files failed ", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadCollectionFiles(
|
||||
collectionName: string,
|
||||
collectionFiles: EnteFile[],
|
||||
setFilesDownloadProgressAttributes: SetFilesDownloadProgressAttributes,
|
||||
) {
|
||||
if (!collectionFiles.length) {
|
||||
return;
|
||||
}
|
||||
let downloadDirPath: string;
|
||||
const electron = globalThis.electron;
|
||||
if (electron) {
|
||||
const selectedDir = await electron.selectDirectory();
|
||||
if (!selectedDir) {
|
||||
return;
|
||||
}
|
||||
downloadDirPath = await createCollectionDownloadFolder(
|
||||
selectedDir,
|
||||
collectionName,
|
||||
);
|
||||
}
|
||||
await downloadFilesWithProgress(
|
||||
collectionFiles,
|
||||
downloadDirPath,
|
||||
setFilesDownloadProgressAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
async function createCollectionDownloadFolder(
|
||||
downloadDirPath: string,
|
||||
collectionName: string,
|
||||
) {
|
||||
const fs = ensureElectron().fs;
|
||||
const collectionDownloadName = await safeDirectoryName(
|
||||
downloadDirPath,
|
||||
collectionName,
|
||||
fs.exists,
|
||||
);
|
||||
const collectionDownloadPath = joinPath(
|
||||
downloadDirPath,
|
||||
collectionDownloadName,
|
||||
);
|
||||
await fs.mkdirIfNeeded(collectionDownloadPath);
|
||||
return collectionDownloadPath;
|
||||
}
|
||||
@@ -1,15 +1,8 @@
|
||||
import type { LocalUser } from "ente-accounts/services/user";
|
||||
import { joinPath } from "ente-base/file-name";
|
||||
import log from "ente-base/log";
|
||||
import { type Electron } from "ente-base/types/ipc";
|
||||
import { saveAsFileAndRevokeObjectURL } from "ente-base/utils/web";
|
||||
import { downloadManager } from "ente-gallery/services/download";
|
||||
import { detectFileTypeInfo } from "ente-gallery/utils/detect-type";
|
||||
import { writeStream } from "ente-gallery/utils/native-stream";
|
||||
import type { AddSaveGroup } from "ente-gallery/components/utils/save-groups";
|
||||
import { downloadAndSaveFiles } from "ente-gallery/services/save";
|
||||
import type { EnteFile } from "ente-media/file";
|
||||
import { ItemVisibility, fileFileName } from "ente-media/file-metadata";
|
||||
import { FileType } from "ente-media/file-type";
|
||||
import { decodeLivePhoto } from "ente-media/live-photo";
|
||||
import { ItemVisibility } from "ente-media/file-metadata";
|
||||
import { type FileOp } from "ente-new/photos/components/SelectedFileOptions";
|
||||
import {
|
||||
addToFavoritesCollection,
|
||||
@@ -18,14 +11,8 @@ import {
|
||||
moveToTrash,
|
||||
} from "ente-new/photos/services/collection";
|
||||
import { updateFilesVisibility } from "ente-new/photos/services/file";
|
||||
import { safeFileName } from "ente-new/photos/utils/native-fs";
|
||||
import { wait } from "ente-utils/promise";
|
||||
import { t } from "i18next";
|
||||
import type {
|
||||
SelectedState,
|
||||
SetFilesDownloadProgressAttributes,
|
||||
SetFilesDownloadProgressAttributesCreator,
|
||||
} from "types/gallery";
|
||||
import type { SelectedState } from "types/gallery";
|
||||
|
||||
export function getSelectedFiles(
|
||||
selected: SelectedState,
|
||||
@@ -41,239 +28,6 @@ export function getSelectedFiles(
|
||||
return files.filter((file) => selectedFilesIDs.has(file.id));
|
||||
}
|
||||
|
||||
export async function getFileFromURL(fileURL: string, name: string) {
|
||||
const fileBlob = await (await fetch(fileURL)).blob();
|
||||
const fileFile = new File([fileBlob], name);
|
||||
return fileFile;
|
||||
}
|
||||
|
||||
export async function downloadFilesWithProgress(
|
||||
files: EnteFile[],
|
||||
downloadDirPath: string,
|
||||
setFilesDownloadProgressAttributes: SetFilesDownloadProgressAttributes,
|
||||
) {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
const canceller = new AbortController();
|
||||
const increaseSuccess = () => {
|
||||
if (canceller.signal.aborted) return;
|
||||
setFilesDownloadProgressAttributes((prev) => ({
|
||||
...prev,
|
||||
success: prev.success + 1,
|
||||
}));
|
||||
};
|
||||
const increaseFailed = () => {
|
||||
if (canceller.signal.aborted) return;
|
||||
setFilesDownloadProgressAttributes((prev) => ({
|
||||
...prev,
|
||||
failed: prev.failed + 1,
|
||||
}));
|
||||
};
|
||||
const isCancelled = () => canceller.signal.aborted;
|
||||
|
||||
setFilesDownloadProgressAttributes({
|
||||
downloadDirPath,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
total: files.length,
|
||||
canceller,
|
||||
});
|
||||
|
||||
const electron = globalThis.electron;
|
||||
if (electron) {
|
||||
await downloadFilesDesktop(
|
||||
electron,
|
||||
files,
|
||||
{ increaseSuccess, increaseFailed, isCancelled },
|
||||
downloadDirPath,
|
||||
);
|
||||
} else {
|
||||
await downloadFiles(files, {
|
||||
increaseSuccess,
|
||||
increaseFailed,
|
||||
isCancelled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadSelectedFiles(
|
||||
files: EnteFile[],
|
||||
setFilesDownloadProgressAttributes: SetFilesDownloadProgressAttributes,
|
||||
) {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
let downloadDirPath: string;
|
||||
const electron = globalThis.electron;
|
||||
if (electron) {
|
||||
downloadDirPath = await electron.selectDirectory();
|
||||
if (!downloadDirPath) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await downloadFilesWithProgress(
|
||||
files,
|
||||
downloadDirPath,
|
||||
setFilesDownloadProgressAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
export async function downloadSingleFile(
|
||||
file: EnteFile,
|
||||
setFilesDownloadProgressAttributes: SetFilesDownloadProgressAttributes,
|
||||
) {
|
||||
let downloadDirPath: string;
|
||||
const electron = globalThis.electron;
|
||||
if (electron) {
|
||||
downloadDirPath = await electron.selectDirectory();
|
||||
if (!downloadDirPath) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await downloadFilesWithProgress(
|
||||
[file],
|
||||
downloadDirPath,
|
||||
setFilesDownloadProgressAttributes,
|
||||
);
|
||||
}
|
||||
|
||||
export async function downloadFiles(
|
||||
files: EnteFile[],
|
||||
progressBarUpdater: {
|
||||
increaseSuccess: () => void;
|
||||
increaseFailed: () => void;
|
||||
isCancelled: () => boolean;
|
||||
},
|
||||
) {
|
||||
for (const file of files) {
|
||||
try {
|
||||
if (progressBarUpdater?.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
await saveAsFile(file);
|
||||
progressBarUpdater?.increaseSuccess();
|
||||
} catch (e) {
|
||||
log.error("download fail for file", e);
|
||||
progressBarUpdater?.increaseFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the given {@link EnteFile} as a file in the user's download folder.
|
||||
*/
|
||||
const saveAsFile = async (file: EnteFile) => {
|
||||
const fileBlob = await downloadManager.fileBlob(file);
|
||||
const fileName = fileFileName(file);
|
||||
if (file.metadata.fileType == FileType.livePhoto) {
|
||||
const { imageFileName, imageData, videoFileName, videoData } =
|
||||
await decodeLivePhoto(fileName, fileBlob);
|
||||
|
||||
await saveBlobPartAsFile(imageData, imageFileName);
|
||||
|
||||
// Downloading multiple works everywhere except, you guessed it,
|
||||
// Safari. Make up for their incompetence by adding a setTimeout.
|
||||
await wait(300) /* arbitrary constant, 300ms */;
|
||||
await saveBlobPartAsFile(videoData, videoFileName);
|
||||
} else {
|
||||
await saveBlobPartAsFile(fileBlob, fileName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the given {@link blob} as a file in the user's download folder.
|
||||
*/
|
||||
const saveBlobPartAsFile = async (blobPart: BlobPart, fileName: string) =>
|
||||
createTypedObjectURL(blobPart, fileName).then((url) =>
|
||||
saveAsFileAndRevokeObjectURL(url, fileName),
|
||||
);
|
||||
|
||||
const createTypedObjectURL = async (blobPart: BlobPart, fileName: string) => {
|
||||
const blob = blobPart instanceof Blob ? blobPart : new Blob([blobPart]);
|
||||
const { mimeType } = await detectFileTypeInfo(new File([blob], fileName));
|
||||
return URL.createObjectURL(new Blob([blob], { type: mimeType }));
|
||||
};
|
||||
|
||||
async function downloadFilesDesktop(
|
||||
electron: Electron,
|
||||
files: EnteFile[],
|
||||
progressBarUpdater: {
|
||||
increaseSuccess: () => void;
|
||||
increaseFailed: () => void;
|
||||
isCancelled: () => boolean;
|
||||
},
|
||||
downloadPath: string,
|
||||
) {
|
||||
for (const file of files) {
|
||||
try {
|
||||
if (progressBarUpdater?.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
await downloadFileDesktop(electron, file, downloadPath);
|
||||
progressBarUpdater?.increaseSuccess();
|
||||
} catch (e) {
|
||||
log.error("download fail for file", e);
|
||||
progressBarUpdater?.increaseFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFileDesktop(
|
||||
electron: Electron,
|
||||
file: EnteFile,
|
||||
downloadDir: string,
|
||||
) {
|
||||
const fs = electron.fs;
|
||||
|
||||
const stream = await downloadManager.fileStream(file);
|
||||
const fileName = fileFileName(file);
|
||||
|
||||
if (file.metadata.fileType == FileType.livePhoto) {
|
||||
const fileBlob = await new Response(stream).blob();
|
||||
const { imageFileName, imageData, videoFileName, videoData } =
|
||||
await decodeLivePhoto(fileName, fileBlob);
|
||||
const imageExportName = await safeFileName(
|
||||
downloadDir,
|
||||
imageFileName,
|
||||
fs.exists,
|
||||
);
|
||||
const imageStream = new Response(imageData).body;
|
||||
await writeStream(
|
||||
electron,
|
||||
joinPath(downloadDir, imageExportName),
|
||||
imageStream,
|
||||
);
|
||||
try {
|
||||
const videoExportName = await safeFileName(
|
||||
downloadDir,
|
||||
videoFileName,
|
||||
fs.exists,
|
||||
);
|
||||
const videoStream = new Response(videoData).body;
|
||||
await writeStream(
|
||||
electron,
|
||||
joinPath(downloadDir, videoExportName),
|
||||
videoStream,
|
||||
);
|
||||
} catch (e) {
|
||||
await fs.rm(joinPath(downloadDir, imageExportName));
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
const fileExportName = await safeFileName(
|
||||
downloadDir,
|
||||
fileName,
|
||||
fs.exists,
|
||||
);
|
||||
await writeStream(
|
||||
electron,
|
||||
joinPath(downloadDir, fileExportName),
|
||||
stream,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const shouldShowAvatar = (
|
||||
file: EnteFile,
|
||||
user: LocalUser | undefined,
|
||||
@@ -299,22 +53,19 @@ export const shouldShowAvatar = (
|
||||
export const performFileOp = async (
|
||||
op: FileOp,
|
||||
files: EnteFile[],
|
||||
onAddSaveGroup: AddSaveGroup,
|
||||
markTempDeleted: (files: EnteFile[]) => void,
|
||||
clearTempDeleted: () => void,
|
||||
markTempHidden: (files: EnteFile[]) => void,
|
||||
clearTempHidden: () => void,
|
||||
fixCreationTime: (files: EnteFile[]) => void,
|
||||
setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator,
|
||||
) => {
|
||||
switch (op) {
|
||||
case "download": {
|
||||
const setSelectedFileDownloadProgressAttributes =
|
||||
setFilesDownloadProgressAttributesCreator(
|
||||
t("files_count", { count: files.length }),
|
||||
);
|
||||
await downloadSelectedFiles(
|
||||
await downloadAndSaveFiles(
|
||||
files,
|
||||
setSelectedFileDownloadProgressAttributes,
|
||||
t("files_count", { count: files.length }),
|
||||
onAddSaveGroup,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Head from "next/head";
|
||||
import React from "react";
|
||||
import { haveWindow } from "../env";
|
||||
import { albumsAppOrigin, isCustomAlbumsAppOrigin } from "../origins";
|
||||
|
||||
interface CustomHeadProps {
|
||||
title: string;
|
||||
@@ -23,3 +25,62 @@ export const CustomHead: React.FC<CustomHeadProps> = ({ title }) => (
|
||||
<meta name="referrer" content="strict-origin-when-cross-origin" />
|
||||
</Head>
|
||||
);
|
||||
|
||||
/**
|
||||
* A static SSR-ed variant of {@link CustomHead} for use with the albums app
|
||||
* deployed on production Ente instances for link previews.
|
||||
*
|
||||
* In particular,
|
||||
*
|
||||
* - Any client side modifications to the document's head will be too late for
|
||||
* use by the link previews, so the contents of this need to part of the
|
||||
* static HTML.
|
||||
*
|
||||
* - "og:image" needs to be an absolute URL.
|
||||
*
|
||||
* To avoid getting in the way of self hosters, we do a deployment URL check
|
||||
* before inlining this into the build.
|
||||
*/
|
||||
export const CustomHeadAlbums: React.FC = () => (
|
||||
<Head>
|
||||
<title>Ente Photos</title>
|
||||
<link rel="icon" href="/images/favicon.png" type="image/png" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Safely store and share your best moments"
|
||||
/>
|
||||
<meta
|
||||
name="og:image"
|
||||
content="https://albums.ente.io/images/preview.jpg"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="referrer" content="strict-origin-when-cross-origin" />
|
||||
</Head>
|
||||
);
|
||||
|
||||
/**
|
||||
* A convenience fan out to conditionally show one of {@link CustomHead} or
|
||||
* {@link CustomHeadAlbums}.
|
||||
*
|
||||
* 1. This component defaults to {@link CustomHeadAlbums} during SSR unless a
|
||||
* custom endpoint is defined.
|
||||
*
|
||||
* 2. Currently the photos and albums app use the same code. During SSR this
|
||||
* uses the albums variant, and then does a client side update to the photos
|
||||
* head when it detects that the origin it is being served on is not the
|
||||
* albums origin.
|
||||
*
|
||||
* The current content of the head is such that it sort of works for both photos
|
||||
* and public albums, so the client side update is just an enhancement. We
|
||||
* should not need this component when the photos and public albums app split.
|
||||
*/
|
||||
export const CustomHeadPhotosOrAlbums: React.FC<CustomHeadProps> = ({
|
||||
title,
|
||||
}) =>
|
||||
isCustomAlbumsAppOrigin ||
|
||||
(haveWindow() &&
|
||||
new URL(window.location.href).origin != albumsAppOrigin()) ? (
|
||||
<CustomHead {...{ title }} />
|
||||
) : (
|
||||
<CustomHeadAlbums />
|
||||
);
|
||||
|
||||
@@ -685,5 +685,5 @@
|
||||
"person_favorites": "مفضلات {{name}}",
|
||||
"shared_favorites": "",
|
||||
"added_by_name": "أضيفت بواسطة {{name}}",
|
||||
"unowned_files_not_processed": ""
|
||||
"unowned_files_not_processed": "لم تتم معالجة الملفات المضافة من قبل مستخدمين آخرين"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"intro_slide_2_title": "",
|
||||
"intro_slide_2": "",
|
||||
"intro_slide_3_title": "",
|
||||
"intro_slide_3": "",
|
||||
"login": "",
|
||||
"sign_up": "",
|
||||
"intro_slide_3": "اندروید، آیاواس، وب، رایانه رومیزی",
|
||||
"login": "ورود",
|
||||
"sign_up": "ثبت نام",
|
||||
"new_to_ente": "",
|
||||
"existing_user": "",
|
||||
"enter_email": "",
|
||||
@@ -16,27 +16,27 @@
|
||||
"email_already_registered": "",
|
||||
"email_sent": "",
|
||||
"check_inbox_hint": "",
|
||||
"verification_code": "",
|
||||
"resend_code": "",
|
||||
"verify": "",
|
||||
"send_otp": "",
|
||||
"generic_error": "",
|
||||
"generic_error_retry": "",
|
||||
"verification_code": "کد تایید",
|
||||
"resend_code": "ارسال مجدد کد",
|
||||
"verify": "تایید",
|
||||
"send_otp": "ارسال رمز یکبار مصرف",
|
||||
"generic_error": "یک مشکلی پیش آمده",
|
||||
"generic_error_retry": "مشکلی پیش آمده، لطفا دوباره تلاش کنید",
|
||||
"invalid_code_error": "",
|
||||
"expired_code_error": "",
|
||||
"status_sending": "",
|
||||
"status_sent": "",
|
||||
"password": "",
|
||||
"link_password_description": "",
|
||||
"unlock": "",
|
||||
"set_password": "",
|
||||
"sign_in": "",
|
||||
"incorrect_password": "",
|
||||
"incorrect_password_or_no_account": "",
|
||||
"expired_code_error": "کد تایید شما باطل شد",
|
||||
"status_sending": "در حال ارسال...",
|
||||
"status_sent": "ارسال شد!",
|
||||
"password": "رمز عبور",
|
||||
"link_password_description": "رمز عبور خودرا جهت باز شدن آلبوم بنویسید",
|
||||
"unlock": "بازکردن",
|
||||
"set_password": "تنظیم رمز عبور",
|
||||
"sign_in": "ورود",
|
||||
"incorrect_password": "رمز عبور نادرست",
|
||||
"incorrect_password_or_no_account": "رمز عبور نادرست یا ایمیل ثبت نام نشده",
|
||||
"pick_password_hint": "",
|
||||
"pick_password_caution": "",
|
||||
"key_generation_in_progress": "",
|
||||
"confirm_password": "",
|
||||
"confirm_password": "تایید رمز عبور",
|
||||
"referral_source_hint": "",
|
||||
"referral_source_info": "",
|
||||
"password_mismatch_error": "",
|
||||
@@ -46,12 +46,12 @@
|
||||
"new_album": "",
|
||||
"create_albums": "",
|
||||
"album_name": "",
|
||||
"close": "",
|
||||
"yes": "",
|
||||
"no": "",
|
||||
"nothing_here": "",
|
||||
"upload": "",
|
||||
"import": "",
|
||||
"close": "بستن",
|
||||
"yes": "بله",
|
||||
"no": "خیر",
|
||||
"nothing_here": "هیچی درحال حاضر اینجا نیست",
|
||||
"upload": "بارگذاری",
|
||||
"import": "وارد کردن",
|
||||
"add_photos": "",
|
||||
"add_more_photos": "",
|
||||
"add_photos_count_one": "",
|
||||
@@ -79,11 +79,11 @@
|
||||
"mouse_scroll": "",
|
||||
"pan": "",
|
||||
"pinch": "",
|
||||
"drag": "",
|
||||
"tap_inside_image": "",
|
||||
"tap_outside_image": "",
|
||||
"shortcuts": "",
|
||||
"show_shortcuts": "",
|
||||
"drag": "کشیدن",
|
||||
"tap_inside_image": "زدن در داخل تصویر",
|
||||
"tap_outside_image": "زدن در بیرون تصویر",
|
||||
"shortcuts": "میانبرها",
|
||||
"show_shortcuts": "نمایش میانبرها",
|
||||
"zoom_preset": "",
|
||||
"toggle_controls": "",
|
||||
"toggle_live": "",
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
"ok": "OK",
|
||||
"success": "Sucesso",
|
||||
"error": "Erro",
|
||||
"note": "",
|
||||
"note": "Nota",
|
||||
"offline_message": "Você está sem internet, as memórias em cache estão sendo exibidas",
|
||||
"install": "Instalar",
|
||||
"install_mobile_app": "Instale nosso aplicativo para <a>Android</a> ou <b>iOS</b> para copiar todas suas fotos com segurança",
|
||||
@@ -685,5 +685,5 @@
|
||||
"person_favorites": "Favoritos de {{name}}",
|
||||
"shared_favorites": "Favoritos compartilhados",
|
||||
"added_by_name": "Adicionado por {{name}}",
|
||||
"unowned_files_not_processed": ""
|
||||
"unowned_files_not_processed": "Não processou os arquivos adicionados por outros usuários"
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"sign_up": "Регистрация",
|
||||
"new_to_ente": "Новенький в Ente",
|
||||
"existing_user": "Существующий пользователь",
|
||||
"enter_email": "Введите ваш email адрес",
|
||||
"invalid_email_error": "Введите действительный email адрес",
|
||||
"enter_email": "Введите адрес электронной почты",
|
||||
"invalid_email_error": "Введите действительный адрес электронной почты",
|
||||
"required": "Обязательное поле",
|
||||
"email_not_registered": "Такой email не зарегистрирован",
|
||||
"email_already_registered": "Такой email уже зарегистрирован",
|
||||
@@ -32,7 +32,7 @@
|
||||
"set_password": "Установить пароль",
|
||||
"sign_in": "Зарегистрироваться",
|
||||
"incorrect_password": "Неверный пароль",
|
||||
"incorrect_password_or_no_account": "",
|
||||
"incorrect_password_or_no_account": "Неверный пароль или электронная почта не зарегистрирована",
|
||||
"pick_password_hint": "Пожалуйста, введите пароль, который мы можем использовать для шифрования ваших данных",
|
||||
"pick_password_caution": "Мы не храним ваш пароль, поэтому, если вы его забудете, <strong>мы ничем не сможем вам помочь</strong>для восстановления ваших данных без пароля.",
|
||||
"key_generation_in_progress": "Генерируем ключи шифрования...",
|
||||
@@ -40,7 +40,7 @@
|
||||
"referral_source_hint": "Как вы узнали о Ente? (необязательно)",
|
||||
"referral_source_info": "Будет полезно, если вы укажете, где вы узнали о нас, так как мы не отслеживаем установки приложения!",
|
||||
"password_mismatch_error": "Пароли не совпадают",
|
||||
"show_or_hide_password": "",
|
||||
"show_or_hide_password": "Показать или скрыть пароль",
|
||||
"welcome_to_ente_title": "Добро пожаловать в <a/>",
|
||||
"welcome_to_ente_subtitle": "Сквозное зашифрованное хранение фотографий и общий доступ к ним",
|
||||
"new_album": "Новый альбом",
|
||||
@@ -59,11 +59,11 @@
|
||||
"select_photos": "Выбрать фотографии",
|
||||
"file_upload": "Загрузка файла",
|
||||
"preparing": "Подготовка",
|
||||
"processed_counts": "",
|
||||
"upload_reading_metadata_files": "",
|
||||
"processed_counts": "{{count, number}} / {{total, number}}",
|
||||
"upload_reading_metadata_files": "Чтение файлов метаданных",
|
||||
"upload_cancelling": "Отмена оставшихся загрузок",
|
||||
"upload_done": "",
|
||||
"upload_skipped": "",
|
||||
"upload_done": "{{count, number}} загружено",
|
||||
"upload_skipped": "{{count, number}} пропущено",
|
||||
"initial_load_delay_warning": "Первая загрузка может занять некоторое время",
|
||||
"no_account": "У меня нет учетной записи",
|
||||
"existing_account": "Уже есть аккаунт",
|
||||
@@ -74,28 +74,28 @@
|
||||
"download_favorites": "Скачать избранные",
|
||||
"download_uncategorized": "Скачать без категорий",
|
||||
"download_hidden_items": "Скачать скрытые элементы",
|
||||
"audio": "",
|
||||
"more": "",
|
||||
"mouse_scroll": "",
|
||||
"pan": "",
|
||||
"pinch": "",
|
||||
"drag": "",
|
||||
"tap_inside_image": "",
|
||||
"tap_outside_image": "",
|
||||
"shortcuts": "",
|
||||
"show_shortcuts": "",
|
||||
"zoom_preset": "",
|
||||
"toggle_controls": "",
|
||||
"toggle_live": "",
|
||||
"toggle_audio": "",
|
||||
"toggle_favorite": "",
|
||||
"toggle_archive": "",
|
||||
"view_info": "",
|
||||
"audio": "Аудио",
|
||||
"more": "Ещё",
|
||||
"mouse_scroll": "Прокрутка мышью",
|
||||
"pan": "Pan",
|
||||
"pinch": "Pinch",
|
||||
"drag": "Drag",
|
||||
"tap_inside_image": "Нажмите внутри изображения",
|
||||
"tap_outside_image": "Нажмите снаружи изображения",
|
||||
"shortcuts": "Ярлыки",
|
||||
"show_shortcuts": "Показать ярлыки",
|
||||
"zoom_preset": "Предустановленный масштаб",
|
||||
"toggle_controls": "Переключить управление",
|
||||
"toggle_live": "Переключить прямую трансляцию",
|
||||
"toggle_audio": "Переключить аудио",
|
||||
"toggle_favorite": "Переключить избранное",
|
||||
"toggle_archive": "Переключить архив",
|
||||
"view_info": "Посмотреть информацию",
|
||||
"copy_as_png": "Скопировать как PNG",
|
||||
"toggle_fullscreen": "Полноэкранный режим",
|
||||
"exit_fullscreen": "",
|
||||
"go_fullscreen": "",
|
||||
"zoom": "",
|
||||
"exit_fullscreen": "Выйти из полноэкранного режима",
|
||||
"go_fullscreen": "Перейти в полноэкранный режим",
|
||||
"zoom": "Увеличить",
|
||||
"play": "Воспроизведение",
|
||||
"pause": "Пауза",
|
||||
"previous": "Предыдущий",
|
||||
@@ -132,7 +132,7 @@
|
||||
"password_changed_elsewhere": "Пароль изменен в другом месте",
|
||||
"password_changed_elsewhere_message": "Пожалуйста, войдите снова на этом устройстве, чтобы использовать новый пароль для аутентификации.",
|
||||
"go_back": "Вернуться назад",
|
||||
"account": "",
|
||||
"account": "Аккаунт",
|
||||
"recovery_key": "Ключ восстановления",
|
||||
"do_this_later": "Сделать позже",
|
||||
"save_key": "Сохранить ключ",
|
||||
@@ -148,9 +148,9 @@
|
||||
"no_recovery_key_message": "Из-за природы нашего сквозного протокола шифрования ваши данные не могут быть расшифрованы без вашего пароля или ключа восстановления",
|
||||
"no_two_factor_recovery_key_message": "Пожалуйста, отправьте электронное письмо на адрес <a>{{emailID}}</a> с вашего зарегистрированного адреса электронной почты",
|
||||
"contact_support": "Связаться с поддержкой",
|
||||
"help": "",
|
||||
"ente_help": "",
|
||||
"blog": "",
|
||||
"help": "Помощь",
|
||||
"ente_help": "Ente Помощь",
|
||||
"blog": "Блог",
|
||||
"request_feature": "Запросить функцию",
|
||||
"support": "Поддержка",
|
||||
"cancel": "Отменить",
|
||||
@@ -158,11 +158,11 @@
|
||||
"logout_message": "Вы уверены, что хотите выйти?",
|
||||
"delete_account": "Удалить аккаунт",
|
||||
"delete_account_manually_message": "<p>Пожалуйста, отправьте письмо по адресу <a>{{emailID}}</a> с вашего зарегистрированного адреса электронной почты.</p><p>Ваш запрос будет обработан в течение 72 часов</p>",
|
||||
"change_email": "Изменить email адрес",
|
||||
"change_email": "Изменить адрес электронной почты",
|
||||
"ok": "ОК",
|
||||
"success": "Успешно",
|
||||
"error": "Ошибка",
|
||||
"note": "",
|
||||
"note": "Заметка",
|
||||
"offline_message": "Вы не в сети, кэшированные воспоминания отображаются",
|
||||
"install": "Устанавливать",
|
||||
"install_mobile_app": "Установите наше приложение <a>Android</a> или <b>iOS</b> для автоматического резервного копирования всех ваших фотографий",
|
||||
@@ -226,11 +226,11 @@
|
||||
"delete_photos": "Удалить фото",
|
||||
"keep_photos": "Оставить фото",
|
||||
"share_album": "Поделиться альбомом",
|
||||
"sharing_with_self": "",
|
||||
"sharing_already_shared": "",
|
||||
"sharing_with_self": "Вы не можете поделиться с самим собой",
|
||||
"sharing_already_shared": "Вы уже поделились этим с {{email}}",
|
||||
"sharing_album_not_allowed": "Делиться альбомом запрещено",
|
||||
"sharing_disabled_for_free_accounts": "Совместное использование отключено для бесплатных аккаунтов",
|
||||
"sharing_user_does_not_exist": "",
|
||||
"sharing_user_does_not_exist": "Пользователь с такой электронной почтой не найден",
|
||||
"search": "Поиск",
|
||||
"search_results": "Результаты поиска",
|
||||
"no_results": "Ничего не найдено",
|
||||
@@ -246,9 +246,9 @@
|
||||
"terms_and_conditions": "Я согласен с тем, что <a> условия </a> и <b>политика конфиденциальности</b>",
|
||||
"people": "Люди",
|
||||
"indexing_scheduled": "Индексация запланирована...",
|
||||
"indexing_photos": "",
|
||||
"indexing_fetching": "",
|
||||
"indexing_people": "",
|
||||
"indexing_photos": "Обновление индексов...",
|
||||
"indexing_fetching": "Синхронизация индексов...",
|
||||
"indexing_people": "Синхронизация людей...",
|
||||
"syncing_wait": "Синхронизация...",
|
||||
"people_empty_too_few": "Люди будут показаны здесь, когда будет достаточно фотографий человека",
|
||||
"unnamed_person": "Безымянный человек",
|
||||
@@ -371,7 +371,7 @@
|
||||
"leave_shared_album": "Да, уходи",
|
||||
"confirm_remove_message": "Выбранные элементы будут удалены из этого альбома. Элементы, которые есть только в этом альбоме, будут перемещены в раздел Без категории.",
|
||||
"confirm_remove_incl_others_message": "Некоторые из удаляемых вами элементов были добавлены другими пользователями, и вы потеряете к ним доступ.",
|
||||
"oldest": "Старейший",
|
||||
"oldest": "Самые старые",
|
||||
"last_updated": "Последнее обновление",
|
||||
"name": "Имя",
|
||||
"fix_creation_time": "Назначьте время",
|
||||
@@ -388,7 +388,7 @@
|
||||
"sharing_details": "Обмен подробностями",
|
||||
"modify_sharing": "Изменить общий доступ",
|
||||
"add_collaborators": "Добавление соавторов",
|
||||
"add_new_email": "Добавить новый email адрес",
|
||||
"add_new_email": "Добавить новую электронную почту",
|
||||
"shared_with_people_count_zero": "Делитесь с конкретными людьми",
|
||||
"shared_with_people_count_one": "Совместно с 1 человеком",
|
||||
"shared_with_people_count": "Поделился с {{count, number}} люди",
|
||||
@@ -495,8 +495,8 @@
|
||||
"stop_watching_folder_message": "Ваши существующие файлы не будут удалены, но Ente прекратит автоматическое обновление связанного альбома Ente при внесении изменений в эту папку.",
|
||||
"yes_stop": "Да, остановись",
|
||||
"change_folder": "Изменить папку",
|
||||
"view_logs": "",
|
||||
"view_logs_message": "",
|
||||
"view_logs": "Просмотреть логи",
|
||||
"view_logs_message": "<p>При этом будут показаны журналы отладки, которые вы можете отправить нам по электронной почте, чтобы помочь в устранении вашей проблемы.</p><p>Обратите внимание, что будут указаны имена файлов, которые помогут отслеживать проблемы с конкретными файлами.</p>",
|
||||
"weak_device_hint": "Используемый вами веб-браузер недостаточно мощный, чтобы зашифровать ваши фотографии. Пожалуйста, попробуйте войти в Ente на своем компьютере или загрузить мобильное/настольное приложение Ente.",
|
||||
"drag_and_drop_hint": "Или перетащите в основное окно",
|
||||
"authenticate": "Проверка подлинности",
|
||||
@@ -595,8 +595,8 @@
|
||||
"image": "Изображение",
|
||||
"video": "Видео",
|
||||
"live_photo": "Живое фото",
|
||||
"live": "",
|
||||
"edit_image": "",
|
||||
"live": "Прямая трансляция",
|
||||
"edit_image": "Редактировать изображение",
|
||||
"photo_editor": "Редактор фото",
|
||||
"confirm_editor_close": "Вы уверены, что хотите закрыть редактор?",
|
||||
"confirm_editor_close_message": "Загрузите отредактированное изображение или сохраните копию в ente, чтобы сохранить внесенные изменения.",
|
||||
@@ -625,9 +625,9 @@
|
||||
"reset": "Сбросить",
|
||||
"faster_upload": "Более быстрая загрузка данных",
|
||||
"faster_upload_description": "Загрузка маршрута через близлежащие серверы",
|
||||
"open_ente_on_startup": "",
|
||||
"open_ente_on_startup": "Открывать Enter при запуске",
|
||||
"cast_album_to_tv": "Воспроизвести альбом на ТВ",
|
||||
"cast_to_tv": "",
|
||||
"cast_to_tv": "Воспроизвести на ТВ",
|
||||
"enter_cast_pin_code": "Введите код, который вы видите на экране телевизора ниже, чтобы выполнить сопряжение с этим устройством.",
|
||||
"code": "Код",
|
||||
"pair_device_to_tv": "Сопряжение устройств",
|
||||
@@ -639,7 +639,7 @@
|
||||
"pair_with_pin": "Соединение с помощью булавки",
|
||||
"pair_with_pin_description": "Пара с PIN-кодом работает с любым экраном, на котором вы хотите посмотреть ваш альбом.",
|
||||
"visit_cast_url": "Перейдите на страницу <a>{{url}}</a> на устройстве, которое вы хотите подключить.",
|
||||
"passkeys": "Passkeys",
|
||||
"passkeys": "Ключ доступа",
|
||||
"passkey_fetch_failed": "Не удалось получить ваши ключи.",
|
||||
"manage_passkey": "Управление ключами",
|
||||
"delete_passkey": "Удалить пароль",
|
||||
@@ -675,15 +675,15 @@
|
||||
"server_endpoint": "Конечная точка сервера",
|
||||
"more_information": "Дополнительная информация",
|
||||
"save": "Сохранить",
|
||||
"theme": "",
|
||||
"system": "",
|
||||
"light": "",
|
||||
"dark": "",
|
||||
"streamable_videos": "",
|
||||
"processing_videos_status": "",
|
||||
"share_favorites": "",
|
||||
"person_favorites": "",
|
||||
"shared_favorites": "",
|
||||
"added_by_name": "",
|
||||
"unowned_files_not_processed": ""
|
||||
"theme": "Тема",
|
||||
"system": "Системная",
|
||||
"light": "Светлая",
|
||||
"dark": "Тёмная",
|
||||
"streamable_videos": "Потоковое видео",
|
||||
"processing_videos_status": "Обработка видео...",
|
||||
"share_favorites": "Поделиться избранными",
|
||||
"person_favorites": "{{name}} избранных",
|
||||
"shared_favorites": "Общие избранные",
|
||||
"added_by_name": "Добавлено {{name}}",
|
||||
"unowned_files_not_processed": "Файлы, добавленные другими пользователями, не были обработаны"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"intro_slide_1_title": "Sao lưu riêng tư<br/>cho những kỷ niệm của bạn",
|
||||
"intro_slide_1": "Mã hóa đầu cuối mặc định",
|
||||
"intro_slide_2_title": "Lưu trữ an toàn<br/>tại nơi trú ẩn",
|
||||
"intro_slide_2": "Được thiết kế để tồn tại lâu dài",
|
||||
"intro_slide_1": "Mã hóa đầu cuối theo mặc định",
|
||||
"intro_slide_2_title": "Lưu trữ an toàn<br/>ở hầm trú ẩn hạt nhân",
|
||||
"intro_slide_2": "Được thiết kế để trường tồn",
|
||||
"intro_slide_3_title": "Có sẵn<br/>mọi nơi",
|
||||
"intro_slide_3": "Android, iOS, Web, Desktop",
|
||||
"login": "Đăng nhập",
|
||||
"sign_up": "Đăng ký",
|
||||
"new_to_ente": "Mới đến Ente",
|
||||
"existing_user": "Người dùng hiện tại",
|
||||
"new_to_ente": "Mới dùng Ente",
|
||||
"existing_user": "Đã có tài khoản",
|
||||
"enter_email": "Nhập địa chỉ email",
|
||||
"invalid_email_error": "Nhập một email hợp lệ",
|
||||
"required": "Bắt buộc",
|
||||
"email_not_registered": "Email chưa được đăng kí",
|
||||
"email_already_registered": "Email đã được đăng kí",
|
||||
"email_already_registered": "Email đã được đăng ký",
|
||||
"email_sent": "Mã xác minh đã được gửi đến <a>{{email}}</a>",
|
||||
"check_inbox_hint": "Vui lòng kiểm tra hộp thư đến (và thư rác) để hoàn tất xác minh",
|
||||
"verification_code": "Mã xác minh",
|
||||
@@ -32,15 +32,15 @@
|
||||
"set_password": "Đặt mật khẩu",
|
||||
"sign_in": "Đăng nhập",
|
||||
"incorrect_password": "Mật khẩu không chính xác",
|
||||
"incorrect_password_or_no_account": "",
|
||||
"pick_password_hint": "Vui lòng nhập mật khẩu mà chúng tôi có thể sử dụng để mã hóa dữ liệu của bạn",
|
||||
"pick_password_caution": "Chúng tôi không lưu trữ mật khẩu của bạn, vì vậy nếu bạn quên, <strong>chúng tôi sẽ không thể giúp bạn</strong> khôi phục dữ liệu mà không có khóa khôi phục.",
|
||||
"key_generation_in_progress": "Đang tạo khóa mã hóa...",
|
||||
"incorrect_password_or_no_account": "Sai mật khẩu hoặc email chưa được đăng ký",
|
||||
"pick_password_hint": "Vui lòng nhập một mật khẩu dùng để mã hóa dữ liệu của bạn",
|
||||
"pick_password_caution": "Chúng tôi không lưu trữ mật khẩu của bạn, nên nếu bạn quên, <strong>chúng tôi sẽ không thể giúp bạn </strong>khôi phục dữ liệu nếu không có mã khôi phục.",
|
||||
"key_generation_in_progress": "Đang mã hóa...",
|
||||
"confirm_password": "Xác nhận mật khẩu",
|
||||
"referral_source_hint": "Bạn đã nghe về Ente từ đâu? (tùy chọn)",
|
||||
"referral_source_info": "Chúng tôi không theo dõi cài đặt ứng dụng, sẽ rất hữu ích nếu bạn cho chúng tôi biết bạn đã tìm thấy chúng tôi ở đâu!",
|
||||
"referral_source_hint": "Bạn biết Ente từ đâu? (tùy chọn)",
|
||||
"referral_source_info": "Chúng tôi không theo dõi cài đặt ứng dụng, nên nếu bạn bật mí bạn tìm thấy chúng tôi từ đâu sẽ rất hữu ích!",
|
||||
"password_mismatch_error": "Mật khẩu không khớp",
|
||||
"show_or_hide_password": "",
|
||||
"show_or_hide_password": "Ẩn hoặc hiện mật khẩu",
|
||||
"welcome_to_ente_title": "Chào mừng đến với <a/>",
|
||||
"welcome_to_ente_subtitle": "Lưu trữ và chia sẻ ảnh được mã hóa đầu cuối",
|
||||
"new_album": "Album mới",
|
||||
@@ -53,222 +53,222 @@
|
||||
"upload": "Tải lên",
|
||||
"import": "Nhập",
|
||||
"add_photos": "Thêm ảnh",
|
||||
"add_more_photos": "Thêm nhiều ảnh hơn",
|
||||
"add_more_photos": "Thêm ảnh",
|
||||
"add_photos_count_one": "Thêm 1 mục",
|
||||
"add_photos_count": "Thêm {{count, number}} mục",
|
||||
"select_photos": "Chọn ảnh",
|
||||
"file_upload": "Tải tệp lên",
|
||||
"preparing": "",
|
||||
"processed_counts": "",
|
||||
"upload_reading_metadata_files": "",
|
||||
"upload_cancelling": "Hủy bỏ các tải lên còn lại",
|
||||
"upload_done": "",
|
||||
"upload_skipped": "",
|
||||
"initial_load_delay_warning": "Tải lần đầu có thể mất một chút thời gian",
|
||||
"no_account": "Không có tài khoản",
|
||||
"preparing": "Đang chuẩn bị",
|
||||
"processed_counts": "{{count, number}} / {{total, number}}",
|
||||
"upload_reading_metadata_files": "Đang đọc siêu dữ liệu",
|
||||
"upload_cancelling": "Hủy bỏ các lượt tải lên còn lại",
|
||||
"upload_done": "Đã tải lên {{count, number}}",
|
||||
"upload_skipped": "{{count, number}} bị bỏ qua",
|
||||
"initial_load_delay_warning": "Lần đầu tải có thể mất một ít thời gian",
|
||||
"no_account": "Chưa có tài khoản",
|
||||
"existing_account": "Đã có tài khoản",
|
||||
"create": "Tạo",
|
||||
"files_count": "",
|
||||
"files_count": "{{count, number}} tệp",
|
||||
"download": "Tải xuống",
|
||||
"download_album": "Tải xuống album",
|
||||
"download_favorites": "Tải xuống mục yêu thích",
|
||||
"download_uncategorized": "Tải xuống chưa phân loại",
|
||||
"download_hidden_items": "Tải xuống các mục ẩn",
|
||||
"audio": "",
|
||||
"more": "",
|
||||
"mouse_scroll": "",
|
||||
"pan": "",
|
||||
"pinch": "",
|
||||
"drag": "",
|
||||
"tap_inside_image": "",
|
||||
"tap_outside_image": "",
|
||||
"shortcuts": "",
|
||||
"show_shortcuts": "",
|
||||
"zoom_preset": "",
|
||||
"toggle_controls": "",
|
||||
"toggle_live": "",
|
||||
"toggle_audio": "",
|
||||
"toggle_favorite": "",
|
||||
"toggle_archive": "",
|
||||
"view_info": "",
|
||||
"copy_as_png": "Sao chép dưới dạng PNG",
|
||||
"toggle_fullscreen": "Chuyển đổi chế độ toàn màn hình",
|
||||
"exit_fullscreen": "",
|
||||
"go_fullscreen": "",
|
||||
"zoom": "",
|
||||
"play": "",
|
||||
"pause": "",
|
||||
"download_uncategorized": "Tải xuống mục chưa phân loại",
|
||||
"download_hidden_items": "Tải xuống mục ẩn",
|
||||
"audio": "Âm thanh",
|
||||
"more": "Thêm",
|
||||
"mouse_scroll": "Cuộn chuột",
|
||||
"pan": "Lia qua",
|
||||
"pinch": "Chụm 2 ngón",
|
||||
"drag": "Kéo",
|
||||
"tap_inside_image": "Nhấn lên ảnh",
|
||||
"tap_outside_image": "Nhấn ngoài ảnh",
|
||||
"shortcuts": "Phím tắt",
|
||||
"show_shortcuts": "Hiện phím tắt",
|
||||
"zoom_preset": "Phóng to chi tiết",
|
||||
"toggle_controls": "Bật/tắt điều khiển",
|
||||
"toggle_live": "Bật/tắt Live",
|
||||
"toggle_audio": "Bật/tắt âm thanh",
|
||||
"toggle_favorite": "Thích/bỏ thích",
|
||||
"toggle_archive": "Lưu trữ/bỏ lưu trữ",
|
||||
"view_info": "Xem thông tin",
|
||||
"copy_as_png": "Sao chép dạng PNG",
|
||||
"toggle_fullscreen": "Chế độ toàn màn hình",
|
||||
"exit_fullscreen": "Thoát toàn màn hình",
|
||||
"go_fullscreen": "Toàn màn hình",
|
||||
"zoom": "Thu phóng",
|
||||
"play": "Phát",
|
||||
"pause": "Dừng",
|
||||
"previous": "Trước",
|
||||
"next": "Tiếp theo",
|
||||
"video_seek": "",
|
||||
"quality": "",
|
||||
"auto": "",
|
||||
"original": "",
|
||||
"speed": "",
|
||||
"title_photos": "Ảnh Ente",
|
||||
"title_auth": "Xác thực Ente",
|
||||
"next": "Kế tiếp",
|
||||
"video_seek": "Tua video",
|
||||
"quality": "Chất lượng",
|
||||
"auto": "Tự động",
|
||||
"original": "Gốc",
|
||||
"speed": "Tốc độ",
|
||||
"title_photos": "Ente Photos",
|
||||
"title_auth": "Ente Auth",
|
||||
"title_accounts": "Tài khoản Ente",
|
||||
"upload_first_photo": "Tải lên ảnh đầu tiên của bạn",
|
||||
"import_your_folders": "Nhập các thư mục của bạn",
|
||||
"upload_dropzone_hint": "Thả để sao lưu các tệp của bạn",
|
||||
"watch_folder_dropzone_hint": "Thả để thêm thư mục theo dõi",
|
||||
"trash_files_title": "Xóa tệp?",
|
||||
"import_your_folders": "Nhập thư mục của bạn",
|
||||
"upload_dropzone_hint": "Kéo thả để sao lưu tệp của bạn",
|
||||
"watch_folder_dropzone_hint": "Kéo thả để thêm thư mục theo dõi",
|
||||
"trash_files_title": "Xóa các tệp?",
|
||||
"trash_file_title": "Xóa tệp?",
|
||||
"delete_files_title": "Xóa ngay lập tức?",
|
||||
"delete_files_message": "Các tệp đã chọn sẽ bị xóa vĩnh viễn khỏi tài khoản Ente của bạn.",
|
||||
"selected_count": "{{selected, number}} đã chọn",
|
||||
"selected_and_yours_count": "{{selected, number}} đã chọn {{yours, number}} của bạn",
|
||||
"selected_count": "{{selected, number}} mục đã chọn",
|
||||
"selected_and_yours_count": "{{selected, number}} mục đã chọn, trong đó {{yours, number}} là của bạn",
|
||||
"delete": "Xóa",
|
||||
"favorite": "Yêu thích",
|
||||
"favorite": "Thích",
|
||||
"convert": "Chuyển đổi",
|
||||
"multi_folder_upload": "Phát hiện nhiều thư mục",
|
||||
"upload_to_choice": "Bạn có muốn tải chúng vào",
|
||||
"upload_to_choice": "Bạn có muốn tải chúng thành",
|
||||
"upload_to_single_album": "Một album duy nhất",
|
||||
"upload_to_album_per_folder": "Album riêng biệt",
|
||||
"upload_to_album_per_folder": "Các album riêng biệt",
|
||||
"session_expired": "Phiên đã hết hạn",
|
||||
"session_expired_message": "Phiên của bạn đã hết hạn, vui lòng đăng nhập lại để tiếp tục",
|
||||
"password_generation_failed": "Trình duyệt của bạn không thể tạo một khóa mạnh đáp ứng tiêu chuẩn mã hóa của Ente, vui lòng thử sử dụng ứng dụng di động hoặc trình duyệt khác",
|
||||
"password_generation_failed": "Trình duyệt của bạn không thể tạo một mã mạnh đáp ứng tiêu chuẩn mã hóa của Ente, vui lòng dùng ứng dụng di động hoặc trình duyệt khác",
|
||||
"change_password": "Đổi mật khẩu",
|
||||
"password_changed_elsewhere": "Mật khẩu đã được thay đổi ở nơi khác",
|
||||
"password_changed_elsewhere_message": "Vui lòng đăng nhập lại trên thiết bị này để sử dụng mật khẩu mới của bạn để xác thực.",
|
||||
"password_changed_elsewhere_message": "Vui lòng đăng nhập lại trên thiết bị này và dùng mật khẩu mới của bạn.",
|
||||
"go_back": "Quay lại",
|
||||
"account": "",
|
||||
"recovery_key": "Khóa khôi phục",
|
||||
"do_this_later": "Làm điều này sau",
|
||||
"save_key": "Lưu khóa",
|
||||
"recovery_key_description": "Nếu bạn quên mật khẩu của mình, cách duy nhất để khôi phục dữ liệu của bạn là với khóa này.",
|
||||
"key_not_stored_note": "Chúng tôi không lưu trữ khóa này, vì vậy hãy lưu nó ở một nơi an toàn",
|
||||
"recovery_key_generation_failed": "Mã khôi phục không thể được tạo, vui lòng thử lại",
|
||||
"account": "Tài khoản",
|
||||
"recovery_key": "Mã khôi phục",
|
||||
"do_this_later": "Để sau",
|
||||
"save_key": "Lưu mã",
|
||||
"recovery_key_description": "Nếu bạn quên mật khẩu, cách duy nhất để khôi phục dữ liệu của bạn là dùng mã này.",
|
||||
"key_not_stored_note": "Chúng tôi không lưu trữ mã này, nên hãy lưu nó ở một nơi an toàn",
|
||||
"recovery_key_generation_failed": "Không tạo được mã khôi phục, vui lòng thử lại",
|
||||
"forgot_password": "Quên mật khẩu",
|
||||
"recover_account": "Khôi phục tài khoản",
|
||||
"recover": "Khôi phục",
|
||||
"no_recovery_key_title": "Không có khóa khôi phục?",
|
||||
"incorrect_recovery_key": "Khóa khôi phục không chính xác",
|
||||
"sorry": "Xin lỗi",
|
||||
"no_recovery_key_message": "Do tính chất của giao thức mã hóa đầu cuối của chúng tôi, dữ liệu của bạn không thể được giải mã mà không có mật khẩu hoặc khóa khôi phục của bạn",
|
||||
"no_recovery_key_title": "Không có mã khôi phục?",
|
||||
"incorrect_recovery_key": "Mã khôi phục không chính xác",
|
||||
"sorry": "Rất tiếc",
|
||||
"no_recovery_key_message": "Do tính chất của giao thức mã hóa đầu cuối, không thể giải mã dữ liệu của bạn mà không có mật khẩu hoặc mã khôi phục",
|
||||
"no_two_factor_recovery_key_message": "Vui lòng gửi email đến <a>{{emailID}}</a> từ địa chỉ email đã đăng ký của bạn",
|
||||
"contact_support": "Liên hệ hỗ trợ",
|
||||
"help": "",
|
||||
"ente_help": "",
|
||||
"blog": "",
|
||||
"request_feature": "Yêu cầu tính năng",
|
||||
"help": "Trợ giúp",
|
||||
"ente_help": "Trợ giúp Ente",
|
||||
"blog": "Blog",
|
||||
"request_feature": "Đề xuất tính năng",
|
||||
"support": "Hỗ trợ",
|
||||
"cancel": "Hủy",
|
||||
"logout": "Đăng xuất",
|
||||
"logout_message": "Bạn có chắc chắn muốn đăng xuất không?",
|
||||
"logout_message": "Bạn có chắc muốn đăng xuất không?",
|
||||
"delete_account": "Xóa tài khoản",
|
||||
"delete_account_manually_message": "<p>Vui lòng gửi email đến <a>{{emailID}}</a> từ địa chỉ email đã đăng ký của bạn.</p><p>Yêu cầu của bạn sẽ được xử lý trong vòng 72 giờ.</p>",
|
||||
"change_email": "Đổi email",
|
||||
"ok": "OK",
|
||||
"success": "Thành công",
|
||||
"error": "Lỗi",
|
||||
"note": "",
|
||||
"offline_message": "Bạn đang ngoại tuyến, các kỷ niệm đã được lưu vào bộ nhớ cache đang được hiển thị",
|
||||
"note": "Ghi chú",
|
||||
"offline_message": "Bạn đang ngoại tuyến, các kỷ niệm hiển thị là từ bộ nhớ đệm",
|
||||
"install": "Cài đặt",
|
||||
"install_mobile_app": "Cài đặt ứng dụng <a>Android</a> hoặc <b>iOS</b> của chúng tôi để tự động sao lưu tất cả ảnh của bạn",
|
||||
"download_app": "Tải xuống ứng dụng desktop",
|
||||
"download_app_message": "Xin lỗi, thao tác này hiện chỉ được hỗ trợ trên ứng dụng desktop của chúng tôi",
|
||||
"download_app": "Tải xuống ứng dụng máy tính",
|
||||
"download_app_message": "Rất tiếc, thao tác này hiện chỉ hỗ trợ trên ứng dụng máy tính",
|
||||
"subscription": "Gói đăng ký",
|
||||
"manage_payment_method": "Quản lý phương thức thanh toán",
|
||||
"manage_family": "Quản lý gia đình",
|
||||
"family_plan": "Gói gia đình",
|
||||
"leave_family_plan": "Rời khỏi gói gia đình",
|
||||
"leave": "Rời",
|
||||
"leave_family_plan_confirm": "Bạn có chắc chắn muốn rời khỏi gói gia đình không?",
|
||||
"leave_family_plan_confirm": "Bạn có chắc muốn rời khỏi gói gia đình không?",
|
||||
"choose_plan": "Chọn gói của bạn",
|
||||
"manage_plan": "Quản lý đăng ký của bạn",
|
||||
"current_usage": "Sử dụng hiện tại là <strong>{{usage}}</strong>",
|
||||
"two_months_free": "Nhận 2 tháng miễn phí với các gói hàng năm",
|
||||
"free_plan_option": "Tiếp tục với gói miễn phí",
|
||||
"free_plan_description": "{{storage}} miễn phí mãi mãi",
|
||||
"manage_plan": "Quản lý gói đăng ký",
|
||||
"current_usage": "Hiện dùng <strong>{{usage}}</strong>",
|
||||
"two_months_free": "Nhận 2 tháng miễn phí với các gói theo năm",
|
||||
"free_plan_option": "Dùng tiếp gói miễn phí",
|
||||
"free_plan_description": "{{storage}} miễn phí vĩnh viễn",
|
||||
"active": "Hoạt động",
|
||||
"subscription_info_free": "Bạn đang ở gói miễn phí",
|
||||
"subscription_info_family": "Bạn đang ở gói gia đình do",
|
||||
"subscription_info_expired": "Gói đăng ký của bạn đã hết hạn, vui lòng <a>gia hạn</a>",
|
||||
"subscription_info_renewal_cancelled": "Gói đăng ký của bạn sẽ bị hủy vào {{date, date}}",
|
||||
"subscription_info_storage_quota_exceeded": "Bạn đã vượt quá hạn mức lưu trữ của mình, vui lòng <a>nâng cấp</a>",
|
||||
"subscription_info_free": "Bạn đang dùng gói miễn phí",
|
||||
"subscription_info_family": "Bạn đang dùng gói gia đình của",
|
||||
"subscription_info_expired": "Gói của bạn đã hết hạn, vui lòng <a>gia hạn</a>",
|
||||
"subscription_info_renewal_cancelled": "Gói của bạn sẽ bị hủy vào {{date, date}}",
|
||||
"subscription_info_storage_quota_exceeded": "Bạn đã vượt hạn mức lưu trữ của mình, vui lòng <a>nâng cấp</a>",
|
||||
"subscription_status_renewal_active": "Gia hạn vào {{date, date}}",
|
||||
"subscription_status_renewal_cancelled": "Kết thúc vào {{date, date}}",
|
||||
"add_on_valid_till": "Gói bổ sung {{storage}} của bạn có hiệu lực đến {{date, date}}",
|
||||
"subscription_expired": "Gói đăng ký đã hết hạn",
|
||||
"storage_quota_exceeded": "Đã vượt quá giới hạn lưu trữ",
|
||||
"subscription_purchase_success": "<p>Chúng tôi đã nhận được thanh toán của bạn</p><p>Gói đăng ký của bạn có hiệu lực đến <strong>{{date, date}}</strong></p>",
|
||||
"storage_quota_exceeded": "Đã vượt hạn mức lưu trữ",
|
||||
"subscription_purchase_success": "<p>Chúng tôi đã nhận được thanh toán</p><p>Gói của bạn có hiệu lực đến <strong>{{date, date}}</strong></p>",
|
||||
"subscription_purchase_cancelled": "Giao dịch của bạn đã bị hủy, vui lòng thử lại nếu bạn muốn đăng ký",
|
||||
"subscription_purchase_failed": "Giao dịch đăng ký không thành công, vui lòng thử lại",
|
||||
"subscription_verification_error": "Xác minh gói đăng ký không thành công",
|
||||
"update_payment_method_message": "Chúng tôi xin lỗi, thanh toán không thành công khi chúng tôi cố gắng tính phí thẻ của bạn, vui lòng cập nhật phương thức thanh toán của bạn và thử lại",
|
||||
"subscription_purchase_failed": "Giao dịch không thành công, vui lòng thử lại",
|
||||
"subscription_verification_error": "Xác minh gói không thành công",
|
||||
"update_payment_method_message": "Rất tiếc, thẻ của bạn thanh toán không thành công, vui lòng cập nhật phương thức thanh toán và thử lại",
|
||||
"payment_method_authentication_failed": "Chúng tôi không thể xác thực phương thức thanh toán của bạn. Vui lòng chọn phương thức thanh toán khác và thử lại",
|
||||
"update_payment_method": "Cập nhật phương thức thanh toán",
|
||||
"monthly": "Hàng tháng",
|
||||
"yearly": "Hàng năm",
|
||||
"month_short": "th",
|
||||
"monthly": "Theo tháng",
|
||||
"yearly": "Theo năm",
|
||||
"month_short": "tháng",
|
||||
"year": "năm",
|
||||
"update_subscription": "Thay đổi gói",
|
||||
"update_subscription_title": "Xác nhận thay đổi gói",
|
||||
"update_subscription_message": "Bạn có chắc chắn muốn thay đổi gói của mình không?",
|
||||
"cancel_subscription": "Hủy đăng ký",
|
||||
"cancel_subscription_message": "<p>Tất cả dữ liệu của bạn sẽ bị xóa khỏi máy chủ của chúng tôi vào cuối kỳ thanh toán này.</p><p>Bạn có chắc chắn muốn hủy đăng ký của mình không?</p>",
|
||||
"cancel_subscription_with_addon_message": "<p>Bạn có chắc chắn muốn hủy đăng ký của mình không?</p>",
|
||||
"subscription_cancel_success": "Hủy đăng ký thành công",
|
||||
"reactivate_subscription": "Kích hoạt lại đăng ký",
|
||||
"reactivate_subscription_message": "Khi được kích hoạt lại, bạn sẽ bị tính phí vào {{date, date}}",
|
||||
"subscription_activate_success": "Kích hoạt đăng ký thành công",
|
||||
"update_subscription_message": "Bạn có chắc muốn thay đổi gói của mình không?",
|
||||
"cancel_subscription": "Hủy gói",
|
||||
"cancel_subscription_message": "<p>Toàn bộ dữ liệu của bạn sẽ bị xóa khỏi máy chủ của chúng tôi vào cuối kỳ thanh toán này.</p><p>Bạn có chắc muốn hủy gói của mình không?</p>",
|
||||
"cancel_subscription_with_addon_message": "<p>Bạn có chắc muốn hủy gói của mình không?</p>",
|
||||
"subscription_cancel_success": "Hủy gói thành công",
|
||||
"reactivate_subscription": "Kích hoạt lại gói",
|
||||
"reactivate_subscription_message": "Khi kích hoạt lại, bạn sẽ bị tính phí vào {{date, date}}",
|
||||
"subscription_activate_success": "Kích hoạt gói thành công ",
|
||||
"thank_you": "Cảm ơn bạn",
|
||||
"cancel_subscription_on_mobile": "Hủy đăng ký di động",
|
||||
"cancel_subscription_on_mobile_message": "Vui lòng hủy đăng ký của bạn từ ứng dụng di động để kích hoạt một đăng ký ở đây",
|
||||
"mail_to_manage_subscription": "Vui lòng liên hệ với chúng tôi tại <a>{{emailID}}</a> để quản lý đăng ký của bạn",
|
||||
"cancel_subscription_on_mobile": "Hủy gói trên điện thoại",
|
||||
"cancel_subscription_on_mobile_message": "Vui lòng hủy gói của bạn từ ứng dụng di động để kích hoạt một gói ở đây",
|
||||
"mail_to_manage_subscription": "Vui lòng liên hệ với chúng tôi qua <a>{{emailID}}</a> để quản lý gói của bạn",
|
||||
"rename": "Đổi tên",
|
||||
"rename_file": "Đổi tên tệp",
|
||||
"rename_album": "Đổi tên album",
|
||||
"delete_album": "Xóa album",
|
||||
"delete_album_title": "Xóa album?",
|
||||
"delete_album_message": "Có xóa các bức ảnh (và video) có trong album này từ <a>tất cả</a> các album khác mà chúng là một phần không?",
|
||||
"delete_album_message": "Xóa luôn các tấm ảnh (và video) có trong album này khỏi <a>toàn bộ</a> album khác cũng đang chứa chúng?",
|
||||
"delete_photos": "Xóa ảnh",
|
||||
"keep_photos": "Giữ ảnh",
|
||||
"share_album": "Chia sẻ album",
|
||||
"sharing_with_self": "",
|
||||
"sharing_already_shared": "",
|
||||
"sharing_with_self": "Bạn không thể chia sẻ với chính mình",
|
||||
"sharing_already_shared": "Bạn đã chia sẻ với {{email}} rồi",
|
||||
"sharing_album_not_allowed": "Chia sẻ album không được phép",
|
||||
"sharing_disabled_for_free_accounts": "Chia sẻ bị vô hiệu hóa cho các tài khoản miễn phí",
|
||||
"sharing_user_does_not_exist": "",
|
||||
"sharing_disabled_for_free_accounts": "Tài khoản miễn phí không thể chia sẻ",
|
||||
"sharing_user_does_not_exist": "Không tìm thấy người dùng với email này",
|
||||
"search": "Tìm kiếm",
|
||||
"search_results": "Kết quả tìm kiếm",
|
||||
"no_results": "Không tìm thấy kết quả",
|
||||
"search_hint": "Tìm kiếm album, ngày tháng, mô tả, ...",
|
||||
"search_hint": "Tìm album, ngày chụp, mô tả,...",
|
||||
"album": "Album",
|
||||
"date": "Ngày",
|
||||
"description": "Mô tả",
|
||||
"file_type": "Loại tệp",
|
||||
"magic": "Ma thuật",
|
||||
"photos_count_zero": "Không có kỷ niệm",
|
||||
"photos_count_one": "1 kỷ niệm",
|
||||
"photos_count": "{{count, number}} kỷ niệm",
|
||||
"terms_and_conditions": "Tôi đồng ý với <a>các điều khoản</a> và <b>chính sách bảo mật</b>",
|
||||
"photos_count_zero": "Chưa có ảnh nào",
|
||||
"photos_count_one": "1 ảnh",
|
||||
"photos_count": "{{count, number}} ảnh",
|
||||
"terms_and_conditions": "Tôi đồng ý <a>điều khoản</a> và <b>chính sách bảo mật</b>",
|
||||
"people": "Người",
|
||||
"indexing_scheduled": "Lập chỉ mục đã được lên lịch...",
|
||||
"indexing_photos": "",
|
||||
"indexing_fetching": "",
|
||||
"indexing_people": "",
|
||||
"indexing_scheduled": "Đã lên lịch lập chỉ mục...",
|
||||
"indexing_photos": "Đang cập nhật chỉ mục...",
|
||||
"indexing_fetching": "Đang đồng bộ chỉ mục...",
|
||||
"indexing_people": "Đang đồng bộ người...",
|
||||
"syncing_wait": "Đang đồng bộ...",
|
||||
"people_empty_too_few": "Người sẽ được hiển thị ở đây khi có đủ ảnh của một người",
|
||||
"unnamed_person": "Người không tên",
|
||||
"people_empty_too_few": "Sẽ hiện người ở đây khi có ảnh của một người",
|
||||
"unnamed_person": "Chưa đặt tên",
|
||||
"add_a_name": "Thêm một tên",
|
||||
"new_person": "Người mới",
|
||||
"add_name": "Thêm tên",
|
||||
"rename_person": "Đổi tên người",
|
||||
"reset_person_confirm": "Đặt lại người?",
|
||||
"reset_person_confirm_message": "Tên, nhóm khuôn mặt và gợi ý cho người này sẽ được đặt lại",
|
||||
"reset_person_confirm_message": "Tên, nhóm khuôn mặt và những gợi ý cho người này sẽ bị đặt lại",
|
||||
"ignore": "Bỏ qua",
|
||||
"ignore_person_confirm": "Bỏ qua người?",
|
||||
"ignore_person_confirm_message": "Nhóm khuôn mặt này sẽ không được hiển thị trong danh sách người",
|
||||
"ignored": "Đã bỏ qua",
|
||||
"show_person": "Hiển thị người",
|
||||
"review_suggestions": "Xem xét gợi ý",
|
||||
"review_suggestions": "Xem qua gợi ý",
|
||||
"saved_choices": "Lựa chọn đã lưu",
|
||||
"discard_changes": "Bỏ qua thay đổi",
|
||||
"discard_changes_confirm_message": "Bạn có thay đổi chưa được lưu. Những thay đổi này sẽ bị mất nếu bạn đóng mà không lưu",
|
||||
"people_suggestions_finding": "Tìm kiếm khuôn mặt tương tự...",
|
||||
"people_suggestions_empty": "Không còn gợi ý nào cho bây giờ",
|
||||
"people_suggestions_finding": "Tìm khuôn mặt tương tự...",
|
||||
"people_suggestions_empty": "Không còn gợi ý nào",
|
||||
"info": "Thông tin",
|
||||
"file_name": "Tên tệp",
|
||||
"caption_placeholder": "Thêm mô tả",
|
||||
@@ -277,79 +277,79 @@
|
||||
"map": "Bản đồ",
|
||||
"enable_map": "Bật bản đồ",
|
||||
"enable_maps_confirm": "Bật bản đồ?",
|
||||
"enable_maps_confirm_message": "<p>Điều này sẽ hiển thị ảnh của bạn trên bản đồ thế giới.</p><p>Bản đồ được lưu trữ bởi <a>OpenStreetMap</a>, và vị trí chính xác của ảnh của bạn sẽ không bao giờ được chia sẻ.</p><p>Bạn có thể tắt tính năng này bất cứ lúc nào từ Cài đặt.</p>",
|
||||
"enable_maps_confirm_message": "<p>Ảnh của bạn sẽ hiển thị trên bản đồ thế giới.</p><p>Bản đồ được lưu trữ bởi <a>OpenStreetMap</a>, và vị trí chính xác ảnh của bạn không bao giờ được chia sẻ.</p><p>Bạn có thể tắt tính năng này bất cứ lúc nào từ Cài đặt.</p>",
|
||||
"disable_map": "Tắt bản đồ",
|
||||
"disable_maps_confirm": "Tắt bản đồ?",
|
||||
"disable_maps_confirm_message": "<p>Điều này sẽ tắt hiển thị ảnh của bạn trên bản đồ thế giới.</p><p>Bạn có thể bật tính năng này bất cứ lúc nào từ Cài đặt.</p>",
|
||||
"disable_maps_confirm_message": "<p>Ảnh của bạn sẽ thôi hiển thị trên bản đồ thế giới.</p><p>Bạn có thể bật tính năng này bất cứ lúc nào từ Cài đặt.</p>",
|
||||
"details": "Chi tiết",
|
||||
"view_exif": "Xem tất cả dữ liệu Exif",
|
||||
"no_exif": "Không có dữ liệu Exif",
|
||||
"view_exif": "Xem thông số Exif",
|
||||
"no_exif": "No Exif data",
|
||||
"exif": "Exif",
|
||||
"two_factor": "Xác thực hai yếu tố",
|
||||
"two_factor_authentication": "Xác thực hai yếu tố",
|
||||
"two_factor": "Xác thực 2 bước",
|
||||
"two_factor_authentication": "Xác thực 2 bước",
|
||||
"two_factor_qr_help": "Quét mã QR bên dưới bằng ứng dụng xác thực yêu thích của bạn",
|
||||
"two_factor_manual_entry_title": "Nhập mã thủ công",
|
||||
"two_factor_manual_entry_message": "Vui lòng nhập mã này vào ứng dụng xác thực yêu thích của bạn",
|
||||
"scan_qr_title": "Quét mã QR thay thế",
|
||||
"enable_two_factor": "Bật xác thực hai yếu tố",
|
||||
"scan_qr_title": "Quét mã QR",
|
||||
"enable_two_factor": "Bật xác thực 2 bước",
|
||||
"enable": "Bật",
|
||||
"enabled": "Đã bật",
|
||||
"lost_2fa_device": "Thiết bị xác thực hai yếu tố bị mất",
|
||||
"lost_2fa_device": "Mất thiết bị xác thực 2 bước",
|
||||
"incorrect_code": "Mã không chính xác",
|
||||
"two_factor_info": "Thêm một lớp bảo mật bổ sung bằng cách yêu cầu nhiều hơn email và mật khẩu của bạn để đăng nhập vào tài khoản của bạn",
|
||||
"two_factor_info": "Thêm một lớp bảo mật bổ sung bằng cách yêu cầu nhiều hơn email và mật khẩu của bạn để đăng nhập",
|
||||
"disable": "Tắt",
|
||||
"reconfigure": "Cấu hình lại",
|
||||
"reconfigure_two_factor_hint": "Cập nhật thiết bị xác thực của bạn",
|
||||
"update_two_factor": "Cập nhật xác thực hai yếu tố",
|
||||
"update_two_factor_message": "Tiếp tục sẽ làm vô hiệu hóa bất kỳ thiết bị xác thực nào đã được cấu hình trước đó",
|
||||
"update_two_factor": "Cập nhật xác thực 2 bước",
|
||||
"update_two_factor_message": "Tiếp tục sẽ khiến mọi thiết bị xác thực được cấu hình trước đó bị vô hiệu hóa",
|
||||
"update": "Cập nhật",
|
||||
"disable_two_factor": "Tắt xác thực hai yếu tố",
|
||||
"disable_two_factor_message": "Bạn có chắc chắn muốn tắt xác thực hai yếu tố của mình không",
|
||||
"disable_two_factor": "Tắt xác thực 2 bước",
|
||||
"disable_two_factor_message": "Bạn có chắc muốn tắt xác thực 2 bước không",
|
||||
"export_data": "Xuất dữ liệu",
|
||||
"select_folder": "Chọn thư mục",
|
||||
"select_zips": "Chọn tệp zip",
|
||||
"faq": "Câu hỏi thường gặp",
|
||||
"takeout_hint": "Giải nén tất cả các tệp zip vào cùng một thư mục và tải lên. Hoặc tải lên các tệp zip trực tiếp. Xem Câu hỏi thường gặp để biết chi tiết.",
|
||||
"destination": "Điểm đến",
|
||||
"takeout_hint": "Giải nén tất cả tệp zip vào cùng một thư mục và tải lên. Hoặc tải lên trực tiếp các tệp zip. Xem Câu hỏi thường gặp để biết thêm.",
|
||||
"destination": "Đích đến",
|
||||
"start": "Bắt đầu",
|
||||
"last_export_time": "Thời gian xuất cuối cùng",
|
||||
"last_export_time": "Thời gian xuất gần nhất",
|
||||
"export_again": "Đồng bộ lại",
|
||||
"local_storage_not_accessible": "Trình duyệt của bạn hoặc một tiện ích mở rộng đang chặn Ente không lưu dữ liệu vào bộ nhớ cục bộ",
|
||||
"local_storage_not_accessible": "Trình duyệt của bạn hoặc một tiện ích mở rộng đang chặn Ente lưu dữ liệu vào bộ nhớ thiết bị",
|
||||
"email_already_taken": "Email đã được sử dụng",
|
||||
"live_photos_detected": "Các tệp ảnh và video từ Live Photos của bạn đã được gộp thành một tệp duy nhất",
|
||||
"ignored_uploads": "Tải lên đã bị bỏ qua",
|
||||
"ignored_uploads_hint": "Bỏ qua những tệp này vì có tệp có tên và nội dung trùng khớp trong cùng một album",
|
||||
"ignored_uploads_hint": "Những tệp này bị bỏ qua vì có tên và nội dung trùng khớp trong cùng một album",
|
||||
"file_not_uploaded_list": "Các tệp sau không được tải lên",
|
||||
"failed_uploads": "Tải lên không thành công",
|
||||
"failed_uploads_hint": "Sẽ có một tùy chọn để thử lại khi việc tải lên hoàn tất",
|
||||
"retry_failed_uploads": "Thử lại các tệp tải lên không thành công",
|
||||
"failed_uploads_hint": "Sẽ có tùy chọn thử lại sau khi việc tải lên hoàn tất",
|
||||
"retry_failed_uploads": "Thử tải lên lại các tệp không thành công",
|
||||
"thumbnail_generation_failed": "Tạo hình thu nhỏ không thành công",
|
||||
"thumbnail_generation_failed_hint": "Các tệp này đã được tải lên, nhưng rất tiếc chúng tôi không thể tạo hình thu nhỏ cho chúng.",
|
||||
"unsupported_files": "Tệp không được hỗ trợ",
|
||||
"unsupported_files_hint": "Ente chưa hỗ trợ các định dạng tệp này",
|
||||
"blocked_uploads": "Tải lên bị chặn",
|
||||
"blocked_uploads_hint": "Trình duyệt của bạn hoặc một tiện ích mở rộng đang ngăn Ente sử dụng <code>eTags</code> để tải lên các tệp lớn.",
|
||||
"blocked_uploads_hint": "Trình duyệt của bạn hoặc một tiện ích mở rộng đang chặn Ente sử dụng <code>eTags</code> để tải lên các tệp lớn.",
|
||||
"large_files": "Tệp lớn",
|
||||
"large_files_hint": "Các tệp này đã không được tải lên vì chúng vượt quá giới hạn kích thước tệp tối đa của chúng tôi",
|
||||
"large_files_hint": "Các tệp này không thể tải lên vì chúng vượt quá dung lượng tệp tối đa của chúng tôi",
|
||||
"insufficient_storage": "Không đủ dung lượng lưu trữ",
|
||||
"insufficient_storage_hint": "Các tệp này đã không được tải lên vì chúng vượt quá giới hạn kích thước tối đa cho gói lưu trữ của bạn",
|
||||
"uploads_in_progress": "Tải lên đang tiến hành",
|
||||
"insufficient_storage_hint": "Các tệp này không thể tải lên vì chúng vượt quá dung lượng tối đa gói của bạn",
|
||||
"uploads_in_progress": "Đang tải lên",
|
||||
"successful_uploads": "Tải lên thành công",
|
||||
"upload_to_album": "Tải lên album",
|
||||
"add_to_album": "Thêm vào album",
|
||||
"move_to_album": "Di chuyển đến album",
|
||||
"unhide_to_album": "Hiện lại vào album",
|
||||
"unhide_to_album": "Hiện lại trong album",
|
||||
"restore_to_album": "Khôi phục vào album",
|
||||
"section_all": "Tất cả",
|
||||
"section_uncategorized": "Chưa phân loại",
|
||||
"section_archive": "Lưu trữ",
|
||||
"section_hidden": "Ẩn",
|
||||
"section_trash": "Thùng rác",
|
||||
"favorites": "Yêu thích",
|
||||
"favorites": "Đã thích",
|
||||
"archive": "Lưu trữ",
|
||||
"archive_album": "Lưu trữ album",
|
||||
"unarchive": "Khôi phục lưu trữ",
|
||||
"unarchive_album": "Khôi phục lưu trữ album",
|
||||
"unarchive": "Bỏ lưu trữ",
|
||||
"unarchive_album": "Bỏ lưu trữ album",
|
||||
"hide_collection": "Ẩn album",
|
||||
"unhide_collection": "Hiện lại album",
|
||||
"move": "Di chuyển",
|
||||
@@ -357,28 +357,28 @@
|
||||
"remove": "Xóa",
|
||||
"yes_remove": "Có, xóa",
|
||||
"remove_from_album": "Xóa khỏi album",
|
||||
"move_to_trash": "Di chuyển vào thùng rác",
|
||||
"trash_files_message": "Các tệp đã chọn sẽ bị xóa khỏi tất cả các album và di chuyển vào thùng rác.",
|
||||
"trash_file_message": "Tệp sẽ bị xóa khỏi tất cả các album và di chuyển vào thùng rác.",
|
||||
"move_to_trash": "Cho vào thùng rác",
|
||||
"trash_files_message": "Các tệp đã chọn sẽ bị xóa khỏi tất cả album và cho vào thùng rác.",
|
||||
"trash_file_message": "Tệp sẽ bị xóa khỏi tất cả album và cho vào thùng rác.",
|
||||
"delete_permanently": "Xóa vĩnh viễn",
|
||||
"restore": "Khôi phục",
|
||||
"empty_trash": "Làm rỗng thùng rác",
|
||||
"empty_trash_title": "Làm rỗng thùng rác?",
|
||||
"empty_trash": "Xóa sạch thùng rác",
|
||||
"empty_trash_title": "Xóa sạch thùng rác?",
|
||||
"empty_trash_message": "Các tệp này sẽ bị xóa vĩnh viễn khỏi tài khoản Ente của bạn.",
|
||||
"leave_album": "Rời album",
|
||||
"leave_shared_album_title": "Rời album chia sẻ?",
|
||||
"leave_shared_album_message": "Bạn sẽ rời album, và nó sẽ không còn hiển thị cho bạn.",
|
||||
"leave_shared_album_title": "Rời album được chia sẻ?",
|
||||
"leave_shared_album_message": "Bạn sẽ rời album, và nó sẽ không còn hiển thị với bạn.",
|
||||
"leave_shared_album": "Có, rời",
|
||||
"confirm_remove_message": "Các mục đã chọn sẽ bị xóa khỏi album này. Các mục chỉ có trong album này sẽ được chuyển đến Chưa phân loại.",
|
||||
"confirm_remove_incl_others_message": "Một số mục bạn đang xóa đã được thêm bởi người khác, và bạn sẽ mất quyền truy cập vào chúng.",
|
||||
"confirm_remove_incl_others_message": "Vài mục mà bạn đang xóa được thêm bởi người khác, và bạn sẽ mất quyền truy cập vào chúng.",
|
||||
"oldest": "Cũ nhất",
|
||||
"last_updated": "Cập nhật lần cuối",
|
||||
"last_updated": "Mới cập nhật",
|
||||
"name": "Tên",
|
||||
"fix_creation_time": "Sửa thời gian",
|
||||
"fix_creation_time_in_progress": "Đang sửa thời gian",
|
||||
"fix_creation_time_file_updated": "Thời gian tệp đã được cập nhật",
|
||||
"fix_creation_time_completed": "Đã cập nhật thành công tất cả các tệp",
|
||||
"fix_creation_time_completed_with_errors": "Cập nhật thời gian tệp không thành công cho một số tệp, vui lòng thử lại",
|
||||
"fix_creation_time_completed_with_errors": "Cập nhật thời gian một số tệp không thành công, vui lòng thử lại",
|
||||
"fix_creation_time_options": "Chọn tùy chọn bạn muốn sử dụng",
|
||||
"exif_date_time_original": "Exif:DateTimeOriginal",
|
||||
"exif_date_time_digitized": "Exif:DateTimeDigitized",
|
||||
@@ -396,7 +396,7 @@
|
||||
"participants_count_one": "1 người tham gia",
|
||||
"participants_count": "{{count, number}} người tham gia",
|
||||
"add_viewers": "Thêm người xem",
|
||||
"change_permission_to_viewer": "<p>{{selectedEmail}} sẽ không thể thêm nhiều ảnh hơn vào album</p><p>Họ vẫn có thể xóa ảnh đã thêm bởi họ</p>",
|
||||
"change_permission_to_viewer": "<p>{{selectedEmail}} sẽ không thể thêm ảnh vào album</p><p>Họ vẫn có thể xóa ảnh đã thêm bởi họ</p>",
|
||||
"change_permission_to_collaborator": "{{selectedEmail}} sẽ có thể thêm ảnh vào album",
|
||||
"change_permission_title": "Thay đổi quyền?",
|
||||
"confirm_convert_to_viewer": "Có, chuyển thành người xem",
|
||||
@@ -417,10 +417,10 @@
|
||||
"link_expired": "Liên kết đã hết hạn",
|
||||
"link_expired_message": "Liên kết này đã hết hạn hoặc đã bị vô hiệu hóa",
|
||||
"manage_link": "Quản lý liên kết",
|
||||
"link_request_limit_exceeded": "Album này đã được xem trên quá nhiều thiết bị",
|
||||
"link_request_limit_exceeded": "Album này đang được xem trên quá nhiều thiết bị",
|
||||
"allow_downloads": "Cho phép tải xuống",
|
||||
"allow_adding_photos": "Cho phép thêm ảnh",
|
||||
"allow_adding_photos_hint": "Cho phép người có liên kết cũng thêm ảnh vào album chia sẻ.",
|
||||
"allow_adding_photos_hint": "Cho phép người có liên kết thêm ảnh vào album chia sẻ.",
|
||||
"device_limit": "Giới hạn thiết bị",
|
||||
"none": "Không",
|
||||
"link_expiry": "Hết hạn liên kết",
|
||||
@@ -440,30 +440,30 @@
|
||||
"public_link_created": "Liên kết công khai đã được tạo",
|
||||
"public_link_enabled": "Liên kết công khai đã được bật",
|
||||
"collect_photos": "Thu thập ảnh",
|
||||
"disable_file_download": "Vô hiệu hóa tải xuống",
|
||||
"disable_file_download_message": "<p>Bạn có chắc chắn muốn vô hiệu hóa nút tải xuống cho các tệp không?</p><p>Người xem vẫn có thể chụp ảnh màn hình hoặc lưu bản sao của ảnh của bạn bằng các công cụ bên ngoài.</p>",
|
||||
"disable_file_download": "Tắt tải xuống",
|
||||
"disable_file_download_message": "<p>Bạn có chắc muốn tắt nút tải xuống các tệp không?</p><p>Người xem vẫn có thể chụp ảnh màn hình hoặc sao chép ảnh của bạn bằng các công cụ bên ngoài.</p>",
|
||||
"shared_using": "Chia sẻ bằng <a>{{url}}</a>",
|
||||
"sharing_referral_code": "Sử dụng mã <strong>{{referralCode}}</strong> để nhận 10 GB miễn phí",
|
||||
"sharing_referral_code": "Dùng mã <strong>{{referralCode}}</strong> để nhận 10 GB miễn phí",
|
||||
"disable_password": "Vô hiệu hóa khóa mật khẩu",
|
||||
"disable_password_message": "Bạn có chắc chắn muốn vô hiệu hóa khóa mật khẩu không?",
|
||||
"disable_password_message": "Bạn có chắc muốn vô hiệu hóa khóa mật khẩu không?",
|
||||
"password_lock": "Khóa mật khẩu",
|
||||
"lock": "Khóa",
|
||||
"file": "Tệp",
|
||||
"folder": "Thư mục",
|
||||
"google_takeout": "Google takeout",
|
||||
"deduplicate_files": "Xóa trùng tệp",
|
||||
"remove_duplicates": "",
|
||||
"total_size": "",
|
||||
"count": "",
|
||||
"deselect_all": "",
|
||||
"no_duplicates": "",
|
||||
"duplicate_group_description": "",
|
||||
"remove_duplicates_button_count": "",
|
||||
"google_takeout": "Google Takeout",
|
||||
"deduplicate_files": "Xóa tệp trùng",
|
||||
"remove_duplicates": "Xóa trùng lặp",
|
||||
"total_size": "Tổng dung lượng",
|
||||
"count": "Số lượng",
|
||||
"deselect_all": "Bỏ chọn tất cả",
|
||||
"no_duplicates": "Không có trùng lặp",
|
||||
"duplicate_group_description": "{{count}} mục, {{itemSize}} mỗi mục",
|
||||
"remove_duplicates_button_count": "Xóa {{count, number}} mục",
|
||||
"stop_uploads_title": "Dừng tải lên?",
|
||||
"stop_uploads_message": "Bạn có chắc chắn muốn dừng tất cả các tải lên đang diễn ra không?",
|
||||
"stop_uploads_message": "Bạn có chắc muốn dừng tất cả mục đang tải lên không?",
|
||||
"yes_stop_uploads": "Có, dừng tải lên",
|
||||
"stop_downloads_title": "Dừng tải xuống?",
|
||||
"stop_downloads_message": "Bạn có chắc chắn muốn dừng tất cả các tải xuống đang diễn ra không?",
|
||||
"stop_downloads_message": "Bạn có chắc muốn dừng tất cả mục đang tải xuống không?",
|
||||
"yes_stop_downloads": "Có, dừng tải xuống",
|
||||
"albums": "Album",
|
||||
"albums_count_one": "1 Album",
|
||||
@@ -478,14 +478,14 @@
|
||||
"upgrade_now": "Nâng cấp ngay",
|
||||
"renew_now": "Gia hạn ngay",
|
||||
"storage": "Lưu trữ",
|
||||
"used": "đã sử dụng",
|
||||
"used": "đã dùng",
|
||||
"you": "Bạn",
|
||||
"family": "Gia đình",
|
||||
"free": "miễn phí",
|
||||
"of": "của",
|
||||
"watch_folders": "Theo dõi thư mục",
|
||||
"watched_folders": "Thư mục đã theo dõi",
|
||||
"no_folders_added": "Chưa có thư mục nào được thêm",
|
||||
"no_folders_added": "Chưa thêm thư mục nào",
|
||||
"watch_folders_hint_1": "Các thư mục bạn thêm ở đây sẽ được theo dõi tự động",
|
||||
"watch_folders_hint_2": "Tải lên tệp mới vào Ente",
|
||||
"watch_folders_hint_3": "Xóa tệp đã xóa khỏi Ente",
|
||||
@@ -495,51 +495,51 @@
|
||||
"stop_watching_folder_message": "Các tệp hiện có của bạn sẽ không bị xóa, nhưng Ente sẽ ngừng tự động cập nhật album Ente liên kết khi có thay đổi trong thư mục này.",
|
||||
"yes_stop": "Có, dừng lại",
|
||||
"change_folder": "Thay đổi Thư mục",
|
||||
"view_logs": "",
|
||||
"view_logs_message": "",
|
||||
"weak_device_hint": "Trình duyệt web bạn đang sử dụng không đủ mạnh để mã hóa ảnh của bạn. Vui lòng thử đăng nhập vào Ente trên máy tính của bạn, hoặc tải xuống ứng dụng di động/desktop của Ente.",
|
||||
"drag_and_drop_hint": "Hoặc kéo và thả vào cửa sổ Ente",
|
||||
"view_logs": "Xem log",
|
||||
"view_logs_message": "<p>Tải xuống nhật ký lỗi, để bạn có thể gửi qua email cho chúng tôi.</p><p>Lưu ý rằng, trong nhật ký lỗi sẽ bao gồm tên các tệp để giúp theo dõi vấn đề với từng tệp cụ thể.</p>",
|
||||
"weak_device_hint": "Trình duyệt bạn đang sử dụng không đủ mạnh để mã hóa ảnh. Vui lòng dùng Ente trên máy tính, hoặc tải xuống ứng dụng di động/máy tính của Ente.",
|
||||
"drag_and_drop_hint": "Hoặc kéo thả vào cửa sổ Ente",
|
||||
"authenticate": "Xác thực",
|
||||
"uploaded_to_single_collection": "Đã tải lên một bộ sưu tập",
|
||||
"uploaded_to_separate_collections": "Đã tải lên các bộ sưu tập riêng biệt",
|
||||
"nevermind": "Không sao",
|
||||
"update_available": "Cập nhật có sẵn",
|
||||
"update_installable_message": "Một phiên bản mới của Ente đã sẵn sàng để được cài đặt.",
|
||||
"update_available": "Phiên bản mới",
|
||||
"update_installable_message": "Ente có một phiên bản mới, sẵn sàng để cài đặt.",
|
||||
"install_now": "Cài đặt ngay",
|
||||
"install_on_next_launch": "Cài đặt khi khởi động tiếp theo",
|
||||
"update_available_message": "Một phiên bản mới của Ente đã được phát hành, nhưng không thể tự động tải xuống và cài đặt.",
|
||||
"install_on_next_launch": "Cài đặt trong lần khởi động sau",
|
||||
"update_available_message": "Ente có một phiên bản mới, nhưng không thể tự động tải xuống và cài đặt.",
|
||||
"download_and_install": "Tải xuống và cài đặt",
|
||||
"ignore_this_version": "Bỏ qua phiên bản này",
|
||||
"today": "Hôm nay",
|
||||
"yesterday": "Hôm qua",
|
||||
"enter_name": "Nhập tên",
|
||||
"uploader_name_hint": "Thêm một tên để bạn bè biết ai là người đáng cảm ơn cho những bức ảnh tuyệt vời này!",
|
||||
"uploader_name_hint": "Thêm một tên để bạn bè biết ai là người chụp những tấm ảnh tuyệt vời này!",
|
||||
"name_placeholder": "Tên...",
|
||||
"more_details": "Thêm chi tiết",
|
||||
"ml_search": "Học máy",
|
||||
"ml_search_description": "Ente hỗ trợ học máy trên thiết bị cho nhận diện khuôn mặt, tìm kiếm kỳ diệu và các tính năng tìm kiếm nâng cao khác",
|
||||
"ml_search_footnote": "Tìm kiếm kỳ diệu cho phép tìm kiếm ảnh theo nội dung của chúng, ví dụ: 'xe hơi', 'xe hơi đỏ', 'Ferrari'",
|
||||
"ml_search_description": "Ente hỗ trợ học máy trên-thiết-bị nhằm nhận diện khuôn mặt, tìm kiếm vi diệu và các tính năng tìm kiếm nâng cao khác",
|
||||
"ml_search_footnote": "Tìm kiếm vi diệu cho phép tìm ảnh theo nội dung của chúng, ví dụ: 'xe hơi', 'xe hơi đỏ', 'Ferrari'",
|
||||
"indexing": "Đang lập chỉ mục",
|
||||
"processed": "Đã xử lý",
|
||||
"indexing_status_running": "Đang chạy",
|
||||
"indexing_status_fetching": "Đang lấy",
|
||||
"indexing_status_scheduled": "Đã lên lịch",
|
||||
"indexing_status_done": "Đã hoàn thành",
|
||||
"ml_search_disable": "Vô hiệu hóa học máy",
|
||||
"ml_search_disable_confirm": "Bạn có muốn vô hiệu hóa học máy trên tất cả các thiết bị của bạn không?",
|
||||
"ml_search_disable": "Tắt học máy",
|
||||
"ml_search_disable_confirm": "Bạn có muốn tắt học máy trên tất cả các thiết bị của bạn không?",
|
||||
"ml_consent": "Bật học máy",
|
||||
"ml_consent_title": "Bật học máy?",
|
||||
"ml_consent_description": "<p>Nếu bạn bật học máy, Ente sẽ trích xuất thông tin như hình dạng khuôn mặt từ các tệp, bao gồm cả những tệp được chia sẻ với bạn.</p><p>Điều này sẽ xảy ra trên thiết bị của bạn, và bất kỳ thông tin sinh trắc học nào được tạo ra sẽ được mã hóa đầu cuối.</p><p><a>Vui lòng nhấp vào đây để biết thêm chi tiết về tính năng này trong chính sách quyền riêng tư của chúng tôi</a></p>",
|
||||
"ml_consent_description": "<p>Nếu bạn bật học máy, Ente sẽ trích xuất thông tin như hình dạng khuôn mặt từ các tệp, gồm cả những tệp mà bạn được chia sẻ.</p><p>Việc này sẽ diễn ra trên thiết bị của bạn, với mọi thông tin sinh trắc học tạo ra đều được mã hóa đầu cuối.</p><p><a>Vui lòng nhấn vào đây để biết thêm chi tiết về tính năng này trong chính sách quyền riêng tư của chúng tôi</a></p>",
|
||||
"ml_consent_confirmation": "Tôi hiểu và muốn bật học máy",
|
||||
"labs": "Phòng thí nghiệm",
|
||||
"labs": "Thử nghiệm",
|
||||
"password_strength_weak": "Độ mạnh mật khẩu: Yếu",
|
||||
"password_strength_moderate": "Độ mạnh mật khẩu: Trung bình",
|
||||
"password_strength_strong": "Độ mạnh mật khẩu: Mạnh",
|
||||
"preferences": "Tùy chọn",
|
||||
"preferences": "Thiết lập",
|
||||
"language": "Ngôn ngữ",
|
||||
"advanced": "Nâng cao",
|
||||
"export_directory_does_not_exist": "Thư mục xuất không hợp lệ",
|
||||
"export_directory_does_not_exist_message": "<p>Thư mục xuất mà bạn đã chọn không tồn tại.</p><p>Vui lòng chọn một thư mục hợp lệ.</p>",
|
||||
"export_directory_does_not_exist_message": "<p>Thư mục xuất mà bạn đã chọn không tồn tại.</p><p>Vui lòng chọn một thư mục khác.</p>",
|
||||
"storage_unit": {
|
||||
"b": "B",
|
||||
"kb": "KB",
|
||||
@@ -549,33 +549,33 @@
|
||||
},
|
||||
"stop": "Dừng",
|
||||
"sync_continuously": "Đồng bộ liên tục",
|
||||
"export_starting": "Xuất bắt đầu...",
|
||||
"export_starting": "Bắt đầu xuất...",
|
||||
"export_preparing": "Đang chuẩn bị...",
|
||||
"export_renaming_album_folders": "Đang đổi tên thư mục album...",
|
||||
"export_trashing_deleted_files": "Đang xóa tệp đã xóa...",
|
||||
"export_trashing_deleted_albums": "Đang xóa album đã xóa...",
|
||||
"export_trashing_deleted_files": "Đang xóa vĩnh viễn các tệp...",
|
||||
"export_trashing_deleted_albums": "Đang xóa vĩnh viễn các album...",
|
||||
"export_progress": "<a>{{progress.success, number}} / {{progress.total, number}}</a> mục đã đồng bộ",
|
||||
"pending_items": "Mục đang chờ",
|
||||
"delete_account_reason_label": "Lý do chính bạn xóa tài khoản là gì?",
|
||||
"delete_account_reason_placeholder": "Chọn một lý do",
|
||||
"delete_reason": {
|
||||
"missing_feature": "Thiếu một tính năng quan trọng mà tôi cần",
|
||||
"behaviour": "Ứng dụng hoặc một tính năng nhất định không hoạt động như tôi nghĩ nó nên",
|
||||
"found_another_service": "Tôi đã tìm thấy một dịch vụ khác mà tôi thích hơn",
|
||||
"not_listed": "Lý do của tôi không có trong danh sách"
|
||||
"behaviour": "Ứng dụng hoặc một tính năng nhất định không hoạt động như tôi muốn",
|
||||
"found_another_service": "Tôi tìm thấy một dịch vụ khác mà tôi thích hơn",
|
||||
"not_listed": "Lý do không có trong danh sách"
|
||||
},
|
||||
"delete_account_feedback_label": "Chúng tôi rất tiếc khi thấy bạn ra đi. Vui lòng giải thích lý do bạn rời đi để giúp chúng tôi cải thiện.",
|
||||
"delete_account_feedback_placeholder": "Phản hồi",
|
||||
"delete_account_confirm_checkbox_label": "Có, tôi muốn xóa tài khoản này và tất cả dữ liệu của nó vĩnh viễn",
|
||||
"delete_account_confirm_checkbox_label": "Có, tôi muốn xóa vĩnh viễn tài khoản này và tất cả dữ liệu của nó",
|
||||
"delete_account_confirm": "Xác nhận xóa tài khoản",
|
||||
"delete_account_confirm_message": "<p>Tài khoản này được liên kết với các ứng dụng Ente khác, nếu bạn sử dụng bất kỳ.</p><p>Dữ liệu bạn đã tải lên, trên tất cả các ứng dụng Ente, sẽ được lên lịch để xóa, và tài khoản của bạn sẽ bị xóa vĩnh viễn.</p>",
|
||||
"feedback_required": "Xin vui lòng giúp chúng tôi với thông tin này",
|
||||
"delete_account_confirm_message": "<p>Tài khoản này được liên kết với các ứng dụng Ente khác, nếu bạn có dùng.</p><p>Dữ liệu bạn đã tải lên, trên tất cả ứng dụng Ente, sẽ được lên lịch để xóa, và tài khoản của bạn sẽ bị xóa vĩnh viễn.</p>",
|
||||
"feedback_required": "Mong bạn giúp chúng tôi thông tin này",
|
||||
"feedback_required_found_another_service": "Dịch vụ khác làm tốt hơn điều gì?",
|
||||
"recover_two_factor": "Khôi phục xác thực hai yếu tố",
|
||||
"recover_two_factor": "Khôi phục xác thực 2 bước",
|
||||
"at": "tại",
|
||||
"auth_next": "tiếp theo",
|
||||
"auth_download_mobile_app": "Tải xuống ứng dụng di động của chúng tôi để quản lý bí mật của bạn",
|
||||
"no_codes_added_yet": "Chưa có mã nào được thêm",
|
||||
"no_codes_added_yet": "Chưa thêm mã nào",
|
||||
"hide": "Ẩn",
|
||||
"unhide": "Hiện",
|
||||
"sort_by": "Sắp xếp theo",
|
||||
@@ -583,30 +583,30 @@
|
||||
"oldest_first": "Cũ nhất trước",
|
||||
"pin_album": "Ghim album",
|
||||
"unpin_album": "Bỏ ghim album",
|
||||
"unpreviewable_file_message": "Tệp này không thể được xem trước",
|
||||
"download_complete": "Tải xuống hoàn tất",
|
||||
"unpreviewable_file_message": "Không thể xem trước tệp này",
|
||||
"download_complete": "Tải xuống xong",
|
||||
"downloading_album": "Đang tải xuống {{name}}",
|
||||
"download_failed": "Tải xuống thất bại",
|
||||
"download_progress": "{{count, number}} / {{total, number}} tệp",
|
||||
"christmas": "Giáng sinh",
|
||||
"christmas_eve": "Đêm Giáng sinh",
|
||||
"new_year": "Năm mới",
|
||||
"new_year_eve": "Đêm giao thừa",
|
||||
"christmas": "Giáng Sinh",
|
||||
"christmas_eve": "Đêm Thánh",
|
||||
"new_year": "Năm Mới",
|
||||
"new_year_eve": "Đêm Giao Thừa",
|
||||
"image": "Hình ảnh",
|
||||
"video": "Video",
|
||||
"live_photo": "Ảnh trực tiếp",
|
||||
"live": "",
|
||||
"edit_image": "",
|
||||
"live_photo": "Ảnh Live",
|
||||
"live": "Live",
|
||||
"edit_image": "Chỉnh sửa ảnh",
|
||||
"photo_editor": "Trình chỉnh sửa ảnh",
|
||||
"confirm_editor_close": "Bạn có chắc chắn muốn đóng trình chỉnh sửa không?",
|
||||
"confirm_editor_close_message": "Tải xuống hình ảnh đã chỉnh sửa của bạn hoặc lưu bản sao vào Ente để giữ lại các thay đổi của bạn.",
|
||||
"confirm_editor_close": "Bạn có chắc muốn đóng trình chỉnh sửa không?",
|
||||
"confirm_editor_close_message": "Tải xuống hình ảnh đã chỉnh sửa hoặc lưu bản sao vào Ente để giữ các thay đổi của bạn.",
|
||||
"brightness": "Độ sáng",
|
||||
"contrast": "Độ tương phản",
|
||||
"saturation": "Độ bão hòa",
|
||||
"blur": "Mờ",
|
||||
"blur": "Độ mờ",
|
||||
"transform": "Biến đổi",
|
||||
"crop": "Cắt",
|
||||
"aspect_ratio": "Tỷ lệ khung hình",
|
||||
"aspect_ratio": "Tỉ lệ khung hình",
|
||||
"square": "Hình vuông",
|
||||
"freehand": "Vẽ tự do",
|
||||
"apply_crop": "Áp dụng cắt",
|
||||
@@ -614,27 +614,27 @@
|
||||
"rotate_left": "Xoay trái",
|
||||
"rotate_right": "Xoay phải",
|
||||
"flip": "Lật",
|
||||
"flip_vertically": "Lật theo chiều dọc",
|
||||
"flip_horizontally": "Lật theo chiều ngang",
|
||||
"flip_vertically": "Lật dọc",
|
||||
"flip_horizontally": "Lật ngang",
|
||||
"download_edited": "Tải xuống đã chỉnh sửa",
|
||||
"save_a_copy_to_ente": "Lưu một bản sao vào Ente",
|
||||
"restore_original": "Khôi phục gốc",
|
||||
"photo_edit_required_to_save": "Ít nhất một biến đổi hoặc điều chỉnh màu sắc phải được thực hiện trước khi lưu.",
|
||||
"photo_edit_required_to_save": "Phải thực hiện ít nhất một biến đổi hoặc điều chỉnh màu sắc trước khi lưu.",
|
||||
"colors": "Màu sắc",
|
||||
"invert_colors": "Đảo ngược màu",
|
||||
"reset": "Đặt lại",
|
||||
"faster_upload": "Tải lên nhanh hơn",
|
||||
"faster_upload_description": "Định tuyến tải lên qua các máy chủ gần đó",
|
||||
"open_ente_on_startup": "",
|
||||
"faster_upload_description": "Tải lên các máy chủ gần bạn",
|
||||
"open_ente_on_startup": "Mở Ente khi khởi động",
|
||||
"cast_album_to_tv": "Phát album trên TV",
|
||||
"cast_to_tv": "",
|
||||
"cast_to_tv": "Phát trên TV",
|
||||
"enter_cast_pin_code": "Nhập mã bạn thấy trên TV bên dưới để ghép nối thiết bị này.",
|
||||
"code": "Mã",
|
||||
"pair_device_to_tv": "Ghép nối thiết bị",
|
||||
"tv_not_found": "Không tìm thấy TV. Bạn đã nhập mã PIN đúng chưa?",
|
||||
"cast_auto_pair": "Ghép nối tự động",
|
||||
"cast_auto_pair_description": "Ghép nối tự động chỉ hoạt động với các thiết bị hỗ trợ Chromecast.",
|
||||
"choose_device_from_browser": "Chọn một thiết bị tương thích với phát từ cửa sổ trình duyệt.",
|
||||
"choose_device_from_browser": "Chọn một thiết bị phát tương thích từ cửa sổ trình duyệt.",
|
||||
"cast_auto_pair_failed": "Ghép nối tự động Chromecast thất bại. Vui lòng thử lại.",
|
||||
"pair_with_pin": "Ghép nối bằng PIN",
|
||||
"pair_with_pin_description": "Ghép nối bằng PIN hoạt động với bất kỳ màn hình nào bạn muốn xem album của mình.",
|
||||
@@ -643,29 +643,29 @@
|
||||
"passkey_fetch_failed": "Không thể lấy khóa truy cập của bạn.",
|
||||
"manage_passkey": "Quản lý khóa truy cập",
|
||||
"delete_passkey": "Xóa khóa truy cập",
|
||||
"delete_passkey_confirmation": "Bạn có chắc chắn muốn xóa khóa truy cập này không? Hành động này không thể hoàn tác.",
|
||||
"delete_passkey_confirmation": "Bạn có chắc muốn xóa khóa truy cập này không? Hành động này không thể hoàn tác.",
|
||||
"rename_passkey": "Đổi tên khóa truy cập",
|
||||
"add_passkey": "Thêm khóa truy cập",
|
||||
"enter_passkey_name": "Nhập tên khóa truy cập",
|
||||
"passkeys_description": "Khóa truy cập là một yếu tố thứ hai hiện đại và an toàn cho tài khoản Ente của bạn. Chúng sử dụng xác thực sinh trắc học trên thiết bị để tiện lợi và an toàn.",
|
||||
"created_at": "Được tạo vào",
|
||||
"passkeys_description": "Khóa truy cập là một yếu tố bảo mật hiện đại cho tài khoản Ente của bạn. Chúng sử dụng xác thực sinh trắc học trên thiết bị để tiện lợi và an toàn.",
|
||||
"created_at": "Đã tạo vào",
|
||||
"passkey_add_failed": "Không thể thêm khóa truy cập",
|
||||
"passkey_login_failed": "Đăng nhập bằng khóa truy cập thất bại",
|
||||
"passkey_login_invalid_url": "URL đăng nhập không hợp lệ.",
|
||||
"passkey_login_already_claimed_session": "Phiên này đã được xác minh.",
|
||||
"passkey_login_generic_error": "Đã xảy ra lỗi khi đăng nhập bằng khóa truy cập.",
|
||||
"passkey_login_credential_hint": "Nếu khóa truy cập của bạn ở trên thiết bị khác, bạn có thể mở trang này trên thiết bị đó để xác minh.",
|
||||
"passkeys_not_supported": "Khóa truy cập không được hỗ trợ trong trình duyệt này",
|
||||
"passkeys_not_supported": "Khóa truy cập không được hỗ trợ trên trình duyệt này",
|
||||
"try_again": "Thử lại",
|
||||
"check_status": "Kiểm tra trạng thái",
|
||||
"passkey_login_instructions": "Thực hiện các bước từ trình duyệt của bạn để tiếp tục đăng nhập.",
|
||||
"passkey_login": "Đăng nhập bằng khóa truy cập",
|
||||
"totp_login": "Đăng nhập bằng TOTP",
|
||||
"passkey": "Mã khóa",
|
||||
"passkey_verify_description": "Xác minh mã khóa của bạn để đăng nhập vào tài khoản.",
|
||||
"passkey": "Khóa truy cập",
|
||||
"passkey_verify_description": "Xác minh khóa truy cập của bạn để đăng nhập vào tài khoản.",
|
||||
"waiting_for_verification": "Đang chờ xác minh...",
|
||||
"verification_still_pending": "Xác minh vẫn đang chờ",
|
||||
"passkey_verified": "Mã khóa đã được xác minh",
|
||||
"passkey_verified": "Khóa truy cập đã được xác minh",
|
||||
"redirecting_back_to_app": "Đang chuyển hướng bạn trở lại ứng dụng...",
|
||||
"redirect_close_instructions": "Bạn có thể đóng cửa sổ này sau khi ứng dụng mở.",
|
||||
"redirect_again": "Chuyển hướng lại",
|
||||
@@ -675,15 +675,15 @@
|
||||
"server_endpoint": "Điểm cuối máy chủ",
|
||||
"more_information": "Thêm thông tin",
|
||||
"save": "Lưu",
|
||||
"theme": "",
|
||||
"system": "",
|
||||
"light": "",
|
||||
"dark": "",
|
||||
"streamable_videos": "",
|
||||
"processing_videos_status": "",
|
||||
"share_favorites": "",
|
||||
"person_favorites": "",
|
||||
"shared_favorites": "",
|
||||
"added_by_name": "",
|
||||
"unowned_files_not_processed": ""
|
||||
"theme": "Chủ đề",
|
||||
"system": "Giống hệ thống",
|
||||
"light": "Sáng",
|
||||
"dark": "Tối",
|
||||
"streamable_videos": "Video có thể phát",
|
||||
"processing_videos_status": "Đang xử lý video...",
|
||||
"share_favorites": "Chia sẻ những mục thích",
|
||||
"person_favorites": "{{name}} đã thích",
|
||||
"shared_favorites": "Những mục thích đã chia sẻ",
|
||||
"added_by_name": "Được thêm bởi {{name}}",
|
||||
"unowned_files_not_processed": "Các tệp được thêm bởi người dùng khác không được xử lý"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"set_password": "设置密码",
|
||||
"sign_in": "登录",
|
||||
"incorrect_password": "密码错误",
|
||||
"incorrect_password_or_no_account": "",
|
||||
"incorrect_password_or_no_account": "密码错误或邮箱未注册",
|
||||
"pick_password_hint": "请输入我们可以用来加密您数据的密码",
|
||||
"pick_password_caution": "我们不会存储您的密码,因此如果您忘记密码, <strong>我们将无法帮助您</strong>在没有恢复密钥的情况下恢复您的数据。",
|
||||
"key_generation_in_progress": "正在生成加密密钥...",
|
||||
@@ -40,7 +40,7 @@
|
||||
"referral_source_hint": "您是如何知道Ente的? (可选的)",
|
||||
"referral_source_info": "我们不跟踪应用程序安装情况,如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
|
||||
"password_mismatch_error": "两次输入的密码不一致",
|
||||
"show_or_hide_password": "",
|
||||
"show_or_hide_password": "显示或隐藏密码",
|
||||
"welcome_to_ente_title": "欢迎来到 <a/>",
|
||||
"welcome_to_ente_subtitle": "端到端加密的照片存储和共享",
|
||||
"new_album": "新建相册",
|
||||
@@ -58,12 +58,12 @@
|
||||
"add_photos_count": "添加 {{count, number}} 个项目",
|
||||
"select_photos": "选择照片",
|
||||
"file_upload": "上传文件",
|
||||
"preparing": "",
|
||||
"processed_counts": "",
|
||||
"upload_reading_metadata_files": "",
|
||||
"preparing": "准备中",
|
||||
"processed_counts": "{{count, number}} / {{total, number}}",
|
||||
"upload_reading_metadata_files": "正在读取元数据文件",
|
||||
"upload_cancelling": "正在取消剩余的上传内容",
|
||||
"upload_done": "",
|
||||
"upload_skipped": "",
|
||||
"upload_done": "{{count, number}} 个已上传",
|
||||
"upload_skipped": "{{count, number}} 个已跳过",
|
||||
"initial_load_delay_warning": "第一次加载可能需要一些时间",
|
||||
"no_account": "没有账号",
|
||||
"existing_account": "已有账号",
|
||||
@@ -96,15 +96,15 @@
|
||||
"exit_fullscreen": "退出全屏",
|
||||
"go_fullscreen": "全屏显示",
|
||||
"zoom": "缩放",
|
||||
"play": "",
|
||||
"pause": "",
|
||||
"play": "播放",
|
||||
"pause": "暂停",
|
||||
"previous": "上一个",
|
||||
"next": "下一个",
|
||||
"video_seek": "",
|
||||
"quality": "",
|
||||
"auto": "",
|
||||
"original": "",
|
||||
"speed": "",
|
||||
"video_seek": "视频跳转",
|
||||
"quality": "质量",
|
||||
"auto": "自动",
|
||||
"original": "原始",
|
||||
"speed": "速度",
|
||||
"title_photos": "Ente 照片",
|
||||
"title_auth": "Ente 验证器",
|
||||
"title_accounts": "Ente 账户",
|
||||
@@ -162,7 +162,7 @@
|
||||
"ok": "确定",
|
||||
"success": "成功",
|
||||
"error": "错误",
|
||||
"note": "",
|
||||
"note": "提示",
|
||||
"offline_message": "您处于离线状态,正在显示已缓存的回忆",
|
||||
"install": "安装",
|
||||
"install_mobile_app": "安装我们的 <a>Android</a> 或 <b>iOS</b> 应用程序来自动备份您的所有照片",
|
||||
@@ -225,7 +225,7 @@
|
||||
"delete_album_message": "也删除此相册中存在的照片(和视频),从 <a>他们所加入的所有</a> 个其他相册?",
|
||||
"delete_photos": "删除照片",
|
||||
"keep_photos": "保留照片",
|
||||
"share_album": "分享相册",
|
||||
"share_album": "共享相册",
|
||||
"sharing_with_self": "您不能与自己共享",
|
||||
"sharing_already_shared": "您已经与 {{email}} 共享了",
|
||||
"sharing_album_not_allowed": "不允许分享相册",
|
||||
@@ -389,7 +389,7 @@
|
||||
"modify_sharing": "更改共享",
|
||||
"add_collaborators": "添加协作者",
|
||||
"add_new_email": "添加新的电子邮件",
|
||||
"shared_with_people_count_zero": "与特定人员分享",
|
||||
"shared_with_people_count_zero": "与特定人员共享",
|
||||
"shared_with_people_count_one": "已与1个人共享",
|
||||
"shared_with_people_count": "已与 {count, number} 个人共享",
|
||||
"participants_count_zero": "暂无参与者",
|
||||
@@ -442,7 +442,7 @@
|
||||
"collect_photos": "收集照片",
|
||||
"disable_file_download": "禁止下载",
|
||||
"disable_file_download_message": "<p>您确定要禁用文件下载按钮吗?</p><p>观看者仍然可以使用外部工具进行屏幕截图或保存您的照片副本。</p>",
|
||||
"shared_using": "分享方式 <a>{{url}}</a>",
|
||||
"shared_using": "共享方式 <a>{{url}}</a>",
|
||||
"sharing_referral_code": "使用代码 <strong>{{referralCode}}</strong> 获得 10 GB 免费空间",
|
||||
"disable_password": "禁用密码锁",
|
||||
"disable_password_message": "您确定要禁用密码锁吗?",
|
||||
@@ -627,7 +627,7 @@
|
||||
"faster_upload_description": "通过附近的服务器路由上传",
|
||||
"open_ente_on_startup": "启动时打开 Ente",
|
||||
"cast_album_to_tv": "在电视上播放相册",
|
||||
"cast_to_tv": "",
|
||||
"cast_to_tv": "在电视上播放",
|
||||
"enter_cast_pin_code": "输入您在下面的电视上看到的代码来配对此设备。",
|
||||
"code": "代码",
|
||||
"pair_device_to_tv": "配对设备",
|
||||
@@ -679,11 +679,11 @@
|
||||
"system": "系统",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"streamable_videos": "",
|
||||
"processing_videos_status": "",
|
||||
"share_favorites": "",
|
||||
"person_favorites": "",
|
||||
"shared_favorites": "",
|
||||
"added_by_name": "",
|
||||
"unowned_files_not_processed": ""
|
||||
"streamable_videos": "可流媒体播放的视频",
|
||||
"processing_videos_status": "正在处理视频...",
|
||||
"share_favorites": "共享收藏",
|
||||
"person_favorites": "{{name}}的收藏",
|
||||
"shared_favorites": "已共享的收藏",
|
||||
"added_by_name": "由{{name}}添加",
|
||||
"unowned_files_not_processed": "由其他用户添加的文件未被处理"
|
||||
}
|
||||
|
||||
@@ -81,6 +81,13 @@ export const customAPIHost = async () => {
|
||||
export const uploaderOrigin = async () =>
|
||||
(await customAPIOrigin()) ?? "https://uploader.ente.io";
|
||||
|
||||
/**
|
||||
* A static build time constant that is `true` if {@link albumsAppOrigin} has
|
||||
* been customized.
|
||||
*/
|
||||
export const isCustomAlbumsAppOrigin =
|
||||
!!process.env.NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT;
|
||||
|
||||
/**
|
||||
* Return the origin that serves public albums.
|
||||
*
|
||||
|
||||
157
web/packages/gallery/components/utils/save-groups.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
/**
|
||||
* An object that keeps track of progress of a user-initiated download of a set
|
||||
* of files to the user's device.
|
||||
*
|
||||
* This "download" is distinct from the downloads the app does from remote (e.g.
|
||||
* when the user is viewing them).
|
||||
*
|
||||
* What we're doing here is perhaps more accurately described "a user initiated
|
||||
* download of files to the user's device", but that is too long, so we instead
|
||||
* refer to this process as "saving them".
|
||||
*
|
||||
* Note however that the app's UI itself takes the user perspective, so the
|
||||
* upper (UI) layers use the word "download", while this implementation layer
|
||||
* uses the word "save", and there is an unavoidable incongruity in the middle.
|
||||
*/
|
||||
export interface SaveGroup {
|
||||
/**
|
||||
* A randomly generated unique identifier of this set of saves.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* The user visible title of the save group.
|
||||
*
|
||||
* Depending on the context can either be an auto generated string (e.g "5
|
||||
* files"), or the name of the collection which is being downloaded.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* If this save group is associated with a {@link CollectionSummary}, then
|
||||
* the ID of that collection summary.
|
||||
*/
|
||||
collectionSummaryID?: number;
|
||||
/**
|
||||
* `true` if the collection summary associated with the save group is
|
||||
* hidden.
|
||||
*/
|
||||
isHiddenCollectionSummary?: boolean;
|
||||
/**
|
||||
* The path to a directory on the user's file system that was selected by
|
||||
* the user to save the files in when they initiated the download on the
|
||||
* desktop app.
|
||||
*
|
||||
* This property is only set when running in the context of the desktop app.
|
||||
* The web app downloads to the user's default downloads folder, and when
|
||||
* running in the web app this property will not be set.
|
||||
*/
|
||||
downloadDirPath?: string;
|
||||
/**
|
||||
* The total number of files to save to the user's device.
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
* The number of files that have already been save.
|
||||
*/
|
||||
success: number;
|
||||
/**
|
||||
* The number of failures.
|
||||
*/
|
||||
failed: number;
|
||||
/**
|
||||
* An {@link AbortController} that can be used to cancel the save.
|
||||
*/
|
||||
canceller?: AbortController;
|
||||
}
|
||||
|
||||
export const isSaveStarted = (group: SaveGroup) => group.total > 0;
|
||||
|
||||
/**
|
||||
* Return `true` if there are no files in this save group that are pending.
|
||||
*/
|
||||
export const isSaveComplete = ({ total, success, failed }: SaveGroup) =>
|
||||
total == success + failed;
|
||||
|
||||
/**
|
||||
* Return `true` if there are no files in this save group that are pending, but
|
||||
* one or more files had failed to download.
|
||||
*/
|
||||
export const isSaveCompleteWithErrors = (group: SaveGroup) =>
|
||||
group.failed > 0 && isSaveComplete(group);
|
||||
|
||||
/**
|
||||
* Return `true` if this save was cancelled on a user request.
|
||||
*/
|
||||
export const isSaveCancelled = (group: SaveGroup) =>
|
||||
group.canceller?.signal.aborted;
|
||||
|
||||
/**
|
||||
* A function that can be used to add a save group.
|
||||
*
|
||||
* It returns a function that can subsequently be used to update the save group
|
||||
* by applying a transform to it (see {@link UpdateSaveGroup}). The UI will
|
||||
* react and update itself on updates done this way.
|
||||
*/
|
||||
export type AddSaveGroup = (group: Partial<SaveGroup>) => UpdateSaveGroup;
|
||||
|
||||
/**
|
||||
* A function that can be used to update a instance of a save group by applying
|
||||
* the provided transform.
|
||||
*
|
||||
* This is obtained by a call to an instance of {@link AddSaveGroup}. The UI
|
||||
* will update itself to reflect the changes made by the transform.
|
||||
*/
|
||||
export type UpdateSaveGroup = (
|
||||
tranform: (prev: SaveGroup) => SaveGroup,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* A function that can be used to remove a save group.
|
||||
*
|
||||
* Save groups can be removed both on user actions - if the user presses the
|
||||
* close button to discard the notification showing the status of the save group
|
||||
* (cancelling it if needed) - or programmatically, if it is found that there
|
||||
* are no files that need saving for a particular request.
|
||||
*/
|
||||
export type RemoveSaveGroup = (saveGroup: SaveGroup) => void;
|
||||
|
||||
/**
|
||||
* A custom React hook that manages a list of active {@link SaveGroup}s, and
|
||||
* provides functions to add and remove entries to the list.
|
||||
*/
|
||||
export const useSaveGroups = () => {
|
||||
const [saveGroups, setSaveGroups] = useState<SaveGroup[]>([]);
|
||||
|
||||
const handleAddSaveGroup: AddSaveGroup = useCallback((saveGroup) => {
|
||||
const id = Math.random();
|
||||
setSaveGroups((groups) => [
|
||||
...groups,
|
||||
{
|
||||
...saveGroup,
|
||||
id,
|
||||
// TODO(RE):
|
||||
title: saveGroup.title ?? "",
|
||||
total: saveGroup.total ?? 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
},
|
||||
]);
|
||||
return (tx: (group: SaveGroup) => SaveGroup) => {
|
||||
setSaveGroups((groups) =>
|
||||
groups.map((g) => (g.id == id ? tx(g) : g)),
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleRemoveSaveGroup: RemoveSaveGroup = useCallback(
|
||||
({ id }) => setSaveGroups((groups) => groups.filter((g) => g.id != id)),
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
saveGroups,
|
||||
onAddSaveGroup: handleAddSaveGroup,
|
||||
onRemoveSaveGroup: handleRemoveSaveGroup,
|
||||
};
|
||||
};
|
||||
241
web/packages/gallery/services/save.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { joinPath } from "ente-base/file-name";
|
||||
import log from "ente-base/log";
|
||||
import { type Electron } from "ente-base/types/ipc";
|
||||
import { saveAsFileAndRevokeObjectURL } from "ente-base/utils/web";
|
||||
import { downloadManager } from "ente-gallery/services/download";
|
||||
import { detectFileTypeInfo } from "ente-gallery/utils/detect-type";
|
||||
import { writeStream } from "ente-gallery/utils/native-stream";
|
||||
import type { EnteFile } from "ente-media/file";
|
||||
import { fileFileName } from "ente-media/file-metadata";
|
||||
import { FileType } from "ente-media/file-type";
|
||||
import { decodeLivePhoto } from "ente-media/live-photo";
|
||||
import {
|
||||
safeDirectoryName,
|
||||
safeFileName,
|
||||
} from "ente-new/photos/utils/native-fs";
|
||||
import { wait } from "ente-utils/promise";
|
||||
import type { AddSaveGroup } from "../components/utils/save-groups";
|
||||
|
||||
/**
|
||||
* Save the given {@link files} to the user's device.
|
||||
*
|
||||
* If we're running in the context of the web app, the files will be saved to
|
||||
* the user's download folder. If we're running in the context of our desktop
|
||||
* app, the user will be prompted to select a directory on their file system and
|
||||
* the files will be saved therein.
|
||||
*
|
||||
* @param files The files to save.
|
||||
*
|
||||
* @param title A title to show in the UI notification that indicates the
|
||||
* progress of the save.
|
||||
*
|
||||
* @param onAddSaveGroup A function that can be used to create a save group
|
||||
* associated with the save. The newly added save group will correspond to a
|
||||
* notification shown in the UI, and the progress and status of the save can be
|
||||
* communicated by updating the save group's state using the updater function
|
||||
* obtained when adding the save group.
|
||||
*/
|
||||
export const downloadAndSaveFiles = (
|
||||
files: EnteFile[],
|
||||
title: string,
|
||||
onAddSaveGroup: AddSaveGroup,
|
||||
) => downloadAndSave(files, title, onAddSaveGroup);
|
||||
|
||||
/**
|
||||
* Save all the files of a collection to the user's device.
|
||||
*
|
||||
* This is a variant of {@link downloadAndSaveFiles}, except instead of taking a
|
||||
* list of files to save, this variant is tailored for saving saves all the
|
||||
* files that belong to a collection. Otherwise, it broadly behaves similarly;
|
||||
* see that method's documentation for more details.
|
||||
*
|
||||
* When running in the context of the desktop app, instead of saving the files
|
||||
* in the directory selected by the user, files are saved in a directory with
|
||||
* the same name as the collection.
|
||||
*/
|
||||
export const downloadAndSaveCollectionFiles = async (
|
||||
collectionSummaryName: string,
|
||||
collectionSummaryID: number,
|
||||
files: EnteFile[],
|
||||
isHiddenCollectionSummary: boolean,
|
||||
onAddSaveGroup: AddSaveGroup,
|
||||
) =>
|
||||
downloadAndSave(
|
||||
files,
|
||||
collectionSummaryName,
|
||||
onAddSaveGroup,
|
||||
collectionSummaryName,
|
||||
collectionSummaryID,
|
||||
isHiddenCollectionSummary,
|
||||
);
|
||||
|
||||
/**
|
||||
* The lower level primitive that the public API of this module delegates to.
|
||||
*/
|
||||
const downloadAndSave = async (
|
||||
files: EnteFile[],
|
||||
title: string,
|
||||
onAddSaveGroup: AddSaveGroup,
|
||||
collectionSummaryName?: string,
|
||||
collectionSummaryID?: number,
|
||||
isHiddenCollectionSummary?: boolean,
|
||||
) => {
|
||||
const electron = globalThis.electron;
|
||||
|
||||
let downloadDirPath: string | undefined;
|
||||
if (electron) {
|
||||
downloadDirPath = await electron.selectDirectory();
|
||||
if (!downloadDirPath) {
|
||||
// The user cancelled on the directory selection dialog.
|
||||
return;
|
||||
}
|
||||
if (collectionSummaryName) {
|
||||
downloadDirPath = await mkdirCollectionDownloadFolder(
|
||||
electron,
|
||||
downloadDirPath,
|
||||
collectionSummaryName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const canceller = new AbortController();
|
||||
const total = files.length;
|
||||
|
||||
const updateSaveGroup = onAddSaveGroup({
|
||||
title,
|
||||
collectionSummaryID,
|
||||
isHiddenCollectionSummary,
|
||||
downloadDirPath,
|
||||
total,
|
||||
canceller,
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
if (canceller.signal.aborted) break;
|
||||
try {
|
||||
if (electron && downloadDirPath) {
|
||||
await saveFileDesktop(electron, file, downloadDirPath);
|
||||
} else {
|
||||
await saveAsFile(file);
|
||||
}
|
||||
updateSaveGroup((g) => ({ ...g, success: g.success + 1 }));
|
||||
} catch (e) {
|
||||
log.error("File download failed", e);
|
||||
updateSaveGroup((g) => ({ ...g, failed: g.failed + 1 }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the given {@link EnteFile} as a file in the user's download folder.
|
||||
*/
|
||||
const saveAsFile = async (file: EnteFile) => {
|
||||
const fileBlob = await downloadManager.fileBlob(file);
|
||||
const fileName = fileFileName(file);
|
||||
if (file.metadata.fileType == FileType.livePhoto) {
|
||||
const { imageFileName, imageData, videoFileName, videoData } =
|
||||
await decodeLivePhoto(fileName, fileBlob);
|
||||
|
||||
await saveBlobPartAsFile(imageData, imageFileName);
|
||||
|
||||
// Downloading multiple works everywhere except, you guessed it,
|
||||
// Safari. Make up for their incompetence by adding a setTimeout.
|
||||
await wait(300) /* arbitrary constant, 300ms */;
|
||||
await saveBlobPartAsFile(videoData, videoFileName);
|
||||
} else {
|
||||
await saveBlobPartAsFile(fileBlob, fileName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the given {@link blob} as a file in the user's download folder.
|
||||
*/
|
||||
const saveBlobPartAsFile = async (blobPart: BlobPart, fileName: string) =>
|
||||
createTypedObjectURL(blobPart, fileName).then((url) =>
|
||||
saveAsFileAndRevokeObjectURL(url, fileName),
|
||||
);
|
||||
|
||||
const createTypedObjectURL = async (blobPart: BlobPart, fileName: string) => {
|
||||
const blob = blobPart instanceof Blob ? blobPart : new Blob([blobPart]);
|
||||
const { mimeType } = await detectFileTypeInfo(new File([blob], fileName));
|
||||
return URL.createObjectURL(new Blob([blob], { type: mimeType }));
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new directory on the user's file system with the same name as the
|
||||
* provided {@link collectionName} under the provided {@link downloadDirPath},
|
||||
* and return the full path to the created directory.
|
||||
*
|
||||
* This function can be used only when running in the context of our desktop
|
||||
* app, and so such requires an {@link Electron} instance as the witness.
|
||||
*/
|
||||
const mkdirCollectionDownloadFolder = async (
|
||||
{ fs }: Electron,
|
||||
downloadDirPath: string,
|
||||
collectionName: string,
|
||||
) => {
|
||||
const collectionDownloadName = await safeDirectoryName(
|
||||
downloadDirPath,
|
||||
collectionName,
|
||||
fs.exists,
|
||||
);
|
||||
const collectionDownloadPath = joinPath(
|
||||
downloadDirPath,
|
||||
collectionDownloadName,
|
||||
);
|
||||
await fs.mkdirIfNeeded(collectionDownloadPath);
|
||||
return collectionDownloadPath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save a file to the given {@link directoryPath} using native filesystem APIs.
|
||||
*
|
||||
* This is a sibling of {@link saveAsFile} for use when we are running in the
|
||||
* context of our desktop app. Unlike the browser, the desktop app can use
|
||||
* native file system APIs to efficiently write the files on disk without
|
||||
* needing to prompt the user for each write.
|
||||
*
|
||||
* @param electron An {@link Electron} instance, a witness to the fact that
|
||||
* we're running in the desktop app.
|
||||
*
|
||||
* @param file The {@link EnteFile} whose contents we want to save to the user's
|
||||
* file system.
|
||||
*
|
||||
* @param directoryPath The file system directory in which to save the file.
|
||||
*/
|
||||
const saveFileDesktop = async (
|
||||
electron: Electron,
|
||||
file: EnteFile,
|
||||
directoryPath: string,
|
||||
) => {
|
||||
const fs = electron.fs;
|
||||
|
||||
const createExportName = (fileName: string) =>
|
||||
safeFileName(directoryPath, fileName, fs.exists);
|
||||
|
||||
const writeStreamToFile = (
|
||||
exportName: string,
|
||||
stream: ReadableStream<Uint8Array> | null,
|
||||
) => writeStream(electron, joinPath(directoryPath, exportName), stream);
|
||||
|
||||
const stream = await downloadManager.fileStream(file);
|
||||
const fileName = fileFileName(file);
|
||||
|
||||
if (file.metadata.fileType == FileType.livePhoto) {
|
||||
const { imageFileName, imageData, videoFileName, videoData } =
|
||||
await decodeLivePhoto(fileName, await new Response(stream).blob());
|
||||
const imageExportName = await createExportName(imageFileName);
|
||||
await writeStreamToFile(imageExportName, new Response(imageData).body);
|
||||
try {
|
||||
await writeStreamToFile(
|
||||
await createExportName(videoFileName),
|
||||
new Response(videoData).body,
|
||||
);
|
||||
} catch (e) {
|
||||
await fs.rm(joinPath(directoryPath, imageExportName));
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
await writeStreamToFile(await createExportName(fileName), stream);
|
||||
}
|
||||
};
|
||||
@@ -91,7 +91,7 @@ const readNumericHeader = (res: Response, key: string) => {
|
||||
export const writeStream = async (
|
||||
_: Electron,
|
||||
path: string,
|
||||
stream: ReadableStream,
|
||||
stream: ReadableStream | null,
|
||||
) => {
|
||||
const params = new URLSearchParams({ path });
|
||||
const url = new URL(`stream://write?${params.toString()}`);
|
||||
|
||||