Compare commits

...

76 Commits

Author SHA1 Message Date
Prateek Sunal
109ac573c9 [mob] remove NativeAdapter to support big file upload (#5843)
## Description

Big file uploads fail when using NativeAdapter, this PR:
- [x] Removes NativeAdapter http client adapter

## Tests
2025-05-08 12:45:53 +05:30
Prateek Sunal
23559252e6 chore: bump version 2025-05-08 12:45:22 +05:30
Prateek Sunal
31b31b1a52 chore: lint update 2025-05-08 12:45:11 +05:30
Prateek Sunal
8333e2ad7b fix: not remove it for enteDio 2025-05-08 12:42:59 +05:30
Prateek Sunal
cb5c9f3170 chore: lint fix 2025-05-08 12:38:58 +05:30
Prateek Sunal
7b2e6cb1bd fix(network): remove NativeAdapter to support big file upload 2025-05-08 12:38:42 +05:30
Neeraj
d18d939489 [mob] Navigate to BackupSettings when tapping "Waiting for network" status (#5835)
## Description

## Tests
2025-05-08 11:28:59 +05:30
Neeraj Gupta
b3376f27aa Fixed typo 2025-05-08 11:27:01 +05:30
Vishnu Mohandas
f238b55df3 [docs] env, ports and more docs in /self-hosting (#5823) 2025-05-07 19:10:54 +05:30
mngshm
d15a034869 consistency (2) 2025-05-07 19:09:07 +05:30
mngshm
7b3ae417e8 consistency 2025-05-07 18:56:00 +05:30
Neeraj Gupta
e322958b25 Navigate to BackupSettings when tapping "Waiting for network" status 2025-05-07 16:53:02 +05:30
Neeraj
0d660f239f [mob] Bump version v1.0.9 (#5834)
## Description

Bumping version to prepare for next release.

## Tests
2025-05-07 16:24:46 +05:30
Prateek Sunal
c4a50fc9fb chore: bump version to 1039 2025-05-07 16:15:41 +05:30
mngshm
8856ad1520 Sidebar 2025-05-07 13:30:40 +05:30
mangesh
e8158ef45a [staff] show family member storage quota (#5770) 2025-05-07 10:55:02 +05:30
Manav Rathi
4fa0bf76e8 [desktop] Generisize the creation of utility processes (#5829)
ffmpeg code about to become one
2025-05-06 18:58:41 +05:30
Manav Rathi
92a9b34836 Generisize 2025-05-06 18:52:03 +05:30
Manav Rathi
10d7162d6e Rename 2025-05-06 18:00:10 +05:30
Manav Rathi
2a1b8ae18e Generalize 2025-05-06 17:39:48 +05:30
Manav Rathi
5abf2cb35e Extract 2025-05-06 17:24:03 +05:30
Manav Rathi
367170be95 [desktop] Video stream generation - WIP Part x/x (#5827) 2025-05-06 17:05:48 +05:30
Manav Rathi
4d7cfee60f Fix slowness 2025-05-06 16:03:13 +05:30
Prateek Sunal
29152d1f85 [mob] bump to 1038 (#5817)
## Description

Bump version to 1038

## Tests
2025-05-06 15:39:25 +05:30
Neeraj
6b4ffa4822 [mob][photos] Fix share to Ente (#5821)
## Description

Fix [issue with sharing to
Ente](https://github.com/ente-io/ente/discussions/5755)

## Tests
2025-05-06 15:38:32 +05:30
Manav Rathi
2883f4bed6 Tweak 2025-05-06 15:08:52 +05:30
laurenspriem
c96275cdd1 Update load message 2025-05-06 14:48:57 +05:30
Manav Rathi
9db8324ffd Sketch 2025-05-06 14:42:23 +05:30
mngshm
0c664b94b9 Make storageLimit nullable and display 'NA' 2025-05-06 14:41:42 +05:30
Manav Rathi
c087e419d5 Outline 2025-05-06 13:00:41 +05:30
mngshm
5ba5cae5ef mark redirection info as IMPORTANT in doc 2025-05-06 12:50:27 +05:30
Manav Rathi
4ea211d923 Sketch interruptible loop 2025-05-06 12:32:57 +05:30
mngshm
8d8202adab Env and Ports 2025-05-06 12:13:48 +05:30
mngshm
267f93e41e Merge branch 'main' into fam 2025-05-06 11:05:10 +05:30
Manav Rathi
260ec952b4 Not needed 2025-05-06 10:14:28 +05:30
Prateek Sunal
5e311c2813 fix: bump to 1038 2025-05-05 20:53:51 +05:30
Prateek Sunal
1d3268916f [mob] fix ffmpeg-kit android compilation (#5813)
## Description

- [x] Fix failing android build
- [x] Don't redirect to Backup Status screen when "Preview Failed"
status is pressed.

## Tests

- [x] Test if app works and everything is fine
2025-05-05 20:23:22 +05:30
Prateek Sunal
73192cd0fd fix: remove unused import and simplify navigation logic in PreviewStatusWidget 2025-05-05 20:21:55 +05:30
Prateek Sunal
9c886b3fa3 fix: update ffmpeg kit resolved reference in pubspec.lock 2025-05-05 20:01:39 +05:30
Prateek Sunal
017832f11e feat: update ffmpeg kit source 2025-05-05 18:38:49 +05:30
Prateek Sunal
67e76bc42f chore: update locals 2025-05-05 18:38:37 +05:30
laurenspriem
9a6579c55c Refactor 2025-05-05 17:30:12 +05:30
laurenspriem
17c0cdef14 Fix backup share issue 2025-05-05 17:21:39 +05:30
mngshm
dd0cc71f36 Minor 2025-05-05 16:37:29 +05:30
mangesh
21fd6ab463 [staff] match title casing to key in the UserData interface (#5812) 2025-05-05 16:36:43 +05:30
mngshm
6e2142c605 match title casing to key in the UserData interface 2025-05-05 16:28:23 +05:30
Manav Rathi
16338682ed [docs] Mention UNC path workaround to create network drive (#5811) 2025-05-05 15:58:05 +05:30
Manav Rathi
a7e8d3dfa6 [docs] Mention UNC path workaround to create network drive 2025-05-05 15:51:03 +05:30
Manav Rathi
6e9014b915 [desktop] Tweak the backfill behaviour in case of transients (#5809) 2025-05-05 15:34:24 +05:30
Neeraj
b5e7a3f83f [mob] Bump version v1.0.7 (#5810)
## Description

## Tests
2025-05-05 15:24:19 +05:30
Neeraj Gupta
d8d76f452d Bump version v1.0.7 2025-05-05 15:23:14 +05:30
Laurens Priem
c2e475c666 Face thumbnail logging (#5808)
## Description

Change logging flow for face thumbnail generation
2025-05-05 15:03:06 +05:30
Manav Rathi
9a4bc898f0 [desktop] Tweak the backfill behaviour in case of transients 2025-05-05 15:02:28 +05:30
laurenspriem
ca92aa8c62 Include delay 2025-05-05 14:59:53 +05:30
laurenspriem
56c6d7ed3c Remove redundant reset 2025-05-05 14:49:57 +05:30
mangesh
6ee4bce676 Merge branch 'main' into fam 2025-05-05 14:47:44 +05:30
laurenspriem
ff3f01af56 Increase queue size 2025-05-05 14:47:05 +05:30
Laurens Priem
b5ba81e22b [mob][photos] Fix memories update regression (#5807)
## Description

Fixed regression in memories update scheme.
2025-05-05 14:23:04 +05:30
laurenspriem
d5aab7c6df Fix memories update regression 2025-05-05 14:18:58 +05:30
Manav Rathi
2749457611 [web] Ensure copy as PNG option is reset when we get the original (#5806)
Fixes: https://github.com/ente-io/ente/discussions/5802
2025-05-05 14:02:22 +05:30
Manav Rathi
883b14e96a [web] Ensure copy as PNG option is reset when we get the original
Fixes: https://github.com/ente-io/ente/discussions/5802
2025-05-05 13:58:13 +05:30
Neeraj
59d7e0acac [mobile] New translations (#5799)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-app)
2025-05-05 13:39:10 +05:30
Neeraj
68ac3503ed [server] Use access ctrl for verifying access (#5801)
## Description

## Tests
2025-05-05 13:33:26 +05:30
mngshm
58649db181 fix Linters in UpdateSubscription Component 2025-05-05 13:25:21 +05:30
mangesh
92ca4eeb15 [staff] consolidate and separate interfaces (#5765)
The codebase was too cluttered with interfaces spread all over the
codebase. Separated all the commonly usable types into a single
`types/index.ts` file. Some types which are only usable in that
particular component are left untouched.

P.S: Inspiration from families codebase.
2025-05-05 13:21:12 +05:30
Manav Rathi
d3e06e6cc9 [web] Ensure ellipsizing of caption (#5805)
`text-align: right` causes the ellipsizing to sometimes work, sometimes
not, depending on the exact contents of the line (tested in current
Chrome). Tweak the design to work with the normal text align to try and
ensure the elision is always ellipsized.
2025-05-05 13:19:22 +05:30
Manav Rathi
3cef3e9bdc [web] Ensure ellipsizing of caption
`text-align: right` causes the ellipsizing to sometimes work, sometimes not,
depending on the exact contents of the line (tested in current Chrome). Tweak
the design to work with the normal text align to try and ensure the ellision is
always ellipsized.
2025-05-05 13:12:58 +05:30
mangesh
d318952feb [quickstart] Gracefully handle case when docker compose is not present (#5804)
When docker is present but docker compose is not present, the `docker
compose` invocation would fail. We want the early exit (`set -e`), so
instead do a fallback to set dcv to an empty string so that it later
fails in the `test -z dcv` case below and prints the intended error
message.
2025-05-05 13:08:49 +05:30
Manav Rathi
6d8051dfa0 [quickstart] Gracefully handle case when docker compose is not present
When docker is present but docker compose is not present, the `docker compose`
invocation would fail. We want the early exit (`set -e`), so instead do a
fallback to set dcv to an empty string so that it later fails in the `test -z
dcv` case below and prints the intended error message.
2025-05-05 12:37:48 +05:30
Neeraj Gupta
d198f0c273 Use access ctrl for verifying access 2025-05-05 10:01:33 +05:30
Crowdin Bot
a88249de09 New Crowdin translations by GitHub Action 2025-05-05 01:05:08 +00:00
mngshm
0a3abb20a1 making linters happy 2025-04-30 20:10:29 +05:30
mngshm
9f9288a5c0 show family member storage quota 2025-04-30 19:59:11 +05:30
mngshm
100c1d3803 use nullish coalescing to avoid optional chaining 2025-04-30 15:57:24 +05:30
mngshm
408cc05f7c fix: usage conversion import 2025-04-30 15:28:36 +05:30
mngshm
9f70aab9b5 refactor: consolidate and separate interfaces 2025-04-30 15:24:51 +05:30
60 changed files with 1344 additions and 479 deletions

View File

@@ -16,6 +16,7 @@ import type {
FFmpegCommand,
FolderWatch,
PendingUploads,
UtilityProcessType,
ZipItem,
} from "../types/ipc";
import { logToDisk } from "./log";
@@ -47,7 +48,6 @@ import {
} from "./services/fs";
import { convertToJPEG, generateImageThumbnail } from "./services/image";
import { logout } from "./services/logout";
import { createMLWorker } from "./services/ml";
import {
lastShownChangelogVersion,
masterKeyB64,
@@ -70,6 +70,7 @@ import {
watchUpdateIgnoredFiles,
watchUpdateSyncedFiles,
} from "./services/watch";
import { triggerCreateUtilityProcess } from "./services/workers";
/**
* Listen for IPC events sent/invoked by the renderer process, and route them to
@@ -233,9 +234,11 @@ export const attachIPCHandlers = () => {
* the main window to do their thing.
*/
export const attachMainWindowIPCHandlers = (mainWindow: BrowserWindow) => {
// - ML
// - Utility processes
ipcMain.on("createMLWorker", () => createMLWorker(mainWindow));
ipcMain.on("triggerCreateUtilityProcess", (_, type: UtilityProcessType) =>
triggerCreateUtilityProcess(type, mainWindow),
);
};
/**

View File

@@ -0,0 +1,59 @@
/**
* A object that behaves similar to the default export of "./log", except this
* can be used from within a utility process.
*
* ---
*
* We cannot directly do
*
* import log from "../log";
*
* because that requires the Electron APIs that are not available to a utility
* process (See: [Note: Using Electron APIs in UtilityProcess]).
*
* But even if that were to work, logging will still be problematic since we'd
* try opening the log file from two different Node.js processes (this one, and
* the main one), and I didn't find any indication in the electron-log
* repository that the log file's integrity would be maintained in such cases.
*
* So instead we provide this proxy log object that uses the
* `process.parentPort` to transport the logs over to the main process, where
* the {@link processUtilityProcessLogMessage} function in the main process is
* expected to handle these (sending them to the actual log).
*/
export default {
error: (s: string, e?: unknown) =>
mainProcess("log.errorString", messageWithError(s, e)),
warn: (s: string, e?: unknown) =>
mainProcess("log.warnString", messageWithError(s, e)),
info: (...ms: unknown[]) => mainProcess("log.info", ms),
/**
* Unlike the real {@link log.debug}, this is (a) eagerly evaluated, and (b)
* accepts only strings.
*/
debugString: (s: string) => mainProcess("log.debugString", s),
};
/**
* Send a message to the main process using a barebones RPC protocol.
*/
const mainProcess = (method: string, param: unknown) =>
process.parentPort.postMessage({ method, p: param });
// Duplicated verbatim from ./log.ts
const messageWithError = (message: string, e?: unknown) => {
if (!e) return message;
let es: string;
if (e instanceof Error) {
// In practice, we expect ourselves to be called with Error objects, so
// this is the happy path so to say.
es = [`${e.name}: ${e.message}`, e.stack].filter((x) => x).join("\n");
} else {
// For the rest rare cases, use the default string serialization of e.
// eslint-disable-next-line @typescript-eslint/no-base-to-string
es = String(e);
}
return `${message}: ${es}`;
};

View File

@@ -83,6 +83,56 @@ const logDebug = (param: () => unknown) => {
}
};
/**
* Handle log messages posted from the utility process in the main process.
*
* See: [Note: Using Electron APIs in UtilityProcess]
*
* @param message The arbitrary message that was received as an argument to the
* "message" event invoked on a {@link UtilityProcess}.
*
* @returns true if the message was recognized and handled, and false otherwise.
*/
export const processUtilityProcessLogMessage = (
logTag: string,
message: unknown,
) => {
const m = message; /* shorter alias */
if (m && typeof m == "object" && "method" in m && "p" in m) {
const p = m.p;
switch (m.method) {
case "log.errorString":
if (typeof p == "string") {
logError(`${logTag} ${p}`);
return true;
}
break;
case "log.warnString":
if (typeof p == "string") {
logWarn(`${logTag} ${p}`);
return true;
}
break;
case "log.info":
if (Array.isArray(p)) {
// Need to cast from any[] to unknown[]
logInfo(logTag, ...(p as unknown[]));
return true;
}
break;
case "log.debugString":
if (typeof p == "string") {
logDebug(() => `${logTag} ${p}`);
return true;
}
break;
default:
break;
}
}
return false;
};
/**
* Ente's logger.
*

View File

@@ -15,47 +15,13 @@ import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "node:path";
import * as ort from "onnxruntime-node";
import log from "../log-worker";
import { messagePortMainEndpoint } from "../utils/comlink";
import { wait } from "../utils/common";
import { writeStream } from "../utils/stream";
import { fsStatMtime } from "./fs";
/**
* We cannot do
*
* import log from "../log";
*
* because that requires the Electron APIs that are not available to a utility
* process (See: [Note: Using Electron APIs in UtilityProcess]). But even if
* that were to work, logging will still be problematic since we'd try opening
* the log file from two different Node.js processes (this one, and the main
* one), and I didn't find any indication in the electron-log repository that
* the log file's integrity would be maintained in such cases.
*
* So instead we create this proxy log object that uses `process.parentPort` to
* transport the logs over to the main process.
*/
const log = {
/**
* Unlike the real {@link log.error}, this accepts only the first string
* argument, not the second optional error one.
*/
errorString: (s: string) => mainProcess("log.errorString", s),
info: (...ms: unknown[]) => mainProcess("log.info", ms),
/**
* Unlike the real {@link log.debug}, this is (a) eagerly evaluated, and (b)
* accepts only strings.
*/
debugString: (s: string) => mainProcess("log.debugString", s),
};
/**
* Send a message to the main process using a barebones RPC protocol.
*/
const mainProcess = (method: string, param: unknown) =>
process.parentPort.postMessage({ method, p: param });
log.debugString(`Started ML worker process`);
log.debugString("Started ML utility process");
process.parentPort.once("message", (e) => {
// Initialize ourselves with the data we got from our parent.
@@ -93,7 +59,7 @@ const parseInitData = (data: unknown) => {
) {
_userDataPath = data.userDataPath;
} else {
log.errorString("Unparseable initialization data");
log.error("Unparseable initialization data");
}
};
@@ -161,7 +127,7 @@ const modelPathDownloadingIfNeeded = async (
} else {
const size = (await fs.stat(modelPath)).size;
if (size !== expectedByteSize) {
log.errorString(
log.error(
`The size ${size} of model ${modelName} does not match the expected size, downloading again`,
);
await downloadModel(modelPath, modelName);

View File

@@ -1,5 +1,6 @@
/**
* @file ML related functionality. This code runs in the main process.
* @file This main process code and interface for dealing with the various
* utility processes that we create.
*/
import {
@@ -9,13 +10,14 @@ import {
} from "electron";
import { app, utilityProcess } from "electron/main";
import path from "node:path";
import log from "../log";
import type { UtilityProcessType } from "../../types/ipc";
import log, { processUtilityProcessLogMessage } from "../log";
/** The active ML worker (utility) process, if any. */
/** The active ML utility process, if any. */
let _child: UtilityProcess | undefined;
/**
* Create a new ML worker process, terminating the older ones (if any).
* Create a new ML utility process, terminating the older ones (if any).
*
* [Note: ML IPC]
*
@@ -36,7 +38,7 @@ let _child: UtilityProcess | undefined;
* does not forward events to the renderer, causing the UI to jitter.
*
* The solution for this is to spawn an Electron UtilityProcess, which we can
* think of a regular Node.js child process. This frees up the Node.js main
* think of a regular Node.js child process. This frees up the Node.js main
* process, and would remove the jitter.
* https://www.electronjs.org/docs/latest/tutorial/process-model
*
@@ -70,9 +72,21 @@ let _child: UtilityProcess | undefined;
* The RPC protocol is handled using comlink on both ends. The port itself needs
* to be relayed using `postMessage`.
*/
export const createMLWorker = (window: BrowserWindow) => {
export const triggerCreateUtilityProcess = (
type: UtilityProcessType,
window: BrowserWindow,
) => {
switch (type) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
case "ml":
triggerCreateMLUtilityProcess(window);
break;
}
};
export const triggerCreateMLUtilityProcess = (window: BrowserWindow) => {
if (_child) {
log.debug(() => "Terminating previous ML worker process");
log.debug(() => "Terminating previous ML utility process");
_child.kill();
_child = undefined;
}
@@ -83,7 +97,7 @@ export const createMLWorker = (window: BrowserWindow) => {
const userDataPath = app.getPath("userData");
child.postMessage({ userDataPath }, [port1]);
window.webContents.postMessage("createMLWorker/port", undefined, [port2]);
window.webContents.postMessage("utilityProcessPort/ml", undefined, [port2]);
handleMessagesFromUtilityProcess(child);
@@ -114,34 +128,10 @@ export const createMLWorker = (window: BrowserWindow) => {
* we use the `parentPort` in the utility process.
*/
const handleMessagesFromUtilityProcess = (child: UtilityProcess) => {
const logTag = "[ml-worker]";
child.on("message", (m: unknown) => {
if (m && typeof m == "object" && "method" in m && "p" in m) {
const p = m.p;
switch (m.method) {
case "log.errorString":
if (typeof p == "string") {
log.error(`${logTag} ${p}`);
return;
}
break;
case "log.info":
if (Array.isArray(p)) {
// Need to cast from any[] to unknown[]
log.info(logTag, ...(p as unknown[]));
return;
}
break;
case "log.debugString":
if (typeof p == "string") {
log.debug(() => `${logTag} ${p}`);
return;
}
break;
default:
break;
}
if (processUtilityProcessLogMessage("[ml-worker]", m)) {
return;
}
log.info("Ignoring unknown message from ML worker", m);
log.info("Ignoring unknown message from ML utility process", m);
});
};

View File

@@ -332,14 +332,13 @@ const handleGenerateHLSWrite = async (
return new Response(null, { status: 204 });
}
const { playlistPath, videoPath } = result;
const { playlistPath, videoPath, videoSize, dimensions } = result;
try {
await uploadVideoSegments(videoPath, objectUploadURL);
await uploadVideoSegments(videoPath, videoSize, objectUploadURL);
const playlistToken = randomUUID();
pendingVideoResults.set(playlistToken, playlistPath);
const { dimensions, videoSize } = result;
return new Response(
JSON.stringify({ playlistToken, dimensions, videoSize }),
{ status: 200 },
@@ -364,19 +363,32 @@ const handleGenerateHLSWrite = async (
*
* See: [Note: Upload HLS video segment from node side].
*
* @param videoFilePath The path to the file on the user's file system to
* upload.
*
* @param videoSize The size in bytes of the file at {@link videoFilePath}.
*
* @param objectUploadURL A pre-signed URL to upload the file.
*
* ---
*
* This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOkOr4xx`
* from `web/packages/base/http.ts` (we don't have the rest of the scaffolding
* used by that function, which is why it is inlined bespoked).
* from `web/packages/base/http.ts`
*
* It handles the specific use case of uploading videos since generating the HLS
* stream is a fairly expensive operation, so a retry to discount transient
* network issues is called for. There are only 2 retries for a total of 3
* attempts, and the retry gaps are more spaced out.
* - We don't have the rest of the scaffolding used by that function, which is
* why it is intially inlined bespoked.
*
* - It handles the specific use case of uploading videos since generating the
* HLS stream is a fairly expensive operation, so a retry to discount
* transient network issues is called for. There are only 2 retries for a
* total of 3 attempts, and the retry gaps are more spaced out.
*
* - Later it was discovered that net.fetch is much slower than node's native
* fetch, so this implementation has further diverged.
*/
export const uploadVideoSegments = async (
videoFilePath: string,
videoSize: number,
objectUploadURL: string,
) => {
const waitTimeBeforeNextTry = [5000, 20000];
@@ -387,8 +399,17 @@ export const uploadVideoSegments = async (
const nodeStream = fs_.createReadStream(videoFilePath);
const webStream = Readable.toWeb(nodeStream);
const res = await net.fetch(objectUploadURL, {
// net.fetch is 40-50x slower than the native fetch for this
// particular PUT request. This is easily reproducible (replace
// `fetch` with `net.fetch`, then even on localhost the PUT requests
// start taking a minute or so; with node's native fetch, it is
// second(s)).
const res = await fetch(objectUploadURL, {
method: "PUT",
// net.fetch apparently deduces and inserts a content-length,
// because when we use the node native fetch then we need to
// provide it explicitly.
headers: { "Content-Length": `${videoSize}` },
// The duplex option is required since we're passing a stream.
//
// @ts-expect-error TypeScript's libdom.d.ts does not include

View File

@@ -69,6 +69,7 @@ import type {
FFmpegCommand,
FolderWatch,
PendingUploads,
UtilityProcessType,
ZipItem,
} from "./types/ipc";
@@ -215,18 +216,19 @@ const ffmpegExec = (
outputFileExtension,
);
// - ML
// - Utility processes
const createMLWorker = () => {
const triggerCreateUtilityProcess = (type: UtilityProcessType) => {
const portEvent = `utilityProcessPort/${type}`;
const l = (event: IpcRendererEvent) => {
void windowLoaded.then(() => {
// "*"" is the origin to send to.
window.postMessage("createMLWorker/port", "*", event.ports);
ipcRenderer.off("createMLWorker/port", l);
window.postMessage(portEvent, "*", event.ports);
ipcRenderer.off(portEvent, l);
});
};
ipcRenderer.on("createMLWorker/port", l);
ipcRenderer.send("createMLWorker");
ipcRenderer.on(portEvent, l);
ipcRenderer.send("triggerCreateUtilityProcess", type);
};
// - Watch
@@ -393,7 +395,7 @@ contextBridge.exposeInMainWorld("electron", {
// - ML
createMLWorker,
triggerCreateUtilityProcess,
// - Watch

View File

@@ -5,6 +5,8 @@
* See [Note: types.ts <-> preload.ts <-> ipc.ts]
*/
export type UtilityProcessType = "ml";
export interface AppUpdate {
autoUpdatable: boolean;
version: string;

View File

@@ -339,6 +339,10 @@ export const sidebar = [
text: "Backups",
link: "/self-hosting/faq/backup",
},
{
text: "Environment variables",
link: "/self-hosting/faq/environment",
},
],
},
],

View File

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

View File

@@ -0,0 +1,49 @@
---
title: "Environment Variables and Ports"
description: "Information about all the Environment Variables needed to run Ente"
---
# Environment variables and ports
A self-hosted Ente instance requires specific endpoints in both Museum (the server) and web apps. This document outlines the essential environment variables and port mappings of the web apps.
Here's the list of important variables that a self hoster should know about:
### Museum
1. `NEXT_PUBLIC_ENTE_ENDPOINT`
The above environment variable is used to configure Museums endpoint. Where Museum is
running and which port it is listening on. This endpoint should be configured for
all the apps to connect to your self hosted endpoint.
All the apps (regardless of platform) by default connect to api.ente.io - which is
our production instance of Museum.
### Web Apps
> [!IMPORTANT]
> Web apps don't need to be configured with the below endpoints. Web app environment
> variables are being documented here just so that the users know everything in detail.
> Checkout [Configuring your Server](/self-hosting/museum) to configure endpoints for
> particular app.
In Ente, all the web apps are separate NextJS applications. Therefore, they are all
configured via environment variables. The photos app (Ente Photos) has information
about and connects to other web apps like albums, cast, etc.
1. `NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT`
This environment variable is used to configure and declare the endpoint for the Albums
web app.
## Ports
The below format is according to how ports are mapped in Docker.
Typically,`<host>:<container-port>`
1. `8080:8080`: Museum (Ente's server)
2. `3000:3000`: Ente Photos web app
3. `3001:3001`: Ente Accounts web app
4. `3003:3003`: [EEnte Auth](https://ente.io/auth/)
5. `3004:3004`: [Ente Cast web app](http://ente.io/cast)

View File

@@ -55,7 +55,11 @@ apps:
family: family.myente.xyz
```
By default, all the values redirect to our publicly hosted production services.
>[!IMPORTANT]
>By default, all the values redirect to our publicly hosted production services.
>For example, if `public-albums` is not configured your shared album will
>use the `albums.ente.io` URL.
After you are done with filling the values, restart museum and the app will
start utilizing those endpoints instead of Ente's production instances.
@@ -67,3 +71,7 @@ Similarly, you can use the default
[`local.yaml`](https://github.com/ente-io/ente/tree/main/server/configurations/local.yaml)
as a reference for building a functioning `museum.yaml` for many other
functionalities like SMTP, Discord notifications, Hardcoded-OTTs, etc.
## References
- [Environment variables and ports](/self-hosting/faq/environment)

View File

@@ -10,10 +10,10 @@ import "./App.css";
import FamilyTableComponent from "./components/FamilyComponentTable";
import StorageBonusTableComponent from "./components/StorageBonusTableComponent";
import TokensTableComponent from "./components/TokenTableComponent";
import type { UserData } from "./components/UserComponent";
import UserComponent from "./components/UserComponent";
import duckieimage from "./components/duckie.png";
import { apiOrigin } from "./services/support";
import type { UserData, UserResponse } from "./types";
export let email = "";
export let token = "";
@@ -29,38 +29,6 @@ export const setToken = (newToken: string) => {
export const getEmail = () => email;
export const getToken = () => token;
interface User {
ID: string;
email: string;
creationTime: number;
}
interface Subscription {
productID: string;
paymentProvider: string;
expiryTime: number;
storage: number;
}
interface Security {
isEmailMFAEnabled: boolean;
isTwoFactorEnabled: boolean;
passkeys: string;
passkeyCount: number;
canDisableEmailMFA: boolean;
}
interface UserResponse {
user: User;
subscription: Subscription;
authCodes?: number;
details?: {
usage?: number;
storageBonus?: number;
profileData: Security;
};
}
const App: React.FC = () => {
const [localEmail, setLocalEmail] = useState<string>("");
const [localToken, setLocalToken] = useState<string>("");
@@ -139,7 +107,7 @@ const App: React.FC = () => {
console.log("API Response:", userDataResponse);
const extractedUserData: UserData = {
User: {
user: {
"User ID": userDataResponse.user.ID || "None",
Email: userDataResponse.user.email || "None",
"Creation time":
@@ -147,7 +115,7 @@ const App: React.FC = () => {
userDataResponse.user.creationTime / 1000,
).toLocaleString() || "None",
},
Storage: {
storage: {
Total: userDataResponse.subscription.storage
? userDataResponse.subscription.storage >= 1024 ** 3
? `${(userDataResponse.subscription.storage / 1024 ** 3).toFixed(2)} GB`
@@ -166,7 +134,7 @@ const App: React.FC = () => {
: `${(userDataResponse.details.storageBonus / 1024 ** 2).toFixed(2)} MB`
: "None",
},
Subscription: {
subscription: {
"Product ID":
userDataResponse.subscription.productID || "None",
Provider:
@@ -176,7 +144,7 @@ const App: React.FC = () => {
userDataResponse.subscription.expiryTime / 1000,
).toLocaleString() || "None",
},
Security: {
security: {
"Email MFA": userDataResponse.details?.profileData
.isEmailMFAEnabled
? "Enabled"

View File

@@ -10,10 +10,10 @@ import {
import React, { useEffect, useState } from "react";
import { getEmail, getToken } from "../App";
import { apiOrigin } from "../services/support";
interface ErrorResponse {
message: string;
}
import type { ErrorResponse } from "../types";
// The below interfaces will only be used in this file
// hence not including them into a sub-merged types file
interface ChangeEmailProps {
open: boolean;
onClose: () => void;

View File

@@ -10,14 +10,7 @@ import {
import React, { useState } from "react";
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
import { apiOrigin } from "../services/support";
interface UserData {
subscription?: {
userID: string;
// Add other properties as per your API response structure
};
// Add other properties as per your API response structure
}
import type { UserData } from "../types";
interface CloseFamilyProps {
open: boolean;

View File

@@ -10,14 +10,7 @@ import {
import React, { useState } from "react";
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
import { apiOrigin } from "../services/support";
interface UserData {
subscription?: {
userID: string;
// Add other properties as per your API response structure
};
// Add other properties as per your API response structure
}
import type { UserData } from "../types";
interface Disable2FAProps {
open: boolean;

View File

@@ -10,20 +10,7 @@ import {
import React, { useState } from "react";
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
import { apiOrigin } from "../services/support";
interface UserData {
subscription?: {
userID: string;
// Add other properties as per your API response structure
};
// Add other properties as per your API response structure
}
interface DisablePasskeysProps {
open: boolean;
handleClose: () => void;
handleDisablePasskeys: () => void; // Callback to handle disabling passkeys
}
import type { DisablePasskeysProps, UserData } from "../types";
const DisablePasskeys: React.FC<DisablePasskeysProps> = ({
open,

View File

@@ -13,23 +13,10 @@ import * as React from "react";
import { useEffect, useState } from "react";
import { getEmail, getToken } from "../App";
import { apiOrigin } from "../services/support";
import type { FamilyMember, UserData } from "../types";
import { formatUsageToGB } from "../utils/";
import CloseFamily from "./CloseFamily";
interface FamilyMember {
id: string;
email: string;
status: string;
usage: number;
}
interface UserData {
details: {
familyData: {
members: FamilyMember[];
};
};
}
const FamilyTableComponent: React.FC = () => {
const [familyMembers, setFamilyMembers] = useState<FamilyMember[]>([]);
const [closeFamilyOpen, setCloseFamilyOpen] = useState(false);
@@ -54,7 +41,7 @@ const FamilyTableComponent: React.FC = () => {
}
const userData = (await response.json()) as UserData; // Typecast to UserData interface
const members: FamilyMember[] =
userData.details.familyData.members;
userData.details?.familyData.members ?? [];
setFamilyMembers(members);
} catch (error) {
console.error("Error fetching family data:", error);
@@ -69,11 +56,6 @@ const FamilyTableComponent: React.FC = () => {
);
}, []);
const formatUsageToGB = (usage: number): string => {
const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2);
return `${usageInGB} GB`;
};
const handleOpenCloseFamily = () => {
setCloseFamilyOpen(true);
};
@@ -111,6 +93,9 @@ const FamilyTableComponent: React.FC = () => {
<Table aria-label="family-table">
<TableHead>
<TableRow>
<TableCell>
<b>ID</b>
</TableCell>
<TableCell>
<b>User</b>
</TableCell>
@@ -121,13 +106,14 @@ const FamilyTableComponent: React.FC = () => {
<b>Usage</b>
</TableCell>
<TableCell>
<b>ID</b>
<b>Quota</b>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{familyMembers.map((member) => (
<TableRow key={member.id}>
<TableCell>{member.id}</TableCell>
<TableCell>{member.email}</TableCell>
<TableCell>
<span
@@ -152,7 +138,15 @@ const FamilyTableComponent: React.FC = () => {
<TableCell>
{formatUsageToGB(member.usage)}
</TableCell>
<TableCell>{member.id}</TableCell>
<TableCell>
{member.status !== "SELF"
? (member.storageLimit &&
formatUsageToGB(
member.storageLimit,
)) ||
"NA"
: ""}
</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -10,14 +10,7 @@ import {
import React, { useState } from "react";
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
import { apiOrigin } from "../services/support";
interface UserData {
subscription?: {
userID: string;
// Add other properties as per your API response structure
};
// Add other properties as per your API response structure
}
import type { UserData } from "../types";
interface ToggleEmailMFAProps {
open: boolean;

View File

@@ -62,8 +62,8 @@ const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
expiryTime: "",
userId: "",
attributes: {
"customerID": "",
"stripeAccountCountry": ""
customerID: "",
stripeAccountCountry: "",
},
});
@@ -108,9 +108,13 @@ const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
expiryTime: expiryTime,
userId: userDataResponse.subscription.userID || "",
attributes: {
customerID: userDataResponse.subscription.attributes.customerID || "",
stripeAccountCountry: userDataResponse.subscription.attributes.stripeAccountCountry || ""
}
customerID:
userDataResponse.subscription.attributes
.customerID || "",
stripeAccountCountry:
userDataResponse.subscription.attributes
.stripeAccountCountry || "",
},
});
} catch (error) {
console.error("Error fetching data:", error);
@@ -174,8 +178,9 @@ const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
transactionId: values.transactionId,
attributes: {
customerID: values.attributes.customerID,
stripeAccountCountry: values.attributes.stripeAccountCountry
}
stripeAccountCountry:
values.attributes.stripeAccountCountry,
},
};
try {

View File

@@ -13,6 +13,7 @@ import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import * as React from "react";
import type { UserComponentProps } from "../types";
import ChangeEmail from "./ChangeEmail";
import DeleteAccount from "./DeleteAccont";
import Disable2FA from "./Disable2FA";
@@ -20,17 +21,6 @@ import DisablePasskeys from "./DisablePasskeys";
import ToggleEmailMFA from "./ToggleEmailMFA";
import UpdateSubscription from "./UpdateSubscription";
export interface UserData {
User: Record<string, string>;
Storage: Record<string, string>;
Subscription: Record<string, string>;
Security: Record<string, string>;
}
interface UserComponentProps {
userData: UserData | null;
}
const UserComponent: React.FC<UserComponentProps> = ({ userData }) => {
const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false);
const [email2FAEnabled, setEmail2FAEnabled] = React.useState(false);
@@ -44,10 +34,10 @@ const UserComponent: React.FC<UserComponentProps> = ({ userData }) => {
const [disablePasskeysOpen, setDisablePasskeysOpen] = React.useState(false);
React.useEffect(() => {
setTwoFactorEnabled(userData?.Security["Two factor 2FA"] === "Enabled");
setEmail2FAEnabled(userData?.Security["Email MFA"] === "Enabled");
setTwoFactorEnabled(userData?.security["Two factor 2FA"] === "Enabled");
setEmail2FAEnabled(userData?.security["Email MFA"] === "Enabled");
setCanDisableEmailMFA(
userData?.Security["Can Disable EmailMFA"] === "Yes",
userData?.security["Can Disable EmailMFA"] === "Yes",
);
}, [userData]);
@@ -148,14 +138,10 @@ const DataTable: React.FC<DataTableProps> = ({
minHeight: 300,
display: "flex",
flexDirection: "column",
marginBottom: "20px",
height: "100%",
width: "100%",
padding: "13px",
padding: "10px",
overflowX: "hidden",
"&:not(:last-child)": {
marginBottom: "40px",
},
}}
>
<Box
@@ -176,9 +162,9 @@ const DataTable: React.FC<DataTableProps> = ({
width: "100%",
}}
>
{title}
{title.charAt(0).toUpperCase() + title.slice(1)}
</Typography>
{title === "User" && (
{title === "user" && (
<IconButton
edge="start"
aria-label="delete"
@@ -187,7 +173,7 @@ const DataTable: React.FC<DataTableProps> = ({
<DeleteIcon style={{ color: "" }} />
</IconButton>
)}
{title === "Subscription" && (
{title === "subscription" && (
<IconButton
edge="end"
aria-label="edit"

View File

@@ -0,0 +1,71 @@
// Type related Users
export interface User {
ID: string;
email: string;
creationTime: number;
}
export interface UserResponse {
user: User;
subscription: Subscription;
authCodes?: number;
details?: {
usage?: number;
storageBonus?: number;
profileData: Security;
};
}
export interface UserData {
user: Record<string, string>;
storage: Record<string, string>;
subscription?: Record<string, string>;
security: Record<string, string>;
details?: {
familyData: {
members: FamilyMember[];
};
};
}
export interface UserComponentProps {
userData: UserData | null;
}
// Error Response Interface
export interface ErrorResponse {
message: string;
}
// Types related to Subscriptions
export interface Subscription {
productID: string;
paymentProvider: string;
expiryTime: number;
storage: number;
}
export interface Security {
isEmailMFAEnabled: boolean;
isTwoFactorEnabled: boolean;
passkeys: string;
passkeyCount: number;
canDisableEmailMFA: boolean;
}
// Types related Family
export interface FamilyMember {
id: string;
email: string;
status: string;
usage: number;
storageLimit: number;
}
// Types related to passkeys
export interface DisablePasskeysProps {
open: boolean;
handleClose: () => void;
handleDisablePasskeys: () => void; // Callback to handle disabling passkeys
}

View File

@@ -0,0 +1,5 @@
// Common utilities
export function formatUsageToGB(usage: number): string {
const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2);
return `${usageInGB} GB`;
}

View File

@@ -1,13 +0,0 @@
/**
* User facing strings in the app.
*
* By keeping them separate, we make our lives easier if/when we need to
* localize the corresponding pages. Right now, these are just the values in the
* default language, English.
*/
const S = {
hello: "Hello Ente!",
error_generic: "Oops, something went wrong.",
};
export default S;

View File

@@ -12,6 +12,9 @@ allprojects {
maven {
url "${project(':background_fetch').projectDir}/libs"
}
maven {
url "${project(':ffmpeg_kit_flutter').projectDir}/libs"
}
}
}

View File

@@ -39,7 +39,6 @@ class NetworkClient {
),
);
_dio.httpClientAdapter = NativeAdapter();
_enteDio.httpClientAdapter = NativeAdapter();
_setupInterceptors(endpoint);

View File

@@ -1904,6 +1904,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage(
"Ein Fehler ist aufgetreten, bitte versuche es erneut"),
"sorry": MessageLookupByLibrary.simpleMessage("Entschuldigung"),
"sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage(
"Leider konnten wir diese Datei momentan nicht sichern, wir werden es später erneut versuchen."),
"sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage(
"Konnte leider nicht zu den Favoriten hinzugefügt werden!"),
"sorryCouldNotRemoveFromFavorites":

View File

@@ -1179,7 +1179,7 @@ class MessageLookup extends MessageLookupByLibrary {
"loadMessage1": MessageLookupByLibrary.simpleMessage(
"You can share your subscription with your family"),
"loadMessage2": MessageLookupByLibrary.simpleMessage(
"We have preserved over 30 million memories so far"),
"We have preserved over 200 million memories so far"),
"loadMessage3": MessageLookupByLibrary.simpleMessage(
"We keep 3 copies of your data, one in an underground fallout shelter"),
"loadMessage4": MessageLookupByLibrary.simpleMessage(

View File

@@ -22,9 +22,18 @@ class MessageLookup extends MessageLookupByLibrary {
static String m0(title) => "${title} (Yo)";
static String m1(count) =>
"${Intl.plural(count, zero: 'Añadir colaborador', one: 'Añadir colaborador', other: 'Añadir colaboradores')}";
static String m2(count) =>
"${Intl.plural(count, one: 'Añadir objeto', other: 'Añadir objetos')}";
static String m3(storageAmount, endDate) =>
"Tu ${storageAmount} adicional es válido hasta ${endDate}";
static String m4(count) =>
"${Intl.plural(count, zero: 'Añadir espectador', one: 'Añadir espectador', other: 'Añadir espectadores')}";
static String m5(emailOrName) => "Añadido por ${emailOrName}";
static String m6(albumName) => "Añadido exitosamente a ${albumName}";
@@ -115,8 +124,14 @@ class MessageLookup extends MessageLookupByLibrary {
static String m36(endDate) => "Prueba gratuita válida hasta ${endDate}";
static String m37(count) =>
"Aún puedes acceder ${Intl.plural(count, one: 'a él', other: 'a ellos')} en Ente mientras tengas una suscripción activa";
static String m38(sizeInMBorGB) => "Liberar ${sizeInMBorGB}";
static String m39(count, formattedSize) =>
"${Intl.plural(count, one: 'Se puede eliminar del dispositivo para liberar ${formattedSize}', other: 'Se pueden eliminar del dispositivo para liberar ${formattedSize}')}";
static String m40(currentlyProcessing, totalCount) =>
"Procesando ${currentlyProcessing} / ${totalCount}";
@@ -137,6 +152,12 @@ class MessageLookup extends MessageLookupByLibrary {
static String m47(personName, email) =>
"Esto enlazará a ${personName} a ${email}";
static String m48(count, formattedCount) =>
"${Intl.plural(count, zero: 'no hay recuerdos', one: '${formattedCount} recuerdo', other: '${formattedCount} recuerdos')}";
static String m49(count) =>
"${Intl.plural(count, one: 'Mover objeto', other: 'Mover objetos')}";
static String m50(albumName) => "Movido exitosamente a ${albumName}";
static String m51(personName) => "No hay sugerencias para ${personName}";
@@ -154,8 +175,16 @@ class MessageLookup extends MessageLookupByLibrary {
static String m56(providerName) =>
"Por favor, habla con el soporte de ${providerName} si se te cobró";
static String m57(name, age) => "¡${name} tiene ${age} años!";
static String m58(name, age) => "${name} cumpliendo ${age} pronto";
static String m59(count) =>
"${Intl.plural(count, zero: 'No hay fotos', one: '1 foto', other: '${count} fotos')}";
static String m60(count) =>
"${Intl.plural(count, zero: '0 fotos', one: '1 foto', other: '${count} fotos')}";
static String m61(endDate) =>
"Prueba gratuita válida hasta ${endDate}.\nPuedes elegir un plan de pago después.";
@@ -227,6 +256,8 @@ class MessageLookup extends MessageLookupByLibrary {
static String m88(name) => "Deportes con ${name}";
static String m89(name) => "Enfocar a ${name}";
static String m90(storageAmountInGB) => "${storageAmountInGB} GB";
static String m91(
@@ -249,6 +280,9 @@ class MessageLookup extends MessageLookupByLibrary {
static String m97(email) => "Este es el ID de verificación de ${email}";
static String m98(count) =>
"${Intl.plural(count, one: 'Esta semana, hace ${count} año', other: 'Esta semana, hace ${count} años')}";
static String m99(dateFormat) => "${dateFormat} a través de los años";
static String m100(count) =>
@@ -273,6 +307,9 @@ class MessageLookup extends MessageLookupByLibrary {
static String m108(email) => "Verificar ${email}";
static String m109(count) =>
"${Intl.plural(count, zero: '0 espectadores añadidos', one: '1 espectador añadido', other: '${count} espectadores añadidos')}";
static String m110(email) =>
"Hemos enviado un correo a <green>${email}</green>";
@@ -307,9 +344,11 @@ class MessageLookup extends MessageLookupByLibrary {
"Agregar nuevo correo electrónico"),
"addCollaborator":
MessageLookupByLibrary.simpleMessage("Agregar colaborador"),
"addCollaborators": m1,
"addFiles": MessageLookupByLibrary.simpleMessage("Añadir archivos"),
"addFromDevice": MessageLookupByLibrary.simpleMessage(
"Agregar desde el dispositivo"),
"addItem": m2,
"addLocation":
MessageLookupByLibrary.simpleMessage("Agregar ubicación"),
"addLocationButton": MessageLookupByLibrary.simpleMessage("Añadir"),
@@ -334,6 +373,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addTrustedContact": MessageLookupByLibrary.simpleMessage(
"Añadir contacto de confianza"),
"addViewer": MessageLookupByLibrary.simpleMessage("Añadir espectador"),
"addViewers": m4,
"addYourPhotosNow":
MessageLookupByLibrary.simpleMessage("Añade tus fotos ahora"),
"addedAs": MessageLookupByLibrary.simpleMessage("Agregado como"),
@@ -362,6 +402,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Todos los recuerdos preservados"),
"allPersonGroupingWillReset": MessageLookupByLibrary.simpleMessage(
"Se eliminarán todas las agrupaciones para esta persona, y se eliminarán todas sus sugerencias"),
"allWillShiftRangeBasedOnFirst": MessageLookupByLibrary.simpleMessage(
"Este es el primero en el grupo. Otras fotos seleccionadas cambiarán automáticamente basándose en esta nueva fecha"),
"allow": MessageLookupByLibrary.simpleMessage("Permitir"),
"allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage(
"Permitir a las personas con el enlace añadir fotos al álbum compartido."),
@@ -510,6 +552,8 @@ class MessageLookup extends MessageLookupByLibrary {
"blackFridaySale":
MessageLookupByLibrary.simpleMessage("Oferta del Black Friday"),
"blog": MessageLookupByLibrary.simpleMessage("Blog"),
"cLBulkEdit":
MessageLookupByLibrary.simpleMessage("Edición masiva de fechas"),
"cLBulkEditDesc": MessageLookupByLibrary.simpleMessage(
"Ahora puedes seleccionar múltiples fotos y editar la fecha/hora para todas ellas con una acción rápida. También es posible cambiar las fechas."),
"cLFamilyPlan":
@@ -520,6 +564,8 @@ class MessageLookup extends MessageLookupByLibrary {
"cLIconDesc": MessageLookupByLibrary.simpleMessage(
"Por fin, un nuevo icono de la aplicación, que creemos que representa mejor nuestro trabajo. También hemos añadido una opción para que puedas seguir utilizando el icono anterior."),
"cLMemories": MessageLookupByLibrary.simpleMessage("Recuerdos"),
"cLMemoriesDesc": MessageLookupByLibrary.simpleMessage(
"Redescubre tus momentos especiales: enfócate en tu gente favorita, tus viajes y vacaciones, tus mejores clics, y mucho más. Activa el aprendizaje de automático, etiquétate a ti mismo y etiqueta a tus amigos para la mejor experiencia."),
"cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"),
"cLWidgetsDesc": MessageLookupByLibrary.simpleMessage(
"Ya están disponibles los widgets de pantalla de inicio con tus recuerdos. Podrás ver tus momentos especiales sin abrir la aplicación."),
@@ -693,6 +739,8 @@ class MessageLookup extends MessageLookupByLibrary {
"criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage(
"Actualización crítica disponible"),
"crop": MessageLookupByLibrary.simpleMessage("Ajustar encuadre"),
"curatedMemories":
MessageLookupByLibrary.simpleMessage("Memorias revisadas"),
"currentUsageIs":
MessageLookupByLibrary.simpleMessage("El uso actual es de "),
"currentlyRunning": MessageLookupByLibrary.simpleMessage("ejecutando"),
@@ -1012,12 +1060,14 @@ class MessageLookup extends MessageLookupByLibrary {
"Almacenamiento libre disponible"),
"freeTrial": MessageLookupByLibrary.simpleMessage("Prueba gratuita"),
"freeTrialValidTill": m36,
"freeUpAccessPostDelete": m37,
"freeUpAmount": m38,
"freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage(
"Liberar espacio del dispositivo"),
"freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage(
"Ahorra espacio en tu dispositivo limpiando archivos que tienen copia de seguridad."),
"freeUpSpace": MessageLookupByLibrary.simpleMessage("Liberar espacio"),
"freeUpSpaceSaving": m39,
"gallery": MessageLookupByLibrary.simpleMessage("Galería"),
"galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage(
"Hasta 1000 memorias mostradas en la galería"),
@@ -1268,6 +1318,7 @@ class MessageLookup extends MessageLookupByLibrary {
"mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"),
"matrix": MessageLookupByLibrary.simpleMessage("Matrix"),
"me": MessageLookupByLibrary.simpleMessage("Yo"),
"memoryCount": m48,
"merchandise": MessageLookupByLibrary.simpleMessage("Mercancías"),
"mergeWithExisting":
MessageLookupByLibrary.simpleMessage("Combinar con existente"),
@@ -1299,6 +1350,7 @@ class MessageLookup extends MessageLookupByLibrary {
"mostRecent": MessageLookupByLibrary.simpleMessage("Más reciente"),
"mostRelevant": MessageLookupByLibrary.simpleMessage("Más relevante"),
"mountains": MessageLookupByLibrary.simpleMessage("Sobre las colinas"),
"moveItem": m49,
"moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage(
"Mover las fotos seleccionadas a una fecha"),
"moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover al álbum"),
@@ -1441,6 +1493,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Borrar permanentemente"),
"permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage(
"¿Eliminar permanentemente del dispositivo?"),
"personIsAge": m57,
"personName":
MessageLookupByLibrary.simpleMessage("Nombre de la persona"),
"personTurningAge": m58,
@@ -1450,10 +1503,15 @@ class MessageLookup extends MessageLookupByLibrary {
"photoGridSize": MessageLookupByLibrary.simpleMessage(
"Tamaño de la cuadrícula de fotos"),
"photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"),
"photocountPhotos": m59,
"photos": MessageLookupByLibrary.simpleMessage("Fotos"),
"photosAddedByYouWillBeRemovedFromTheAlbum":
MessageLookupByLibrary.simpleMessage(
"Las fotos añadidas por ti serán removidas del álbum"),
"photosCount": m60,
"photosKeepRelativeTimeDifference":
MessageLookupByLibrary.simpleMessage(
"Las fotos mantienen una diferencia de tiempo relativa"),
"pickCenterPoint":
MessageLookupByLibrary.simpleMessage("Elegir punto central"),
"pinAlbum": MessageLookupByLibrary.simpleMessage("Fijar álbum"),
@@ -1742,6 +1800,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Seleccionar más fotos"),
"selectOneDateAndTime":
MessageLookupByLibrary.simpleMessage("Seleccionar fecha y hora"),
"selectOneDateAndTimeForAll": MessageLookupByLibrary.simpleMessage(
"Seleccione una fecha y hora para todas"),
"selectPersonToLink": MessageLookupByLibrary.simpleMessage(
"Selecciona persona a vincular"),
"selectReason":
@@ -1881,6 +1941,9 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Más antiguos primero"),
"sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Éxito"),
"sportsWithThem": m88,
"spotlightOnThem": m89,
"spotlightOnYourself":
MessageLookupByLibrary.simpleMessage("Enfócate a ti mismo"),
"startAccountRecoveryTitle":
MessageLookupByLibrary.simpleMessage("Iniciar la recuperación"),
"startBackup":
@@ -1977,6 +2040,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Esta es tu ID de verificación"),
"thisWeekThroughTheYears": MessageLookupByLibrary.simpleMessage(
"Esta semana a través de los años"),
"thisWeekXYearsAgo": m98,
"thisWillLogYouOutOfTheFollowingDevice":
MessageLookupByLibrary.simpleMessage(
"Esto cerrará la sesión del siguiente dispositivo:"),
@@ -2112,6 +2176,7 @@ class MessageLookup extends MessageLookupByLibrary {
"viewRecoveryKey":
MessageLookupByLibrary.simpleMessage("Ver código de recuperación"),
"viewer": MessageLookupByLibrary.simpleMessage("Espectador"),
"viewersSuccessfullyAdded": m109,
"visitWebToManage": MessageLookupByLibrary.simpleMessage(
"Por favor, visita web.ente.io para administrar tu suscripción"),
"waitingForVerification":

View File

@@ -26,6 +26,16 @@ class MessageLookup extends MessageLookupByLibrary {
static String m13(user) =>
"${user}-(e)k ezin izango du argazki gehiago gehitu album honetan \n\nBaina haiek gehitutako argazkiak kendu ahal izango dituzte";
static String m14(isFamilyMember, storageAmountInGb) =>
"${Intl.select(isFamilyMember, {
'true': 'Zure familiak ${storageAmountInGb} GB eskatu du dagoeneko',
'false': 'Zuk ${storageAmountInGb} GB eskatu duzu dagoeneko',
'other': 'Zuk ${storageAmountInGb} GB eskatu duzu dagoeneko!',
})}";
static String m23(albumName) =>
"Honen bidez ${albumName} eskuratzeko esteka publikoa ezabatuko da.";
static String m24(supportEmail) =>
"Mesedez, bidali e-maila ${supportEmail}-era zure erregistratutako e-mail helbidetik";
@@ -38,21 +48,45 @@ class MessageLookup extends MessageLookupByLibrary {
static String m45(expiryTime) =>
"Esteka epe honetan iraungiko da: ${expiryTime}";
static String m48(count, formattedCount) =>
"${Intl.plural(count, zero: 'oroitzapenik ez', one: 'oroitzapen ${formattedCount}', other: '${formattedCount} oroitzapen')}";
static String m53(familyAdminEmail) =>
"Mesedez, jarri harremanetan ${familyAdminEmail}-(r)ekin zure kodea aldatzeko.";
static String m55(passwordStrengthValue) =>
"Pasahitzaren indarra: ${passwordStrengthValue}";
static String m71(storageInGB) =>
"3. Bai zuk bai haiek ${storageInGB} GB* dohainik izango duzue";
static String m72(userEmail) =>
"${userEmail} partekatutako album honetatik ezabatuko da \n\nHaiek gehitutako argazki guztiak ere ezabatuak izango dira albumetik";
static String m77(count) => "${count} hautatuta";
static String m78(count, yourCount) =>
"${count} hautatuta (${yourCount} zureak)";
static String m80(verificationID) =>
"Hau da nire Egiaztatze IDa: ${verificationID} ente.io-rako.";
static String m81(verificationID) =>
"Ei, baieztatu ahal duzu hau dela zure ente.io Egiaztatze IDa?: ${verificationID}";
static String m82(referralCode, referralStorageInGB) =>
"Sartu erreferentzia kodea: ${referralCode}\n\nAplikatu hemen: Ezarpenak → Orokorra→ Erreferentziak, ${referralStorageInGB} GB dohainik izateko ordainpeko plan batean \n\nhttps://ente.io";
static String m83(numberOfPeople) =>
"${Intl.plural(numberOfPeople, zero: 'Partekatu pertsona zehatz batzuekin', one: 'Partekatu pertsona batekin', other: 'Partekatu ${numberOfPeople} pertsonarekin')}";
static String m85(fileType) => "${fileType} hau zure gailutik ezabatuko da.";
static String m86(fileType) =>
"${fileType} hau Ente-n eta zure gailuan dago.";
static String m87(fileType) => "${fileType} hau Ente-tik ezabatuko da.";
static String m90(storageAmountInGB) => "${storageAmountInGB} GB";
static String m96(storageAmountInGB) =>
@@ -79,6 +113,9 @@ class MessageLookup extends MessageLookupByLibrary {
"addMore": MessageLookupByLibrary.simpleMessage("Gehitu gehiago"),
"addViewer": MessageLookupByLibrary.simpleMessage("Gehitu ikuslea"),
"addedAs": MessageLookupByLibrary.simpleMessage("Honela gehituta:"),
"addingToFavorites":
MessageLookupByLibrary.simpleMessage("Gogokoetan gehitzen..."),
"advancedSettings": MessageLookupByLibrary.simpleMessage("Aurreratuak"),
"after1Day": MessageLookupByLibrary.simpleMessage("Egun bat barru"),
"after1Hour": MessageLookupByLibrary.simpleMessage("Ordubete barru"),
"after1Month":
@@ -89,6 +126,7 @@ class MessageLookup extends MessageLookupByLibrary {
"albumParticipantsCount": m8,
"albumUpdated":
MessageLookupByLibrary.simpleMessage("Albuma eguneratuta"),
"albums": MessageLookupByLibrary.simpleMessage("Albumak"),
"allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage(
"Utzi esteka duen jendeari ere album partekatuan argazkiak gehitzen."),
"allowAddingPhotos":
@@ -98,12 +136,37 @@ class MessageLookup extends MessageLookupByLibrary {
"apply": MessageLookupByLibrary.simpleMessage("Aplikatu"),
"applyCodeTitle":
MessageLookupByLibrary.simpleMessage("Aplikatu kodea"),
"archive": MessageLookupByLibrary.simpleMessage("Artxiboa"),
"askDeleteReason": MessageLookupByLibrary.simpleMessage(
"Zein da zure kontua ezabatzeko arrazoi nagusia?"),
"authToChangeEmailVerificationSetting":
MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu emailaren egiaztatzea aldatzeko"),
"authToChangeLockscreenSetting": MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu pantaila blokeatzeko ezarpenak aldatzeko"),
"authToChangeYourEmail": MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu zure emaila aldatzeko"),
"authToChangeYourPassword": MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu zure pasahitza aldatzeko"),
"authToConfigureTwofactorAuthentication":
MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu faktore biko autentifikazioa konfiguratzeko"),
"authToInitiateAccountDeletion": MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu kontu ezabaketa hasteko"),
"authToViewTrashedFiles": MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu paperontzira botatako zure fitxategiak ikusteko"),
"authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu indarrean dauden zure saioak ikusteko"),
"authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu zure ezkutatutako fitxategiak ikusteko"),
"authToViewYourRecoveryKey": MessageLookupByLibrary.simpleMessage(
"Mesedez, autentifikatu zure berreskuratze giltza ikusteko"),
"canNotOpenBody": MessageLookupByLibrary.simpleMessage(
"Sentitzen dugu, album hau ezin da aplikazioan ireki."),
"canNotOpenTitle":
MessageLookupByLibrary.simpleMessage("Ezin dut album hau ireki"),
"canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage(
"Zure fitxategiak baino ezin duzu ezabatu"),
"cancel": MessageLookupByLibrary.simpleMessage("Utzi"),
"cannotAddMorePhotosAfterBecomingViewer": m13,
"change": MessageLookupByLibrary.simpleMessage("Aldatu"),
@@ -116,14 +179,19 @@ class MessageLookup extends MessageLookupByLibrary {
"Aldatu zure erreferentzia kodea"),
"checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage(
"Mesedez, aztertu zure inbox (eta spam) karpetak egiaztatzea osotzeko"),
"claimFreeStorage": MessageLookupByLibrary.simpleMessage(
"Eskatu debaldeko biltegiratzea"),
"claimMore": MessageLookupByLibrary.simpleMessage("Eskatu gehiago!"),
"claimed": MessageLookupByLibrary.simpleMessage("Eskatuta"),
"claimedStorageSoFar": m14,
"codeAppliedPageTitle":
MessageLookupByLibrary.simpleMessage("Kodea aplikatuta"),
"codeChangeLimitReached": MessageLookupByLibrary.simpleMessage(
"Sentitzen dugu, zure kode aldaketa muga gainditu duzu."),
"codeCopiedToClipboard":
MessageLookupByLibrary.simpleMessage("Kodea arbelean kopiatuta"),
"codeUsedByYou":
MessageLookupByLibrary.simpleMessage("Zuk erabilitako kodea"),
"collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage(
"Sortu esteka bat beste pertsona batzuei zure album partekatuan arriskuak gehitu eta ikusten uzteko, naiz eta Ente aplikazio edo kontua ez izan. Oso egokia gertakizun bateko argazkiak biltzeko."),
"collaborativeLink":
@@ -135,6 +203,8 @@ class MessageLookup extends MessageLookupByLibrary {
"collectPhotos":
MessageLookupByLibrary.simpleMessage("Bildu argazkiak"),
"confirm": MessageLookupByLibrary.simpleMessage("Baieztatu"),
"confirm2FADisable": MessageLookupByLibrary.simpleMessage(
"Seguru zaude faktore biko autentifikazioa deuseztatu nahi duzula?"),
"confirmAccountDeletion":
MessageLookupByLibrary.simpleMessage("Baieztatu Kontu Ezabaketa"),
"confirmDeletePrompt": MessageLookupByLibrary.simpleMessage(
@@ -153,10 +223,14 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage(
"Kopiatu eta itsatsi kode hau zure autentifikazio aplikaziora"),
"createAccount": MessageLookupByLibrary.simpleMessage("Sortu kontua"),
"createAlbumActionHint": MessageLookupByLibrary.simpleMessage(
"Luze klikatu argazkiak hautatzeko eta klikatu + albuma sortzeko"),
"createNewAccount":
MessageLookupByLibrary.simpleMessage("Sortu kontu berria"),
"createPublicLink":
MessageLookupByLibrary.simpleMessage("Sortu esteka publikoa"),
"creatingLink":
MessageLookupByLibrary.simpleMessage("Esteka sortzen..."),
"custom": MessageLookupByLibrary.simpleMessage("Aukeran"),
"decrypting": MessageLookupByLibrary.simpleMessage("Deszifratzen..."),
"deleteAccount":
@@ -165,8 +239,19 @@ class MessageLookup extends MessageLookupByLibrary {
"Sentitzen dugu zu joateaz. Mesedez, utziguzu zure feedbacka hobetzen laguntzeko."),
"deleteAccountPermanentlyButton":
MessageLookupByLibrary.simpleMessage("Ezabatu Kontua Betiko"),
"deleteAlbum": MessageLookupByLibrary.simpleMessage("Ezabatu albuma"),
"deleteAlbumDialog": MessageLookupByLibrary.simpleMessage(
"Ezabatu nahi dituzu album honetan dauden argazkiak (eta bideoak) parte diren beste album <bold>guztietatik</bold> ere?"),
"deleteEmailRequest": MessageLookupByLibrary.simpleMessage(
"Mesedez, bidali e-mail bat <warning>account-deletion@ente.io</warning> helbidea zure erregistatutako helbidetik."),
"deleteFromBoth":
MessageLookupByLibrary.simpleMessage("Ezabatu bietatik"),
"deleteFromDevice":
MessageLookupByLibrary.simpleMessage("Ezabatu gailutik"),
"deleteFromEnte":
MessageLookupByLibrary.simpleMessage("Ezabatu Ente-tik"),
"deletePhotos":
MessageLookupByLibrary.simpleMessage("Ezabatu argazkiak"),
"deleteReason1": MessageLookupByLibrary.simpleMessage(
"Behar dudan ezaugarre nagusiren bat falta zaio"),
"deleteReason2": MessageLookupByLibrary.simpleMessage(
@@ -177,14 +262,42 @@ class MessageLookup extends MessageLookupByLibrary {
"Nire arrazoia ez dago zerrendan"),
"deleteRequestSLAText": MessageLookupByLibrary.simpleMessage(
"Zure eskaera 72 ordutan prozesatua izango da."),
"deleteSharedAlbum": MessageLookupByLibrary.simpleMessage(
"Partekatutako albuma ezabatu?"),
"deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage(
"Albuma guztiontzat ezabatuko da \n\nAlbum honetan dauden beste pertsonek partekatutako argazkiak ezin izango dituzu eskuratu"),
"details": MessageLookupByLibrary.simpleMessage("Detaileak"),
"disableDownloadWarningBody": MessageLookupByLibrary.simpleMessage(
"Ikusleek pantaila-irudiak atera ahal dituzte, edo kanpoko tresnen bidez zure argazkien kopiak gorde"),
"disableDownloadWarningTitle":
MessageLookupByLibrary.simpleMessage("Mesedez, ohartu"),
"disableLinkMessage": m23,
"discover": MessageLookupByLibrary.simpleMessage("Aurkitu"),
"discover_babies": MessageLookupByLibrary.simpleMessage("Umeak"),
"discover_celebrations":
MessageLookupByLibrary.simpleMessage("Ospakizunak"),
"discover_food": MessageLookupByLibrary.simpleMessage("Janaria"),
"discover_greenery": MessageLookupByLibrary.simpleMessage("Hostoa"),
"discover_hills": MessageLookupByLibrary.simpleMessage("Muinoak"),
"discover_identity": MessageLookupByLibrary.simpleMessage("Nortasuna"),
"discover_memes": MessageLookupByLibrary.simpleMessage("Memeak"),
"discover_notes": MessageLookupByLibrary.simpleMessage("Oharrak"),
"discover_pets": MessageLookupByLibrary.simpleMessage("Etxe-animaliak"),
"discover_receipts":
MessageLookupByLibrary.simpleMessage("Ordainagiriak"),
"discover_screenshots":
MessageLookupByLibrary.simpleMessage("Pantaila argazkiak"),
"discover_selfies": MessageLookupByLibrary.simpleMessage("Selfiak"),
"discover_sunset":
MessageLookupByLibrary.simpleMessage("Eguzki-sartzea"),
"discover_visiting_cards":
MessageLookupByLibrary.simpleMessage("Bisita txartelak"),
"discover_wallpapers":
MessageLookupByLibrary.simpleMessage("Horma-paperak"),
"doThisLater": MessageLookupByLibrary.simpleMessage("Egin hau geroago"),
"done": MessageLookupByLibrary.simpleMessage("Eginda"),
"dropSupportEmail": m24,
"eligible": MessageLookupByLibrary.simpleMessage("aukerakoak"),
"email": MessageLookupByLibrary.simpleMessage("E-maila"),
"emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage(
"Helbide hau badago erregistratuta lehendik."),
@@ -223,16 +336,30 @@ class MessageLookup extends MessageLookupByLibrary {
"Esteka hau iraungi da. Mesedez, aukeratu beste epemuga bat edo deuseztatu estekaren epemuga."),
"failedToApplyCode":
MessageLookupByLibrary.simpleMessage("Akatsa kodea aplikatzean"),
"failedToFetchReferralDetails": MessageLookupByLibrary.simpleMessage(
"Ezin dugu zure erreferentziaren detailerik lortu. Mesedez, saiatu berriro geroago."),
"failedToLoadAlbums":
MessageLookupByLibrary.simpleMessage("Errorea albumak kargatzen"),
"faq": MessageLookupByLibrary.simpleMessage("FAQ"),
"feedback": MessageLookupByLibrary.simpleMessage("Feedbacka"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Ahaztu pasahitza"),
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
"Debaldeko biltegiratzea eskatuta"),
"freeStorageOnReferralSuccess": m35,
"freeStorageUsable": MessageLookupByLibrary.simpleMessage(
"Debaldeko biltegiratzea erabilgarri"),
"generatingEncryptionKeys":
MessageLookupByLibrary.simpleMessage("Zifratze giltzak sortzen..."),
"help": MessageLookupByLibrary.simpleMessage("Laguntza"),
"hidden": MessageLookupByLibrary.simpleMessage("Ezkutatuta"),
"howItWorks":
MessageLookupByLibrary.simpleMessage("Nola funtzionatzen duen"),
"howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage(
"Mesedez, eska iezaiozu ezarpenen landutako bere e-mail helbidean luze klikatzeko, eta egiaztatu gailu bietako IDak bat direla."),
"iOSLockOut": MessageLookupByLibrary.simpleMessage(
"Autentifikazio biometrikoa deuseztatuta dago. Mesedez, blokeatu eta desblokeatu zure pantaila indarrean jartzeko."),
"importing": MessageLookupByLibrary.simpleMessage("Inportatzen...."),
"incorrectPasswordTitle":
MessageLookupByLibrary.simpleMessage("Pasahitz okerra"),
"incorrectRecoveryKeyBody": MessageLookupByLibrary.simpleMessage(
@@ -246,6 +373,13 @@ class MessageLookup extends MessageLookupByLibrary {
"invalidKey": MessageLookupByLibrary.simpleMessage("Kode okerra"),
"invalidRecoveryKey": MessageLookupByLibrary.simpleMessage(
"Sartu duzun berreskuratze kodea ez da zuzena. Mesedez, ziurtatu 24 hitz duela, eta egiaztatu hitz bakoitzaren idazkera. \n\nBerreskuratze kode zaharren bat sartu baduzu, ziurtatu 64 karaktere duela, eta egiaztatu horietako bakoitza."),
"inviteToEnte":
MessageLookupByLibrary.simpleMessage("Gonbidatu Ente-ra"),
"inviteYourFriends":
MessageLookupByLibrary.simpleMessage("Gonbidatu zure lagunak"),
"itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage(
"Hautatutako elementuak album honetatik kenduko dira"),
"keepPhotos": MessageLookupByLibrary.simpleMessage("Gorde Argazkiak"),
"kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage(
"Mesedez, lagun gaitzazu informazio honekin"),
"linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Gailu muga"),
@@ -262,11 +396,31 @@ class MessageLookup extends MessageLookupByLibrary {
"Sartzeko klikatuz, <u-terms>zerbitzu baldintzak</u-terms> eta <u-policy>pribatutasun politikak</u-policy> onartzen ditut"),
"lostDevice":
MessageLookupByLibrary.simpleMessage("Gailua galdu duzu?"),
"machineLearning":
MessageLookupByLibrary.simpleMessage("Ikasketa automatikoa"),
"magicSearch": MessageLookupByLibrary.simpleMessage("Bilaketa magikoa"),
"manage": MessageLookupByLibrary.simpleMessage("Kudeatu"),
"manageDeviceStorage":
MessageLookupByLibrary.simpleMessage("Kudeatu gailuaren katxea"),
"manageDeviceStorageDesc": MessageLookupByLibrary.simpleMessage(
"Berrikusi eta garbitu katxe lokalaren biltegiratzea."),
"manageLink": MessageLookupByLibrary.simpleMessage("Kudeatu esteka"),
"manageParticipants": MessageLookupByLibrary.simpleMessage("Kudeatu"),
"memoryCount": m48,
"mlConsent": MessageLookupByLibrary.simpleMessage(
"Aktibatu ikasketa automatikoa"),
"mlConsentConfirmation": MessageLookupByLibrary.simpleMessage(
"Ulertzen dut, eta ikasketa automatikoa aktibatu nahi dut"),
"mlConsentDescription": MessageLookupByLibrary.simpleMessage(
"Ikasketa automatikoa aktibatuz gero, Ente-k fitxategietatik informazioa aterako du (ad. argazkien geometria), zurekin partekatutako argazkietatik ere.\n\nHau zure gailuan gertatuko da, eta sortutako informazio biometrikoa puntutik puntura zifratuta egongo da."),
"mlConsentPrivacy": MessageLookupByLibrary.simpleMessage(
"Mesedez, klikatu hemen gure pribatutasun politikan ezaugarri honi buruz detaile gehiago izateko"),
"mlConsentTitle": MessageLookupByLibrary.simpleMessage(
"Ikasketa automatikoa aktibatuko?"),
"moderateStrength": MessageLookupByLibrary.simpleMessage("Ertaina"),
"movedToTrash": MessageLookupByLibrary.simpleMessage("Zarama mugituta"),
"never": MessageLookupByLibrary.simpleMessage("Inoiz ez"),
"newAlbum": MessageLookupByLibrary.simpleMessage("Album berria"),
"noDeviceLimit": MessageLookupByLibrary.simpleMessage("Bat ere ez"),
"noRecoveryKey":
MessageLookupByLibrary.simpleMessage("Berreskuratze giltzarik ez?"),
@@ -275,6 +429,8 @@ class MessageLookup extends MessageLookupByLibrary {
"ok": MessageLookupByLibrary.simpleMessage("Ondo"),
"onlyFamilyAdminCanChangeCode": m53,
"oops": MessageLookupByLibrary.simpleMessage("Ai!"),
"oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage(
"Oops, zerbait txarto joan da"),
"orPickAnExistingOne":
MessageLookupByLibrary.simpleMessage("Edo aukeratu lehengo bat"),
"password": MessageLookupByLibrary.simpleMessage("Pasahitza"),
@@ -285,6 +441,11 @@ class MessageLookup extends MessageLookupByLibrary {
"passwordStrength": m55,
"passwordWarning": MessageLookupByLibrary.simpleMessage(
"Ezin dugu zure pasahitza gorde, beraz, ahazten baduzu, <underline>ezin dugu zure data deszifratu</underline>"),
"peopleUsingYourCode": MessageLookupByLibrary.simpleMessage(
"Jendea zure kodea erabiltzen"),
"photoGridSize":
MessageLookupByLibrary.simpleMessage("Argazki sarearen tamaina"),
"photoSmallCase": MessageLookupByLibrary.simpleMessage("argazkia"),
"pleaseTryAgain":
MessageLookupByLibrary.simpleMessage("Saiatu berriro, mesedez"),
"pleaseWait":
@@ -317,10 +478,30 @@ class MessageLookup extends MessageLookupByLibrary {
"Gailu hau ez da zure pasahitza egiaztatzeko bezain indartsua, baina gailu guztietan funtzionatzen duen modu batean birsortu ahal dugu. \n\nMesedez sartu zure berreskuratze giltza erabiliz eta birsortu zure pasahitza (aurreko berbera erabili ahal duzu nahi izanez gero)."),
"recreatePasswordTitle":
MessageLookupByLibrary.simpleMessage("Berrezarri pasahitza"),
"referralStep1": MessageLookupByLibrary.simpleMessage(
"1. Eman kode hau zure lagunei"),
"referralStep2": MessageLookupByLibrary.simpleMessage(
"2. Haiek ordainpeko plan batean sinatu behar dute"),
"referralStep3": m71,
"referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage(
"Erreferentziak momentuz geldituta daude"),
"remove": MessageLookupByLibrary.simpleMessage("Kendu"),
"removeFromAlbum":
MessageLookupByLibrary.simpleMessage("Kendu albumetik"),
"removeFromAlbumTitle":
MessageLookupByLibrary.simpleMessage("Albumetik kendu?"),
"removeLink": MessageLookupByLibrary.simpleMessage("Ezabatu esteka"),
"removeParticipant":
MessageLookupByLibrary.simpleMessage("Kendu parte hartzailea"),
"removeParticipantBody": m72,
"removePublicLink":
MessageLookupByLibrary.simpleMessage("Ezabatu esteka publikoa"),
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(
"Kentzen ari zaren elementu batzuk beste pertsona batzuek gehitu zituzten, beraz ezin izango dituzu eskuratu"),
"removeWithQuestionMark":
MessageLookupByLibrary.simpleMessage("Ezabatuko?"),
"removingFromFavorites":
MessageLookupByLibrary.simpleMessage("Gogokoetatik kentzen..."),
"resendEmail":
MessageLookupByLibrary.simpleMessage("Birbidali e-maila"),
"resetPasswordTitle":
@@ -335,6 +516,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Eskaneatu barra kode hau zure autentifikazio aplikazioaz"),
"selectReason":
MessageLookupByLibrary.simpleMessage("Aukeratu arrazoia"),
"selectedPhotos": m77,
"selectedPhotosWithYours": m78,
"sendEmail": MessageLookupByLibrary.simpleMessage("Bidali mezua"),
"sendInvite":
MessageLookupByLibrary.simpleMessage("Bidali gonbidapena"),
@@ -350,13 +533,20 @@ class MessageLookup extends MessageLookupByLibrary {
"shareTextConfirmOthersVerificationID": m81,
"shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage(
"Jaitsi Ente argazkiak eta bideoak jatorrizko kalitatean errez partekatu ahal izateko \n\nhttps://ente.io"),
"shareTextReferralCode": m82,
"shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage(
"Partekatu Ente erabiltzen ez dutenekin"),
"shareWithPeopleSectionTitle": m83,
"sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage(
"Sortu partekatutako eta parte hartzeko albumak beste Ente erabiltzaileekin, debaldeko planak dituztenak barne."),
"sharing": MessageLookupByLibrary.simpleMessage("Partekatzen..."),
"signUpTerms": MessageLookupByLibrary.simpleMessage(
"<u-terms>Zerbitzu baldintzak</u-terms> eta <u-policy>pribatutasun politikak</u-policy> onartzen ditut"),
"singleFileDeleteFromDevice": m85,
"singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage(
"Album guztietatik ezabatuko da."),
"singleFileInBothLocalAndRemote": m86,
"singleFileInRemoteOnly": m87,
"someoneSharingAlbumsWithYouShouldSeeTheSameId":
MessageLookupByLibrary.simpleMessage(
"Zurekin albumak partekatzen dituen norbaitek ID berbera ikusi beharko luke bere gailuan."),
@@ -366,11 +556,19 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage(
"Zerbait ez da ondo joan, mesedez, saiatu berriro"),
"sorry": MessageLookupByLibrary.simpleMessage("Barkatu"),
"sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage(
"Sentitzen dut, ezin izan dugu zure gogokoetan gehitu!"),
"sorryCouldNotRemoveFromFavorites":
MessageLookupByLibrary.simpleMessage(
"Sentitzen dugu, ezin izan dugu zure gogokoetatik kendu!"),
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease":
MessageLookupByLibrary.simpleMessage(
"Tamalez, ezin dugu giltza segururik sortu gailu honetan. \n\nMesedez, eman izena beste gailu batetik."),
"storageInGB": m90,
"strongStrength": MessageLookupByLibrary.simpleMessage("Gogorra"),
"subscribe": MessageLookupByLibrary.simpleMessage("Harpidetu"),
"subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage(
"Ordainpeko harpidetza behar duzu partekatzea aktibatzeko."),
"tapToCopy": MessageLookupByLibrary.simpleMessage("jo kopiatzeko"),
"tapToEnterCode":
MessageLookupByLibrary.simpleMessage("Klikatu kodea sartzeko"),
@@ -394,7 +592,12 @@ class MessageLookup extends MessageLookupByLibrary {
"Hau egiteak gailu honetatik aterako zaitu!"),
"toResetVerifyEmail": MessageLookupByLibrary.simpleMessage(
"Zure pasahitza berrezartzeko, mesedez egiaztatu zure e-maila lehenengoz."),
"total": MessageLookupByLibrary.simpleMessage("osotara"),
"trash": MessageLookupByLibrary.simpleMessage("Zarama"),
"tryAgain": MessageLookupByLibrary.simpleMessage("Saiatu berriro"),
"twofactorAuthenticationHasBeenDisabled":
MessageLookupByLibrary.simpleMessage(
"Faktore biko autentifikazioa deuseztatua izan da"),
"twofactorAuthenticationPageTitle":
MessageLookupByLibrary.simpleMessage(
"Faktore biko autentifikatzea"),
@@ -402,6 +605,10 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Faktore biko ezarpena"),
"unavailableReferralCode": MessageLookupByLibrary.simpleMessage(
"Sentitzen dugu, kode hau ezin da erabili."),
"uncategorized":
MessageLookupByLibrary.simpleMessage("Kategori gabekoa"),
"usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage(
"Biltegiratze erabilgarria zure oraingo planaren arabera mugatuta dago. Soberan eskatutako biltegiratzea automatikoki erabili ahal izango duzu zure plan gaurkotzen duzunean."),
"useRecoveryKey": MessageLookupByLibrary.simpleMessage(
"Erabili berreskuratze giltza"),
"verificationId":
@@ -414,6 +621,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Egiaztatu pasahitza"),
"verifyingRecoveryKey": MessageLookupByLibrary.simpleMessage(
"Berreskuratze giltza egiaztatuz..."),
"videoSmallCase": MessageLookupByLibrary.simpleMessage("bideoa"),
"viewRecoveryKey":
MessageLookupByLibrary.simpleMessage("Ikusi berreskuratze kodea"),
"viewer": MessageLookupByLibrary.simpleMessage("Ikuslea"),
@@ -423,7 +631,13 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Ongi etorri berriro!"),
"yesConvertToViewer":
MessageLookupByLibrary.simpleMessage("Bai, egin ikusle"),
"yesDelete": MessageLookupByLibrary.simpleMessage("Bai, ezabatu"),
"yesRemove": MessageLookupByLibrary.simpleMessage("Bai, ezabatu"),
"you": MessageLookupByLibrary.simpleMessage("Zu"),
"youCanAtMaxDoubleYourStorage": MessageLookupByLibrary.simpleMessage(
"* Gehienez zure biltegiratzea bikoiztu ahal duzu"),
"youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage(
"Ezin duzu zeure buruarekin partekatu"),
"yourAccountHasBeenDeleted":
MessageLookupByLibrary.simpleMessage("Zure kontua ezabatua izan da")
};

View File

@@ -716,6 +716,8 @@ class MessageLookup extends MessageLookupByLibrary {
"criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage(
"Mise à jour critique disponible"),
"crop": MessageLookupByLibrary.simpleMessage("Rogner"),
"curatedMemories":
MessageLookupByLibrary.simpleMessage("Souvenirs conservés"),
"currentUsageIs": MessageLookupByLibrary.simpleMessage(
"L\'utilisation actuelle est de "),
"currentlyRunning":
@@ -1880,7 +1882,7 @@ class MessageLookup extends MessageLookupByLibrary {
"shiftDatesAndTime":
MessageLookupByLibrary.simpleMessage("Dates et heure de décalage"),
"showMemories":
MessageLookupByLibrary.simpleMessage("Montrer les souvenirs"),
MessageLookupByLibrary.simpleMessage("Afficher les souvenirs"),
"showPerson":
MessageLookupByLibrary.simpleMessage("Montrer la personne"),
"signOutFromOtherDevices": MessageLookupByLibrary.simpleMessage(

View File

@@ -22,6 +22,9 @@ class MessageLookup extends MessageLookupByLibrary {
static String m0(title) => "${title} (Me)";
static String m2(count) =>
"${Intl.plural(count, one: 'Legg til element', other: 'Legg til elementene')}";
static String m3(storageAmount, endDate) =>
"Tillegget på ${storageAmount} er gyldig til ${endDate}";
@@ -136,6 +139,12 @@ class MessageLookup extends MessageLookupByLibrary {
static String m47(personName, email) =>
"Dette knytter ${personName} til ${email}";
static String m48(count, formattedCount) =>
"${Intl.plural(count, zero: 'ingen minner', one: '${formattedCount} minne', other: '${formattedCount} minner')}";
static String m49(count) =>
"${Intl.plural(count, one: 'Flytt elementet', other: 'Flytt elementene')}";
static String m50(albumName) => "Flyttet til ${albumName}";
static String m51(personName) => "Ingen forslag for ${personName}";
@@ -315,6 +324,7 @@ class MessageLookup extends MessageLookupByLibrary {
"addFiles": MessageLookupByLibrary.simpleMessage("Legg til filer"),
"addFromDevice":
MessageLookupByLibrary.simpleMessage("Legg til fra enhet"),
"addItem": m2,
"addLocation": MessageLookupByLibrary.simpleMessage("Legg til sted"),
"addLocationButton": MessageLookupByLibrary.simpleMessage("Legg til"),
"addMore": MessageLookupByLibrary.simpleMessage("Legg til flere"),
@@ -1248,6 +1258,7 @@ class MessageLookup extends MessageLookupByLibrary {
"mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"),
"matrix": MessageLookupByLibrary.simpleMessage("Matrix"),
"me": MessageLookupByLibrary.simpleMessage("Meg"),
"memoryCount": m48,
"merchandise": MessageLookupByLibrary.simpleMessage("Varer"),
"mergeWithExisting":
MessageLookupByLibrary.simpleMessage("Slå sammen med eksisterende"),
@@ -1279,6 +1290,7 @@ class MessageLookup extends MessageLookupByLibrary {
"mostRecent": MessageLookupByLibrary.simpleMessage("Nyeste"),
"mostRelevant": MessageLookupByLibrary.simpleMessage("Mest relevant"),
"mountains": MessageLookupByLibrary.simpleMessage("Over åsene"),
"moveItem": m49,
"moveSelectedPhotosToOneDate": MessageLookupByLibrary.simpleMessage(
"Flytt valgte bilder til en dato"),
"moveToAlbum": MessageLookupByLibrary.simpleMessage("Flytt til album"),

View File

@@ -172,7 +172,7 @@ class MessageLookup extends MessageLookupByLibrary {
static String m56(providerName) =>
"Fale com o suporte ${providerName} se você foi cobrado";
static String m57(name, age) => "${name} está com ${age}!";
static String m57(name, age) => "${name} tem ${age} anos!";
static String m58(name, age) => "${name} terá ${age} em breve";
@@ -209,7 +209,7 @@ class MessageLookup extends MessageLookupByLibrary {
"3. Ambos os dois ganham ${storageInGB} GB* grátis";
static String m72(userEmail) =>
"${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum";
"${userEmail} será removido do álbum compartilhado\n\nQualquer foto adicionada por ele será removida.";
static String m73(endDate) => "Renovação de assinatura em ${endDate}";
@@ -445,7 +445,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Aplicar código"),
"appstoreSubscription":
MessageLookupByLibrary.simpleMessage("Assinatura da AppStore"),
"archive": MessageLookupByLibrary.simpleMessage("Arquivo"),
"archive": MessageLookupByLibrary.simpleMessage("Arquive"),
"archiveAlbum": MessageLookupByLibrary.simpleMessage("Arquivar álbum"),
"archiving": MessageLookupByLibrary.simpleMessage("Arquivando..."),
"areYouSureThatYouWantToLeaveTheFamily":
@@ -558,7 +558,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Finalmente, um novo ícone para o ente que acreditamos que represente melhor nosso trabalho. Também, adicionamos um alterador de ícone para que você ainda consiga utilizar o ícone antigo."),
"cLMemories": MessageLookupByLibrary.simpleMessage("Memórias"),
"cLMemoriesDesc": MessageLookupByLibrary.simpleMessage(
"Redescubra seus momentos especiais - destaque pessoas importantes, suas viagens e celebrações, melhores visitas e muito mais. Ative a aprendizagem automática, mencione si mesmo e seus amigos para melhor experiência."),
"Relembre momentos especiais - destaque pessoas favoritas, suas viagens e feriados, melhores fotos, e muito mais. Ative o aprendizado automático, marque-se e nomeie seus amigos para melhorar a experiência."),
"cLWidgets": MessageLookupByLibrary.simpleMessage("Widgets"),
"cLWidgetsDesc": MessageLookupByLibrary.simpleMessage(
"Widgets integrados com memórias já estão disponíveis. Eles apareceram com seus melhores momentos sem precisar abrir o ente."),
@@ -616,8 +616,8 @@ class MessageLookup extends MessageLookupByLibrary {
"checkingModels":
MessageLookupByLibrary.simpleMessage("Verificando modelos..."),
"city": MessageLookupByLibrary.simpleMessage("Na cidade"),
"claimFreeStorage": MessageLookupByLibrary.simpleMessage(
"Reivindicar armazenamento grátis"),
"claimFreeStorage":
MessageLookupByLibrary.simpleMessage("Reivindique armaz. grátis"),
"claimMore": MessageLookupByLibrary.simpleMessage("Reivindique mais!"),
"claimed": MessageLookupByLibrary.simpleMessage("Reivindicado"),
"claimedStorageSoFar": m14,
@@ -653,7 +653,7 @@ class MessageLookup extends MessageLookupByLibrary {
"collaborator": MessageLookupByLibrary.simpleMessage("Colaborador"),
"collaboratorsCanAddPhotosAndVideosToTheSharedAlbum":
MessageLookupByLibrary.simpleMessage(
"Os colaboradores podem adicionar fotos e vídeos ao álbum compartilhado."),
"Colaboradores podem adicionar fotos e vídeos ao álbum compartilhado."),
"collaboratorsSuccessfullyAdded": m16,
"collageLayout": MessageLookupByLibrary.simpleMessage("Layout"),
"collageSaved":
@@ -749,7 +749,7 @@ class MessageLookup extends MessageLookupByLibrary {
"delete": MessageLookupByLibrary.simpleMessage("Excluir"),
"deleteAccount": MessageLookupByLibrary.simpleMessage("Excluir conta"),
"deleteAccountFeedbackPrompt": MessageLookupByLibrary.simpleMessage(
"Lamentamos você ir. Compartilhe seu feedback para nos ajudar a melhorar."),
"Lamentamos você ir. Compartilhe seu feedback para ajudar-nos a melhorar."),
"deleteAccountPermanentlyButton": MessageLookupByLibrary.simpleMessage(
"Excluir conta permanentemente"),
"deleteAlbum": MessageLookupByLibrary.simpleMessage("Excluir álbum"),
@@ -896,9 +896,9 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Esvaziar a lixeira?"),
"enable": MessageLookupByLibrary.simpleMessage("Ativar"),
"enableMLIndexingDesc": MessageLookupByLibrary.simpleMessage(
"Ente suporta aprendizagem de máquina para reconhecimento facial, busca mágica e outros recursos de busca avançados"),
"Ente fornece aprendizado automático no dispositivo para reconhecimento facial, busca mágica e outros recursos de busca avançados."),
"enableMachineLearningBanner": MessageLookupByLibrary.simpleMessage(
"Ativar aprendizagem de máquina para busca mágica e reconhecimento facial"),
"Ativar o aprendizado automático para busca mágica e reconhecimento facial"),
"enableMaps": MessageLookupByLibrary.simpleMessage("Ativar mapas"),
"enableMapsDesc": MessageLookupByLibrary.simpleMessage(
"Isso exibirá suas fotos em um mapa mundial.\n\nEste mapa é hospedado por Open Street Map, e as exatas localizações das fotos nunca serão compartilhadas.\n\nVocê pode desativar esta função a qualquer momento em Opções."),
@@ -920,7 +920,7 @@ class MessageLookup extends MessageLookupByLibrary {
"enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage(
"O Ente preserva suas memórias, então eles sempre estão disponíveis para você, mesmo se você perder o dispositivo."),
"enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage(
"Sua família também pode ser adicionada ao seu plano."),
"Sua família também pode ser adicionada ao seu plano."),
"enterAlbumName":
MessageLookupByLibrary.simpleMessage("Inserir nome do álbum"),
"enterCode": MessageLookupByLibrary.simpleMessage("Insira o código"),
@@ -1031,15 +1031,15 @@ class MessageLookup extends MessageLookupByLibrary {
"findThemQuickly":
MessageLookupByLibrary.simpleMessage("Busque-os rapidamente"),
"flip": MessageLookupByLibrary.simpleMessage("Inverter"),
"food": MessageLookupByLibrary.simpleMessage("Prazer em culinária"),
"food": MessageLookupByLibrary.simpleMessage("Amor por culinária"),
"forYourMemories":
MessageLookupByLibrary.simpleMessage("para suas memórias"),
"forgotPassword":
MessageLookupByLibrary.simpleMessage("Esqueci a senha"),
"foundFaces":
MessageLookupByLibrary.simpleMessage("Rostos encontrados"),
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
"Armazenamento grátis reivindicado"),
"freeStorageClaimed":
MessageLookupByLibrary.simpleMessage("Armaz. grátis reivindicado"),
"freeStorageOnReferralSuccess": m35,
"freeStorageUsable":
MessageLookupByLibrary.simpleMessage("Armazenamento disponível"),
@@ -1205,7 +1205,7 @@ class MessageLookup extends MessageLookupByLibrary {
"linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nunca"),
"linkPerson": MessageLookupByLibrary.simpleMessage("Vincular pessoa"),
"linkPersonCaption": MessageLookupByLibrary.simpleMessage(
"para melhor experiência de compartilhamento"),
"para melhorar o compartilhamento"),
"linkPersonToEmail": m46,
"linkPersonToEmailConfirmation": m47,
"livePhotos": MessageLookupByLibrary.simpleMessage("Fotos animadas"),
@@ -1276,7 +1276,7 @@ class MessageLookup extends MessageLookupByLibrary {
"lostDevice":
MessageLookupByLibrary.simpleMessage("Perdeu o dispositivo?"),
"machineLearning":
MessageLookupByLibrary.simpleMessage("Aprendizagem automática"),
MessageLookupByLibrary.simpleMessage("Aprendizado automático"),
"magicSearch": MessageLookupByLibrary.simpleMessage("Busca mágica"),
"magicSearchHint": MessageLookupByLibrary.simpleMessage(
"A busca mágica permite buscar fotos pelo conteúdo, p. e.x. \'flor\', \'carro vermelho\', \'identidade\'"),
@@ -1304,17 +1304,17 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Juntar com o existente"),
"mergedPhotos": MessageLookupByLibrary.simpleMessage("Fotos mescladas"),
"mlConsent": MessageLookupByLibrary.simpleMessage(
"Ativar aprendizagem automática"),
"mlConsentConfirmation": MessageLookupByLibrary.simpleMessage(
"Eu entendo, e desejo ativar a aprendizagem automática"),
"Ativar o aprendizado automático"),
"mlConsentConfirmation":
MessageLookupByLibrary.simpleMessage("Concordo e desejo ativá-lo"),
"mlConsentDescription": MessageLookupByLibrary.simpleMessage(
"Se você ativar a aprendizagem automática, o Ente irá extrair informações como geometria de rosto dos arquivos, incluindo os compartilhados com você.\n\nIsso acontecerá no seu dispositivo, qualquer informação biométrica gerada será criptografada ponta a ponta."),
"Se ativar o aprendizado automático, Ente extrairá informações de geometria facial dos arquivos, incluindo aqueles compartilhados consigo.\n\nIsso acontecerá em seu dispositivo, e qualquer informação biométrica gerada será criptografada de ponta a ponta."),
"mlConsentPrivacy": MessageLookupByLibrary.simpleMessage(
"Clique aqui para mais detalhes sobre este recurso na política de privacidade"),
"mlConsentTitle": MessageLookupByLibrary.simpleMessage(
"Ativar aprendizagem automática?"),
"Ativar aprendizado automático?"),
"mlIndexingDescription": MessageLookupByLibrary.simpleMessage(
"Note que a aprendizagem automática resultará em uso de bateria e largura de banda maior até que todos os itens forem indexados. Considere-se usar o aplicativo para notebook para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente."),
"Saiba que o aprendizado automático afetará a bateria do dispositivo negativamente até todos os itens serem indexados. Utilize a versão para computadores para melhor indexação, todos os resultados se auto-sincronizaram."),
"mobileWebDesktop":
MessageLookupByLibrary.simpleMessage("Celular, Web, Computador"),
"moderateStrength": MessageLookupByLibrary.simpleMessage("Moderado"),
@@ -1405,7 +1405,8 @@ class MessageLookup extends MessageLookupByLibrary {
"onDevice": MessageLookupByLibrary.simpleMessage("No dispositivo"),
"onEnte": MessageLookupByLibrary.simpleMessage(
"No <branding>ente</branding>"),
"onTheRoad": MessageLookupByLibrary.simpleMessage("Na estrada de novo"),
"onTheRoad":
MessageLookupByLibrary.simpleMessage("Na estrada novamente"),
"onlyFamilyAdminCanChangeCode": m53,
"onlyThem": MessageLookupByLibrary.simpleMessage("Apenas eles"),
"oops": MessageLookupByLibrary.simpleMessage("Ops"),
@@ -1474,7 +1475,7 @@ class MessageLookup extends MessageLookupByLibrary {
"personIsAge": m57,
"personName": MessageLookupByLibrary.simpleMessage("Nome da pessoa"),
"personTurningAge": m58,
"pets": MessageLookupByLibrary.simpleMessage("Companhia de pelos"),
"pets": MessageLookupByLibrary.simpleMessage("Companhias peludas"),
"photoDescriptions":
MessageLookupByLibrary.simpleMessage("Descrições das fotos"),
"photoGridSize":
@@ -1546,7 +1547,7 @@ class MessageLookup extends MessageLookupByLibrary {
"privateBackups":
MessageLookupByLibrary.simpleMessage("Cópias privadas"),
"privateSharing":
MessageLookupByLibrary.simpleMessage("Compartilhamento privado"),
MessageLookupByLibrary.simpleMessage("Compartilha privada"),
"proceed": MessageLookupByLibrary.simpleMessage("Continuar"),
"processed": MessageLookupByLibrary.simpleMessage("Processado"),
"processing": MessageLookupByLibrary.simpleMessage("Processando"),
@@ -1889,6 +1890,8 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage(
"Algo deu errado. Tente outra vez"),
"sorry": MessageLookupByLibrary.simpleMessage("Desculpe"),
"sorryBackupFailedDesc": MessageLookupByLibrary.simpleMessage(
"Desculpe, não podemos fazer cópia de segurança deste arquivo no momento, nós tentaremos mais tarde."),
"sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage(
"Desculpe, não foi possível adicionar aos favoritos!"),
"sorryCouldNotRemoveFromFavorites":
@@ -2192,7 +2195,7 @@ class MessageLookup extends MessageLookupByLibrary {
"youCannotDowngradeToThisPlan": MessageLookupByLibrary.simpleMessage(
"Você não pode rebaixar para este plano"),
"youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage(
"Você não pode compartilhar consigo mesmo"),
"Não é possível compartilhar consigo mesmo"),
"youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage(
"Você não tem nenhum item arquivado."),
"youHaveSuccessfullyFreedUp": m113,

View File

@@ -7280,10 +7280,10 @@ class S {
);
}
/// `We have preserved over 30 million memories so far`
/// `We have preserved over 200 million memories so far`
String get loadMessage2 {
return Intl.message(
'We have preserved over 30 million memories so far',
'We have preserved over 200 million memories so far',
name: 'loadMessage2',
desc: '',
args: [],

View File

@@ -721,6 +721,7 @@
"type": "text"
},
"backupFailed": "Sicherung fehlgeschlagen",
"sorryBackupFailedDesc": "Leider konnten wir diese Datei momentan nicht sichern, wir werden es später erneut versuchen.",
"couldNotBackUpTryLater": "Deine Daten konnten nicht gesichert werden.\nWir versuchen es später erneut.",
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente kann Dateien nur verschlüsseln und sichern, wenn du den Zugriff darauf gewährst",
"pleaseGrantPermissions": "Bitte erteile die nötigen Berechtigungen",

View File

@@ -1030,7 +1030,7 @@
"didYouKnow": "Did you know?",
"loadingMessage": "Loading your photos...",
"loadMessage1": "You can share your subscription with your family",
"loadMessage2": "We have preserved over 30 million memories so far",
"loadMessage2": "We have preserved over 200 million memories so far",
"loadMessage3": "We keep 3 copies of your data, one in an underground fallout shelter",
"loadMessage4": "All our apps are open source",
"loadMessage5": "Our source code and cryptography have been externally audited",

View File

@@ -371,6 +371,21 @@
"deleteFromBoth": "Eliminar de ambos",
"newAlbum": "Nuevo álbum",
"albums": "Álbumes",
"memoryCount": "{count, plural, =0{no hay recuerdos} one{{formattedCount} recuerdo} other{{formattedCount} recuerdos}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
"placeholders": {
"count": {
"example": "1",
"type": "int"
},
"formattedCount": {
"type": "String",
"example": "11.513, 11,511"
}
}
},
"selectedPhotos": "{count} seleccionados",
"@selectedPhotos": {
"description": "Display the number of selected photos",
@@ -777,6 +792,14 @@
"share": "Compartir",
"unhideToAlbum": "Hacer visible al álbum",
"restoreToAlbum": "Restaurar al álbum",
"moveItem": "{count, plural,=1 {Mover objeto} other {Mover objetos}}",
"@moveItem": {
"description": "Page title while moving one or more items to an album"
},
"addItem": "{count, plural, =1 {Añadir objeto} other {Añadir objetos}}",
"@addItem": {
"description": "Page title while adding one or more items to album"
},
"createOrSelectAlbum": "Crear o seleccionar álbum",
"selectAlbum": "Seleccionar álbum",
"searchByAlbumNameHint": "Nombre del álbum",
@@ -874,6 +897,7 @@
"authToViewYourMemories": "Por favor, autentícate para ver tus recuerdos",
"unlock": "Desbloquear",
"freeUpSpace": "Liberar espacio",
"freeUpSpaceSaving": "{count, plural, =1 {Se puede eliminar del dispositivo para liberar {formattedSize}} other {Se pueden eliminar del dispositivo para liberar {formattedSize}}}",
"filesBackedUpInAlbum": "Se ha realizado la copia de seguridad de {count, plural, one {1 archivo} other {{formattedNumber} archivos}} de este álbum de forma segura",
"@filesBackedUpInAlbum": {
"description": "Text to tell user how many files have been backed up in the album",
@@ -904,6 +928,18 @@
}
}
},
"@freeUpSpaceSaving": {
"description": "Text to tell user how much space they can free up by deleting items from the device"
},
"freeUpAccessPostDelete": "Aún puedes acceder {count, plural,=1 {a él} other {a ellos}} en Ente mientras tengas una suscripción activa",
"@freeUpAccessPostDelete": {
"placeholders": {
"count": {
"example": "1",
"type": "int"
}
}
},
"freeUpAmount": "Liberar {sizeInMBorGB}",
"thisEmailIsAlreadyInUse": "Este correo electrónico ya está en uso",
"incorrectCode": "Código incorrecto",
@@ -1231,6 +1267,8 @@
"description": "Subtitle to indicate that the user can find people quickly by name"
},
"findPeopleByName": "Encuentra gente rápidamente por su nombre",
"addViewers": "{count, plural, =0 {Añadir espectador} =1{Añadir espectador} other {Añadir espectadores}}",
"addCollaborators": "{count, plural, =0 {Añadir colaborador} =1 {Añadir colaborador} other {Añadir colaboradores}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Mantén pulsado un correo electrónico para verificar el cifrado de extremo a extremo.",
"developerSettingsWarning": "¿Estás seguro de que quieres modificar los ajustes de desarrollador?",
"developerSettings": "Ajustes de desarrollador",
@@ -1362,6 +1400,16 @@
"enableMachineLearningBanner": "Activar aprendizaje automático para búsqueda mágica y reconocimiento facial",
"searchDiscoverEmptySection": "Las imágenes se mostrarán aquí cuando se complete el procesado y la sincronización",
"searchPersonsEmptySection": "Las personas se mostrarán aquí cuando se complete el procesado y la sincronización",
"viewersSuccessfullyAdded": "{count, plural, =0 {0 espectadores añadidos} =1 {1 espectador añadido} other {{count} espectadores añadidos}}",
"@viewersSuccessfullyAdded": {
"placeholders": {
"count": {
"type": "int",
"example": "2"
}
},
"description": "Number of viewers that were successfully added to an album."
},
"collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 colaboradores añadidos} =1 {1 colaborador añadido} other {{count} colaboradores añadidos}}",
"@collaboratorsSuccessfullyAdded": {
"placeholders": {
@@ -1437,6 +1485,15 @@
},
"currentlyRunning": "ejecutando",
"ignored": "ignorado",
"photosCount": "{count, plural, =0 {0 fotos} =1 {1 foto} other {{count} fotos}}",
"@photosCount": {
"placeholders": {
"count": {
"type": "int",
"example": "2"
}
}
},
"file": "Archivo",
"searchSectionsLengthMismatch": "La longitud de las secciones no coincide: {snapshotLength} != {searchLength}",
"@searchSectionsLengthMismatch": {
@@ -1617,17 +1674,30 @@
"selectTime": "Seleccionar hora",
"selectDate": "Seleccionar fecha",
"previous": "Anterior",
"selectOneDateAndTimeForAll": "Seleccione una fecha y hora para todas",
"selectStartOfRange": "Seleccionar inicio del rango",
"thisWillMakeTheDateAndTimeOfAllSelected": "Esto hará que la fecha y la hora de todas las fotos seleccionadas sean las mismas.",
"allWillShiftRangeBasedOnFirst": "Este es el primero en el grupo. Otras fotos seleccionadas cambiarán automáticamente basándose en esta nueva fecha",
"newRange": "Nuevo rango",
"selectOneDateAndTime": "Seleccionar fecha y hora",
"moveSelectedPhotosToOneDate": "Mover las fotos seleccionadas a una fecha",
"shiftDatesAndTime": "Cambiar fechas y hora",
"photosKeepRelativeTimeDifference": "Las fotos mantienen una diferencia de tiempo relativa",
"photocountPhotos": "{count, plural, =0 {No hay fotos} =1 {1 foto} other {{count} fotos}}",
"@photocountPhotos": {
"placeholders": {
"count": {
"type": "int",
"example": "2"
}
}
},
"appIcon": "Ícono",
"notThisPerson": "¿No es esta persona?",
"selectedItemsWillBeRemovedFromThisPerson": "Los elementos seleccionados se eliminarán de esta persona, pero no se eliminarán de tu biblioteca.",
"throughTheYears": "{dateFormat} a través de los años",
"thisWeekThroughTheYears": "Esta semana a través de los años",
"thisWeekXYearsAgo": "{count, plural, =1 {Esta semana, hace {count} año} other {Esta semana, hace {count} años}}",
"youAndThem": "Tú y {name}",
"admiringThem": "Admirando a {name}",
"embracingThem": "Abrazando a {name}",
@@ -1639,6 +1709,9 @@
"backgroundWithThem": "Preciosas vistas con {name}",
"sportsWithThem": "Deportes con {name}",
"roadtripWithThem": "Viaje en carretera con {name}",
"spotlightOnYourself": "Enfócate a ti mismo",
"spotlightOnThem": "Enfocar a {name}",
"personIsAge": "¡{name} tiene {age} años!",
"personTurningAge": "{name} cumpliendo {age} pronto",
"lastTimeWithThem": "Última vez con {name}",
"tripToLocation": "Viaje a {location}",
@@ -1656,9 +1729,12 @@
"cLIcon": "Nuevo ícono",
"cLIconDesc": "Por fin, un nuevo icono de la aplicación, que creemos que representa mejor nuestro trabajo. También hemos añadido una opción para que puedas seguir utilizando el icono anterior.",
"cLMemories": "Recuerdos",
"cLMemoriesDesc": "Redescubre tus momentos especiales: enfócate en tu gente favorita, tus viajes y vacaciones, tus mejores clics, y mucho más. Activa el aprendizaje de automático, etiquétate a ti mismo y etiqueta a tus amigos para la mejor experiencia.",
"cLWidgets": "Widgets",
"cLWidgetsDesc": "Ya están disponibles los widgets de pantalla de inicio con tus recuerdos. Podrás ver tus momentos especiales sin abrir la aplicación.",
"cLFamilyPlan": "Límites de plan familiar",
"cLFamilyPlanDesc": "Ahora puede establecer límites en cuanto al almacenamiento que los miembros de tu familia pueden utilizar.",
"cLBulkEditDesc": "Ahora puedes seleccionar múltiples fotos y editar la fecha/hora para todas ellas con una acción rápida. También es posible cambiar las fechas."
"cLBulkEdit": "Edición masiva de fechas",
"cLBulkEditDesc": "Ahora puedes seleccionar múltiples fotos y editar la fecha/hora para todas ellas con una acción rápida. También es posible cambiar las fechas.",
"curatedMemories": "Memorias revisadas"
}

View File

@@ -290,5 +290,173 @@
"details": "Detaileak",
"claimMore": "Eskatu gehiago!",
"theyAlsoGetXGb": "Haiek ere lortuko dute {storageAmountInGB} GB",
"freeStorageOnReferralSuccess": "{storageAmountInGB} GB norbaitek ordainpeko plan batean sartzen denean zure kodea aplikatzen badu"
"freeStorageOnReferralSuccess": "{storageAmountInGB} GB norbaitek ordainpeko plan batean sartzen denean zure kodea aplikatzen badu",
"shareTextReferralCode": "Sartu erreferentzia kodea: {referralCode}\n\nAplikatu hemen: Ezarpenak → Orokorra→ Erreferentziak, {referralStorageInGB} GB dohainik izateko ordainpeko plan batean \n\nhttps://ente.io",
"claimFreeStorage": "Eskatu debaldeko biltegiratzea",
"inviteYourFriends": "Gonbidatu zure lagunak",
"failedToFetchReferralDetails": "Ezin dugu zure erreferentziaren detailerik lortu. Mesedez, saiatu berriro geroago.",
"referralStep1": "1. Eman kode hau zure lagunei",
"referralStep2": "2. Haiek ordainpeko plan batean sinatu behar dute",
"referralStep3": "3. Bai zuk bai haiek {storageInGB} GB* dohainik izango duzue",
"referralsAreCurrentlyPaused": "Erreferentziak momentuz geldituta daude",
"youCanAtMaxDoubleYourStorage": "* Gehienez zure biltegiratzea bikoiztu ahal duzu",
"claimedStorageSoFar": "{isFamilyMember, select,true {Zure familiak {storageAmountInGb} GB eskatu du dagoeneko} false {Zuk {storageAmountInGb} GB eskatu duzu dagoeneko} other {Zuk {storageAmountInGb} GB eskatu duzu dagoeneko!}}",
"@claimedStorageSoFar": {
"placeholders": {
"isFamilyMember": {
"type": "String",
"example": "true"
},
"storageAmountInGb": {
"type": "int",
"example": "10"
}
}
},
"faq": "FAQ",
"help": "Laguntza",
"oopsSomethingWentWrong": "Oops, zerbait txarto joan da",
"peopleUsingYourCode": "Jendea zure kodea erabiltzen",
"eligible": "aukerakoak",
"total": "osotara",
"codeUsedByYou": "Zuk erabilitako kodea",
"freeStorageClaimed": "Debaldeko biltegiratzea eskatuta",
"freeStorageUsable": "Debaldeko biltegiratzea erabilgarri",
"usableReferralStorageInfo": "Biltegiratze erabilgarria zure oraingo planaren arabera mugatuta dago. Soberan eskatutako biltegiratzea automatikoki erabili ahal izango duzu zure plan gaurkotzen duzunean.",
"removeFromAlbumTitle": "Albumetik kendu?",
"removeFromAlbum": "Kendu albumetik",
"itemsWillBeRemovedFromAlbum": "Hautatutako elementuak album honetatik kenduko dira",
"removeShareItemsWarning": "Kentzen ari zaren elementu batzuk beste pertsona batzuek gehitu zituzten, beraz ezin izango dituzu eskuratu",
"addingToFavorites": "Gogokoetan gehitzen...",
"removingFromFavorites": "Gogokoetatik kentzen...",
"sorryCouldNotAddToFavorites": "Sentitzen dut, ezin izan dugu zure gogokoetan gehitu!",
"sorryCouldNotRemoveFromFavorites": "Sentitzen dugu, ezin izan dugu zure gogokoetatik kendu!",
"subscribeToEnableSharing": "Ordainpeko harpidetza behar duzu partekatzea aktibatzeko.",
"subscribe": "Harpidetu",
"canOnlyRemoveFilesOwnedByYou": "Zure fitxategiak baino ezin duzu ezabatu",
"deleteSharedAlbum": "Partekatutako albuma ezabatu?",
"deleteAlbum": "Ezabatu albuma",
"deleteAlbumDialog": "Ezabatu nahi dituzu album honetan dauden argazkiak (eta bideoak) parte diren beste album <bold>guztietatik</bold> ere?",
"deleteSharedAlbumDialogBody": "Albuma guztiontzat ezabatuko da \n\nAlbum honetan dauden beste pertsonek partekatutako argazkiak ezin izango dituzu eskuratu",
"yesRemove": "Bai, ezabatu",
"creatingLink": "Esteka sortzen...",
"removeWithQuestionMark": "Ezabatuko?",
"removeParticipantBody": "{userEmail} partekatutako album honetatik ezabatuko da \n\nHaiek gehitutako argazki guztiak ere ezabatuak izango dira albumetik",
"keepPhotos": "Gorde Argazkiak",
"deletePhotos": "Ezabatu argazkiak",
"inviteToEnte": "Gonbidatu Ente-ra",
"removePublicLink": "Ezabatu esteka publikoa",
"disableLinkMessage": "Honen bidez {albumName} eskuratzeko esteka publikoa ezabatuko da.",
"sharing": "Partekatzen...",
"youCannotShareWithYourself": "Ezin duzu zeure buruarekin partekatu",
"archive": "Artxiboa",
"createAlbumActionHint": "Luze klikatu argazkiak hautatzeko eta klikatu + albuma sortzeko",
"importing": "Inportatzen....",
"failedToLoadAlbums": "Errorea albumak kargatzen",
"hidden": "Ezkutatuta",
"authToViewYourHiddenFiles": "Mesedez, autentifikatu zure ezkutatutako fitxategiak ikusteko",
"authToViewTrashedFiles": "Mesedez, autentifikatu paperontzira botatako zure fitxategiak ikusteko",
"trash": "Zarama",
"uncategorized": "Kategori gabekoa",
"videoSmallCase": "bideoa",
"photoSmallCase": "argazkia",
"singleFileDeleteHighlight": "Album guztietatik ezabatuko da.",
"singleFileInBothLocalAndRemote": "{fileType} hau Ente-n eta zure gailuan dago.",
"singleFileInRemoteOnly": "{fileType} hau Ente-tik ezabatuko da.",
"singleFileDeleteFromDevice": "{fileType} hau zure gailutik ezabatuko da.",
"deleteFromEnte": "Ezabatu Ente-tik",
"yesDelete": "Bai, ezabatu",
"movedToTrash": "Zarama mugituta",
"deleteFromDevice": "Ezabatu gailutik",
"deleteFromBoth": "Ezabatu bietatik",
"newAlbum": "Album berria",
"albums": "Albumak",
"memoryCount": "{count, plural,=0{oroitzapenik ez}one{oroitzapen {formattedCount}} other{{formattedCount} oroitzapen}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
"placeholders": {
"count": {
"example": "1",
"type": "int"
},
"formattedCount": {
"type": "String",
"example": "11.513, 11,511"
}
}
},
"selectedPhotos": "{count} hautatuta",
"@selectedPhotos": {
"description": "Display the number of selected photos",
"type": "text",
"placeholders": {
"count": {
"example": "5",
"type": "int"
}
}
},
"selectedPhotosWithYours": "{count} hautatuta ({yourCount} zureak)",
"@selectedPhotosWithYours": {
"description": "Display the number of selected photos, including the number of selected photos owned by the user",
"type": "text",
"placeholders": {
"count": {
"example": "12",
"type": "int"
},
"yourCount": {
"example": "2",
"type": "int"
}
}
},
"advancedSettings": "Aurreratuak",
"@advancedSettings": {
"description": "The text to display in the advanced settings section"
},
"photoGridSize": "Argazki sarearen tamaina",
"manageDeviceStorage": "Kudeatu gailuaren katxea",
"manageDeviceStorageDesc": "Berrikusi eta garbitu katxe lokalaren biltegiratzea.",
"machineLearning": "Ikasketa automatikoa",
"mlConsent": "Aktibatu ikasketa automatikoa",
"mlConsentTitle": "Ikasketa automatikoa aktibatuko?",
"mlConsentDescription": "Ikasketa automatikoa aktibatuz gero, Ente-k fitxategietatik informazioa aterako du (ad. argazkien geometria), zurekin partekatutako argazkietatik ere.\n\nHau zure gailuan gertatuko da, eta sortutako informazio biometrikoa puntutik puntura zifratuta egongo da.",
"mlConsentPrivacy": "Mesedez, klikatu hemen gure pribatutasun politikan ezaugarri honi buruz detaile gehiago izateko",
"mlConsentConfirmation": "Ulertzen dut, eta ikasketa automatikoa aktibatu nahi dut",
"magicSearch": "Bilaketa magikoa",
"discover": "Aurkitu",
"@discover": {
"description": "The text to display for the discover section under which we show receipts, screenshots, sunsets, greenery, etc."
},
"discover_identity": "Nortasuna",
"discover_screenshots": "Pantaila argazkiak",
"discover_receipts": "Ordainagiriak",
"discover_notes": "Oharrak",
"discover_memes": "Memeak",
"discover_visiting_cards": "Bisita txartelak",
"discover_babies": "Umeak",
"discover_pets": "Etxe-animaliak",
"discover_selfies": "Selfiak",
"discover_wallpapers": "Horma-paperak",
"discover_food": "Janaria",
"discover_celebrations": "Ospakizunak",
"discover_sunset": "Eguzki-sartzea",
"discover_hills": "Muinoak",
"discover_greenery": "Hostoa",
"authToChangeYourEmail": "Mesedez, autentifikatu zure emaila aldatzeko",
"authToChangeYourPassword": "Mesedez, autentifikatu zure pasahitza aldatzeko",
"authToChangeEmailVerificationSetting": "Mesedez, autentifikatu emailaren egiaztatzea aldatzeko",
"authToInitiateAccountDeletion": "Mesedez, autentifikatu kontu ezabaketa hasteko",
"authToViewYourRecoveryKey": "Mesedez, autentifikatu zure berreskuratze giltza ikusteko",
"authToConfigureTwofactorAuthentication": "Mesedez, autentifikatu faktore biko autentifikazioa konfiguratzeko",
"authToChangeLockscreenSetting": "Mesedez, autentifikatu pantaila blokeatzeko ezarpenak aldatzeko",
"authToViewYourActiveSessions": "Mesedez, autentifikatu indarrean dauden zure saioak ikusteko",
"confirm2FADisable": "Seguru zaude faktore biko autentifikazioa deuseztatu nahi duzula?",
"twofactorAuthenticationHasBeenDisabled": "Faktore biko autentifikazioa deuseztatua izan da",
"iOSLockOut": "Autentifikazio biometrikoa deuseztatuta dago. Mesedez, blokeatu eta desblokeatu zure pantaila indarrean jartzeko.",
"@iOSLockOut": {
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
}
}

View File

@@ -460,7 +460,7 @@
}
}
},
"showMemories": "Montrer les souvenirs",
"showMemories": "Afficher les souvenirs",
"yearsAgo": "{count, plural, one{il y a {count} an} other{il y a {count} ans}}",
"backupSettings": "Paramètres de la sauvegarde",
"backupStatus": "État de la sauvegarde",
@@ -1678,5 +1678,6 @@
"cLFamilyPlan": "Limites pour le forfait Famille",
"cLFamilyPlanDesc": "Vous pouvez maintenant fixer des limites sur la quantité de stockage que les membres de votre famille peuvent utiliser.",
"cLBulkEdit": "Dates de modification multiples",
"cLBulkEditDesc": "Vous pouvez maintenant sélectionner plusieurs photos et modifier la date/heure pour toutes celles-ci, en une seule action rapide. Les dates de décalage sont également prises en charge."
"cLBulkEditDesc": "Vous pouvez maintenant sélectionner plusieurs photos et modifier la date/heure pour toutes celles-ci, en une seule action rapide. Les dates de décalage sont également prises en charge.",
"curatedMemories": "Souvenirs conservés"
}

View File

@@ -371,6 +371,21 @@
"deleteFromBoth": "Slett fra begge",
"newAlbum": "Nytt album",
"albums": "Album",
"memoryCount": "{count, plural, =0{ingen minner} one{{formattedCount} minne} other{{formattedCount} minner}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
"placeholders": {
"count": {
"example": "1",
"type": "int"
},
"formattedCount": {
"type": "String",
"example": "11.513, 11,511"
}
}
},
"selectedPhotos": "{count} valgt",
"@selectedPhotos": {
"description": "Display the number of selected photos",
@@ -777,6 +792,14 @@
"share": "Del",
"unhideToAlbum": "Gjør synlig i album",
"restoreToAlbum": "Gjenopprett til album",
"moveItem": "{count, plural, =1 {Flytt elementet} other {Flytt elementene}}",
"@moveItem": {
"description": "Page title while moving one or more items to an album"
},
"addItem": "{count, plural, =1 {Legg til element} other {Legg til elementene}}",
"@addItem": {
"description": "Page title while adding one or more items to album"
},
"createOrSelectAlbum": "Opprett eller velg album",
"selectAlbum": "Velg album",
"searchByAlbumNameHint": "Albumnavn",

View File

@@ -11,7 +11,7 @@
"enterValidEmail": "Insira um endereço de e-mail válido.",
"deleteAccount": "Excluir conta",
"askDeleteReason": "Por que você quer excluir sua conta?",
"deleteAccountFeedbackPrompt": "Lamentamos você ir. Compartilhe seu feedback para nos ajudar a melhorar.",
"deleteAccountFeedbackPrompt": "Lamentamos você ir. Compartilhe seu feedback para ajudar-nos a melhorar.",
"feedback": "Feedback",
"kindlyHelpUsWithThisInformation": "Ajude-nos com esta informação",
"confirmDeletePrompt": "Sim, eu quero permanentemente excluir esta conta e os dados em todos os aplicativos.",
@@ -158,7 +158,7 @@
"addCollaborator": "Adicionar colaborador",
"addANewEmail": "Adicionar um novo e-mail",
"orPickAnExistingOne": "Ou escolha um existente",
"collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Os colaboradores podem adicionar fotos e vídeos ao álbum compartilhado.",
"collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Colaboradores podem adicionar fotos e vídeos ao álbum compartilhado.",
"enterEmail": "Inserir e-mail",
"albumOwner": "Proprietário",
"@albumOwner": {
@@ -292,7 +292,7 @@
"theyAlsoGetXGb": "Eles também recebem {storageAmountInGB} GB",
"freeStorageOnReferralSuccess": "{storageAmountInGB} GB cada vez que alguém se inscrever a um plano pago e aplicar seu código",
"shareTextReferralCode": "Código de referência do Ente: {referralCode} \n\nAplique-o em Configurações → Geral → Referências para obter {referralStorageInGB} GB grátis após a sua inscrição num plano pago\n\nhttps://ente.io",
"claimFreeStorage": "Reivindicar armazenamento grátis",
"claimFreeStorage": "Reivindique armaz. grátis",
"inviteYourFriends": "Convide seus amigos",
"failedToFetchReferralDetails": "Não foi possível buscar os detalhes de referência. Tente novamente mais tarde.",
"referralStep1": "1. Envie este código aos seus amigos",
@@ -320,7 +320,7 @@
"eligible": "elegível",
"total": "total",
"codeUsedByYou": "Código usado por você",
"freeStorageClaimed": "Armazenamento grátis reivindicado",
"freeStorageClaimed": "Armaz. grátis reivindicado",
"freeStorageUsable": "Armazenamento disponível",
"usableReferralStorageInfo": "O armazenamento disponível é limitado pelo seu plano atual. O excesso de armazenamento reivindicado tornará automaticamente útil quando você atualizar seu plano.",
"removeFromAlbumTitle": "Remover do álbum?",
@@ -341,15 +341,15 @@
"yesRemove": "Sim, excluir",
"creatingLink": "Criando link...",
"removeWithQuestionMark": "Remover?",
"removeParticipantBody": "{userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum",
"removeParticipantBody": "{userEmail} será removido do álbum compartilhado\n\nQualquer foto adicionada por ele será removida.",
"keepPhotos": "Manter fotos",
"deletePhotos": "Excluir fotos",
"inviteToEnte": "Convidar ao Ente",
"removePublicLink": "Remover link público",
"disableLinkMessage": "Isso removerá o link público para acessar \"{albumName}\".",
"sharing": "Compartilhando...",
"youCannotShareWithYourself": "Você não pode compartilhar consigo mesmo",
"archive": "Arquivo",
"youCannotShareWithYourself": "Não é possível compartilhar consigo mesmo",
"archive": "Arquive",
"createAlbumActionHint": "Pressione para selecionar fotos e clique em + para criar um álbum",
"importing": "Importando....",
"failedToLoadAlbums": "Falhou ao carregar álbuns",
@@ -419,12 +419,12 @@
"photoGridSize": "Tamanho da grade de fotos",
"manageDeviceStorage": "Gerenciar cache do dispositivo",
"manageDeviceStorageDesc": "Reveja e limpe o armazenamento de cache local.",
"machineLearning": "Aprendizagem automática",
"mlConsent": "Ativar aprendizagem automática",
"mlConsentTitle": "Ativar aprendizagem automática?",
"mlConsentDescription": "Se você ativar a aprendizagem automática, o Ente irá extrair informações como geometria de rosto dos arquivos, incluindo os compartilhados com você.\n\nIsso acontecerá no seu dispositivo, qualquer informação biométrica gerada será criptografada ponta a ponta.",
"machineLearning": "Aprendizado automático",
"mlConsent": "Ativar o aprendizado automático",
"mlConsentTitle": "Ativar aprendizado automático?",
"mlConsentDescription": "Se ativar o aprendizado automático, Ente extrairá informações de geometria facial dos arquivos, incluindo aqueles compartilhados consigo.\n\nIsso acontecerá em seu dispositivo, e qualquer informação biométrica gerada será criptografada de ponta a ponta.",
"mlConsentPrivacy": "Clique aqui para mais detalhes sobre este recurso na política de privacidade",
"mlConsentConfirmation": "Eu entendo, e desejo ativar a aprendizagem automática",
"mlConsentConfirmation": "Concordo e desejo ativá-lo",
"magicSearch": "Busca mágica",
"discover": "Explorar",
"@discover": {
@@ -445,7 +445,7 @@
"discover_sunset": "Pôr do sol",
"discover_hills": "Colinas",
"discover_greenery": "Vegetação",
"mlIndexingDescription": "Note que a aprendizagem automática resultará em uso de bateria e largura de banda maior até que todos os itens forem indexados. Considere-se usar o aplicativo para notebook para uma indexação mais rápida, todos os resultados serão sincronizados automaticamente.",
"mlIndexingDescription": "Saiba que o aprendizado automático afetará a bateria do dispositivo negativamente até todos os itens serem indexados. Utilize a versão para computadores para melhor indexação, todos os resultados se auto-sincronizaram.",
"loadingModel": "Baixando modelos...",
"waitingForWifi": "Aguardando Wi-Fi...",
"status": "Estado",
@@ -597,7 +597,7 @@
"freeTrial": "Avaliação grátis",
"selectYourPlan": "Selecione seu plano",
"enteSubscriptionPitch": "O Ente preserva suas memórias, então eles sempre estão disponíveis para você, mesmo se você perder o dispositivo.",
"enteSubscriptionShareWithFamily": "Sua família também pode ser adicionada ao seu plano.",
"enteSubscriptionShareWithFamily": "Sua família também pode ser adicionada ao seu plano.",
"currentUsageIs": "O uso atual é ",
"@currentUsageIs": {
"description": "This text is followed by storage usage",
@@ -721,11 +721,12 @@
"type": "text"
},
"backupFailed": "Falhou ao copiar com segurança",
"sorryBackupFailedDesc": "Desculpe, não podemos fazer cópia de segurança deste arquivo no momento, nós tentaremos mais tarde.",
"couldNotBackUpTryLater": "Nós não podemos copiar com segurança seus dados.\nNós tentaremos novamente mais tarde.",
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente pode criptografar e preservar arquivos apenas se você conceder acesso a eles",
"pleaseGrantPermissions": "Por favor, conceda as permissões",
"grantPermission": "Conceder permissões",
"privateSharing": "Compartilhamento privado",
"privateSharing": "Compartilha privada",
"shareOnlyWithThePeopleYouWant": "Compartilhar apenas com as pessoas que você quiser",
"usePublicLinksForPeopleNotOnEnte": "Usar links públicos para pessoas que não estão no Ente",
"allowPeopleToAddPhotos": "Permitir que pessoas adicionem fotos",
@@ -1328,7 +1329,7 @@
"enable": "Ativar",
"enabled": "Ativado",
"moreDetails": "Mais detalhes",
"enableMLIndexingDesc": "Ente suporta aprendizagem de máquina para reconhecimento facial, busca mágica e outros recursos de busca avançados",
"enableMLIndexingDesc": "Ente fornece aprendizado automático no dispositivo para reconhecimento facial, busca mágica e outros recursos de busca avançados.",
"magicSearchHint": "A busca mágica permite buscar fotos pelo conteúdo, p. e.x. 'flor', 'carro vermelho', 'identidade'",
"panorama": "Panorama",
"reenterPassword": "Reinserir senha",
@@ -1397,7 +1398,7 @@
"yesResetPerson": "Sim, redefinir pessoa",
"onlyThem": "Apenas eles",
"checkingModels": "Verificando modelos...",
"enableMachineLearningBanner": "Ativar aprendizagem de máquina para busca mágica e reconhecimento facial",
"enableMachineLearningBanner": "Ativar o aprendizado automático para busca mágica e reconhecimento facial",
"searchDiscoverEmptySection": "As imagens serão exibidas aqui quando o processamento e sincronização for concluído",
"searchPersonsEmptySection": "As pessoas serão exibidas aqui quando o processamento e sincronização for concluído",
"viewersSuccessfullyAdded": "{count, plural, =0 {Adicionado 0 vizualizadores} =1 {Adicionado 1 visualizador} other {Adicionado {count} visualizadores}}",
@@ -1655,7 +1656,7 @@
"dontSave": "Não salvar",
"thisIsMeExclamation": "Este é você!",
"linkPerson": "Vincular pessoa",
"linkPersonCaption": "para melhor experiência de compartilhamento",
"linkPersonCaption": "para melhorar o compartilhamento",
"@linkPersonCaption": {
"description": "Caption for the 'Link person' title. It should be a continuation of the 'Link person' title. Just like how 'Link person' + 'for better sharing experience' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages."
},
@@ -1711,7 +1712,7 @@
"roadtripWithThem": "Viajando de carro com {name}",
"spotlightOnYourself": "Destacar si mesmo",
"spotlightOnThem": "Destacar {name}",
"personIsAge": "{name} está com {age}!",
"personIsAge": "{name} tem {age} anos!",
"personTurningAge": "{name} terá {age} em breve",
"lastTimeWithThem": "Últimos momentos com {name}",
"tripToLocation": "Viajem à {location}",
@@ -1723,13 +1724,13 @@
"beach": "Areia e o mar",
"city": "Na cidade",
"moon": "Na luz do luar",
"onTheRoad": "Na estrada de novo",
"food": "Prazer em culinária",
"pets": "Companhia de pelos",
"onTheRoad": "Na estrada novamente",
"food": "Amor por culinária",
"pets": "Companhias peludas",
"cLIcon": "Novo Ícone",
"cLIconDesc": "Finalmente, um novo ícone para o ente que acreditamos que represente melhor nosso trabalho. Também, adicionamos um alterador de ícone para que você ainda consiga utilizar o ícone antigo.",
"cLMemories": "Memórias",
"cLMemoriesDesc": "Redescubra seus momentos especiais - destaque pessoas importantes, suas viagens e celebrações, melhores visitas e muito mais. Ative a aprendizagem automática, mencione si mesmo e seus amigos para melhor experiência.",
"cLMemoriesDesc": "Relembre momentos especiais - destaque pessoas favoritas, suas viagens e feriados, melhores fotos, e muito mais. Ative o aprendizado automático, marque-se e nomeie seus amigos para melhorar a experiência.",
"cLWidgets": "Widgets",
"cLWidgetsDesc": "Widgets integrados com memórias já estão disponíveis. Eles apareceram com seus melhores momentos sem precisar abrir o ente.",
"cLFamilyPlan": "Limites de planos familiares",

View File

@@ -168,6 +168,7 @@ class MemoriesCacheService {
_logger.info(
"No update needed (shouldUpdate: $_shouldUpdate, forced: $forced)",
);
return;
}
_logger.info(
"Updating memories cache (shouldUpdate: $_shouldUpdate, forced: $forced)",

View File

@@ -16,6 +16,7 @@ import 'package:photos/ui/account/verify_recovery_page.dart';
import 'package:photos/ui/components/home_header_widget.dart';
import 'package:photos/ui/components/notification_widget.dart';
import 'package:photos/ui/home/header_error_widget.dart';
import "package:photos/ui/settings/backup/backup_settings_screen.dart";
import "package:photos/ui/settings/backup/backup_status_screen.dart";
import "package:photos/ui/settings/ml/enable_ml_consent.dart";
import 'package:photos/utils/navigation_util.dart';
@@ -34,7 +35,7 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
late StreamSubscription<SyncStatusUpdate> _subscription;
late StreamSubscription<NotificationEvent> _notificationSubscription;
bool _isPausedDueToNetwork = false;
bool _showStatus = false;
bool _showErrorBanner = false;
bool _showMlBanner = !flagService.hasGrantedMLConsent &&
@@ -45,6 +46,7 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
void initState() {
_subscription = Bus.instance.on<SyncStatusUpdate>().listen((event) {
_logger.info("Received event " + event.status.toString());
_isPausedDueToNetwork = event.status == SyncStatus.paused;
if (event.status == SyncStatus.error) {
setState(() {
_syncError = event.error;
@@ -100,7 +102,9 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
onTap: () {
routeToPage(
context,
const BackupStatusScreen(),
_isPausedDueToNetwork
? const BackupSettingsScreen()
: const BackupStatusScreen(),
forceCustomPageRoute: true,
).ignore();
},

View File

@@ -441,15 +441,18 @@ class _HomeWidgetState extends State<HomeWidget> {
_intentDataStreamSubscription =
ReceiveSharingIntent.instance.getMediaStream().listen(
(List<SharedMediaFile> value) {
if (value.isNotEmpty && value[0].path.contains("albums.ente.io")) {
if (value.isEmpty) {
return;
}
if (value[0].path.contains("albums.ente.io")) {
final uri = Uri.parse(value[0].path);
_handlePublicAlbumLink(uri);
return;
}
if (value.isNotEmpty &&
(value[0].mimeType == "image/*" ||
value[0].mimeType == "video/*")) {
if (value[0].mimeType != null &&
(value[0].mimeType!.contains("image") ||
value[0].mimeType!.contains("video"))) {
showDialog(
context: context,
builder: (BuildContext context) {

View File

@@ -10,7 +10,6 @@ import "package:photos/models/preview/preview_item_status.dart";
import "package:photos/services/filedata/filedata_service.dart";
import "package:photos/services/preview_video_store.dart";
import "package:photos/theme/colors.dart";
import "package:photos/ui/settings/backup/backup_status_screen.dart";
class PreviewStatusWidget extends StatefulWidget {
const PreviewStatusWidget({
@@ -110,14 +109,7 @@ class _PreviewStatusWidgetState extends State<PreviewStatusWidget> {
onTap:
preview == null || preview!.status == PreviewItemStatus.uploaded
? widget.onStreamChange
: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const BackupStatusScreen(),
),
);
},
: null,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,

View File

@@ -1,5 +1,6 @@
import "dart:async";
import "dart:io" show File;
import "dart:math" show pow;
import "package:flutter/foundation.dart";
import "package:logging/logging.dart";
@@ -16,22 +17,6 @@ import "package:photos/utils/file_util.dart";
import "package:photos/utils/standalone/task_queue.dart";
import "package:photos/utils/thumbnail_util.dart";
void resetPool({required bool fullFile}) {
if (fullFile) {
_queueFullFileFaceGenerations = TaskQueue<String>(
maxConcurrentTasks: 5,
taskTimeout: const Duration(minutes: 1),
maxQueueSize: 50,
);
} else {
_queueThumbnailFaceGenerations = TaskQueue<String>(
maxConcurrentTasks: 5,
taskTimeout: const Duration(minutes: 1),
maxQueueSize: 100,
);
}
}
final _logger = Logger("FaceCropUtils");
const int _retryLimit = 3;
@@ -40,7 +25,7 @@ final LRUMap<String, Uint8List?> _faceCropThumbnailCache = LRUMap(1000);
TaskQueue _queueFullFileFaceGenerations = TaskQueue<String>(
maxConcurrentTasks: 5,
taskTimeout: const Duration(minutes: 1),
maxQueueSize: 50,
maxQueueSize: 100,
);
TaskQueue _queueThumbnailFaceGenerations = TaskQueue<String>(
maxConcurrentTasks: 5,
@@ -127,18 +112,34 @@ Future<Map<String, Uint8List>?> getCachedFaceCrops(
}
return faceIdToCrop.isEmpty ? null : faceIdToCrop;
} catch (e, s) {
_logger.severe(
"Error getting face crops for faceIDs: ${faces.map((face) => face.faceID).toList()}",
e,
s,
);
resetPool(fullFile: useFullFile);
if (fetchAttempt <= _retryLimit) {
return getCachedFaceCrops(
enteFile,
faces,
fetchAttempt: fetchAttempt + 1,
useFullFile: useFullFile,
if (e is! TaskQueueTimeoutException &&
e is! TaskQueueOverflowException &&
e is! TaskQueueCancelledException) {
if (fetchAttempt <= _retryLimit) {
final backoff = Duration(
milliseconds: 100 * pow(2, fetchAttempt + 1).toInt(),
);
await Future.delayed(backoff);
_logger.warning(
"Error getting face crops for faceIDs: ${faces.map((face) => face.faceID).toList()}, retrying (attempt ${fetchAttempt + 1}) in ${backoff.inMilliseconds} ms",
e,
s,
);
return getCachedFaceCrops(
enteFile,
faces,
fetchAttempt: fetchAttempt + 1,
useFullFile: useFullFile,
);
}
_logger.severe(
"Error getting face crops for faceIDs: ${faces.map((face) => face.faceID).toList()}",
e,
s,
);
} else {
_logger.info(
"Stopped getting face crops for faceIDs: ${faces.map((face) => face.faceID).toList()} due to $e",
);
}
return null;

View File

@@ -642,8 +642,8 @@ packages:
dependency: "direct main"
description:
path: "flutter/flutter"
ref: flurrify
resolved-ref: f0f49e4238c1d59fdce06c51f94431831c191c82
ref: android-packaged
resolved-ref: "6d5d27a8c259eda6292f204a27fba53da70af20e"
url: "https://github.com/ente-io/ffmpeg-kit"
source: git
version: "6.0.3"

View File

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.4+1034
version: 1.0.10+1040
publish_to: none
environment:
@@ -71,7 +71,7 @@ dependencies:
git:
url: https://github.com/ente-io/ffmpeg-kit
path: flutter/flutter
ref: flurrify
ref: android-packaged
figma_squircle: ^0.6.3
file_saver: ^0.2.14
firebase_core: ^3.6.0

View File

@@ -263,6 +263,7 @@ func main() {
TrashRepository: trashRepo,
UserRepo: userRepo,
UsageCtrl: usageController,
AccessCtrl: accessCtrl,
CollectionRepo: collectionRepo,
TaskLockingRepo: taskLockingRepo,
DiscordController: discordController,

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/ente-io/museum/pkg/controller/access"
"runtime/debug"
"strconv"
"strings"
@@ -44,6 +45,7 @@ type FileController struct {
CollectionRepo *repo.CollectionRepository
TaskLockingRepo *repo.TaskLockRepository
QueueRepo *repo.QueueRepository
AccessCtrl access.Controller
S3Config *s3config.S3Config
ObjectCleanupCtrl *ObjectCleanupController
LockController *lock.LockController
@@ -317,8 +319,10 @@ func (c *FileController) GetUploadURLs(ctx context.Context, userID int64, count
// GetFileURL verifies permissions and returns a presigned url to the requested file
func (c *FileController) GetFileURL(ctx *gin.Context, userID int64, fileID int64) (string, error) {
err := c.verifyFileAccess(userID, fileID)
if err != nil {
if err := c.AccessCtrl.CanAccessFile(ctx, &access.CanAccessFileParams{
ActorUserID: userID,
FileIDs: []int64{fileID},
}); err != nil {
return "", stacktrace.Propagate(err, "")
}
url, err := c.getSignedURLForType(ctx, fileID, ente.FILE)
@@ -333,8 +337,10 @@ func (c *FileController) GetFileURL(ctx *gin.Context, userID int64, fileID int64
// GetThumbnailURL verifies permissions and returns a presigned url to the requested thumbnail
func (c *FileController) GetThumbnailURL(ctx *gin.Context, userID int64, fileID int64) (string, error) {
err := c.verifyFileAccess(userID, fileID)
if err != nil {
if err := c.AccessCtrl.CanAccessFile(ctx, &access.CanAccessFileParams{
ActorUserID: userID,
FileIDs: []int64{fileID},
}); err != nil {
return "", stacktrace.Propagate(err, "")
}
url, err := c.getSignedURLForType(ctx, fileID, ente.THUMBNAIL)
@@ -963,34 +969,6 @@ func (c *FileController) deleteObjectVersionFromHotStorage(objectKey string, ver
return nil
}
func (c *FileController) verifyFileAccess(actorUserID int64, fileID int64) error {
fileOwnerID, err := c.FileRepo.GetOwnerID(fileID)
if err != nil {
return stacktrace.Propagate(err, "")
}
if fileOwnerID != actorUserID {
cIDs, err := c.CollectionRepo.GetCollectionIDsSharedWithUser(actorUserID)
if err != nil {
return stacktrace.Propagate(err, "")
}
cwIDS, err := c.CollectionRepo.GetCollectionIDsSharedWithUser(fileOwnerID)
if err != nil {
return stacktrace.Propagate(err, "")
}
cIDs = append(cIDs, cwIDS...)
accessible, err := c.CollectionRepo.DoesFileExistInCollections(fileID, cIDs)
if err != nil {
return stacktrace.Propagate(err, "")
}
if !accessible {
return stacktrace.Propagate(ente.ErrPermissionDenied, "")
}
}
return nil
}
func (c *FileController) getObjectURL(s3Client *s3.S3, dc string, bucket *string, objectKey string) (ente.UploadURL, error) {
r, _ := s3Client.PutObjectRequest(&s3.PutObjectInput{
Bucket: bucket,

View File

@@ -10,7 +10,7 @@ set -e
dcv=""
if command -v docker >/dev/null
then
dcv=`docker compose version --short 2>/dev/null`
dcv=`docker compose version --short 2>/dev/null || echo`
fi
if test -z "$dcv"

View File

@@ -155,19 +155,19 @@
bottom: 0px;
right: 0;
margin: 20px 24px;
padding-inline: 16px;
border-radius: 3px;
/* Same opacity as the other controls. */
color: rgb(255 255 255 / 0.85);
background-color: rgb(0 0 0 / 0.2);
backdrop-filter: blur(10px);
text-align: right;
max-width: 375px;
max-height: 200px;
p {
margin: 12px 17px;
/* 4 lines max, ellipsis on overflow. */
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;

View File

@@ -373,21 +373,27 @@ export interface Electron {
// - ML
/**
* Create a new ML worker, terminating the older ones (if any).
* Trigger the creation of a new utility process of the given {@link type},
* terminating the older ones (if any).
*
* This creates a new Node.js utility process, and sets things up so that we
* can communicate directly with that utility process using a
* {@link MessagePort} that gets posted using "createMLWorker/port".
* {@link MessagePort} that gets posted on the "utilityProcessPort/<type>"
* channel.
*
* At the other end of that port will be an object that conforms to the
* {@link ElectronMLWorker} interface.
* The code running in the utility process is determined by the specific
* value of {@link type}. Thus, att the other end of that port will be an
* object that conforms to:
*
* - {@link ElectronMLWorker} interface, when type is "ml".
*
* For more details about the IPC flow, see: [Note: ML IPC].
*
* Note: For simplicity of implementation, we assume that there is at most
* one outstanding call to {@link createMLWorker}.
* one outstanding call to {@link triggerCreateUtilityProcess} for a given
* {@link type}.
*/
createMLWorker: () => void;
triggerCreateUtilityProcess: (type: UtilityProcessType) => void;
// - Watch
@@ -590,9 +596,12 @@ export interface Electron {
clearPendingUploads: () => Promise<void>;
}
export type UtilityProcessType = "ml";
/**
* The shape of the object exposed by the Node.js ML worker process on the
* message port that the web layer obtains by doing {@link createMLWorker}.
* The shape of the object exposed by the Node.js utility process listening on
* the other side message port that the web layer obtains by doing
* {@link triggerCreateUtilityProcess} with type "ml".
*/
export interface ElectronMLWorker {
/**

View File

@@ -315,9 +315,13 @@ export class FileViewerPhotoSwipe {
const videoQuality = intendedVideoQualityForFileID(file.id);
const itemData = itemDataForFile(file, { videoQuality }, () =>
pswp.refreshSlideContent(index),
);
const itemData = itemDataForFile(file, { videoQuality }, () => {
// When we get updated item data,
// 1. Clear cached data.
_currentAnnotatedFile = undefined;
// 2. Request a refresh.
pswp.refreshSlideContent(index);
});
if (itemData.fileType === FileType.video) {
const { videoPlaylistURL, videoURL } = itemData;

View File

@@ -7,9 +7,15 @@ import { type PublicAlbumsCredentials } from "ente-base/http";
import log from "ente-base/log";
import { fileLogID, type EnteFile } from "ente-media/file";
import { FileType } from "ente-media/file-type";
import {
getAllLocalFiles,
uniqueFilesByID,
} from "ente-new/photos/services/files";
import { settingsSnapshot } from "ente-new/photos/services/settings";
import { gunzip, gzip } from "ente-new/photos/utils/gzip";
import { randomSample } from "ente-utils/array";
import { ensurePrecondition } from "ente-utils/ensure";
import { wait } from "ente-utils/promise";
import { z } from "zod";
import {
initiateGenerateHLS,
@@ -43,9 +49,12 @@ interface VideoProcessingQueueItem {
* the current client. If present, this serves as an optimization allowing
* us to directly read the file off the user's file system.
*/
timestampedUploadItem: TimestampedFileSystemUploadItem | undefined;
timestampedUploadItem?: TimestampedFileSystemUploadItem;
}
const idleWaitInitial = 10 * 1000; /* 10 sec */
const idleWaitMax = idleWaitInitial * 2 ** 6; /* 640 sec */
/**
* Internal in-memory state shared by the functions in this module.
*
@@ -53,13 +62,31 @@ interface VideoProcessingQueueItem {
*/
class VideoState {
/**
* Queue of videos waiting to be processed.
* Queue of recently uploaded items waiting to be processed.
*/
videoProcessingQueue: VideoProcessingQueueItem[] = [];
liveQueue: VideoProcessingQueueItem[] = [];
/**
* Active queue processor, if any.
*/
queueProcessor: Promise<void> | undefined;
/**
* A promise that the main processing loop waits for in addition to the idle
* timeout. Can be resolved using {@link resolveTick}.
*/
tick: Promise<void> | undefined;
/**
* A function that can be called to resolve {@link tick}.
*
* See: [Note: Exiting idle wait of processing loop].
*/
resolveTick: (() => void) | undefined;
/**
* The time to sleep if nothing is pending.
*
* Goes from {@link idleWaitInitial} to {@link idleWaitMax} in doublings.
* Reset back to {@link idleWaitInitial} in case of any activity.
*/
idleWait = idleWaitInitial;
}
/**
@@ -338,8 +365,8 @@ export const processVideoNewUpload = (
processableUploadItem: ProcessableUploadItem,
) => {
// TODO(HLS):
if (!isVideoProcessingEnabled()) return;
if (!isDesktop) return;
if (!isVideoProcessingEnabled()) return;
if (file.metadata.fileType !== FileType.video) return;
if (processableUploadItem instanceof File) {
// While the types don't guarantee it, we really shouldn't be getting
@@ -352,12 +379,35 @@ export const processVideoNewUpload = (
}
// Enqueue the item.
_state.videoProcessingQueue.push({
_state.liveQueue.push({
file,
timestampedUploadItem: processableUploadItem,
});
// Tickle the processor if it isn't already running.
// Interrupt any idle timeouts if any, go go.
tickNow();
};
/**
* If {@link processQueue} is not already running, start it.
*
* If there is an existing {@link resolveTick} so that if perchance
* {@link processQueue} was waiting on an idle timeout, it wakes up now.
*
* Also create a new {@link tick} and {@link resolveTick} pair for use by
* subsequent calls to {@link tickNow}
*/
const tickNow = () => {
// See: [Note: Exiting idle wait of processing loop] for what this function
// is trying to do.
// Resolve the existing tick (if any).
if (_state.resolveTick) _state.resolveTick();
// Create a new resolvable pair.
_state.tick = new Promise((r) => (_state.resolveTick = r));
// Start the processor if it isn't already running.
_state.queueProcessor ??= processQueue();
};
@@ -365,25 +415,120 @@ export const isVideoProcessingEnabled = () =>
process.env.NEXT_PUBLIC_ENTE_WIP_VIDEO_STREAMING &&
settingsSnapshot().isInternalUser;
/**
* The video processing loop goes through videos one by one, preferring items in
* the liveQueue, otherwise working for a backlog item. If there are no items to
* process, it goes on an idle timeout. The {@link resolveTick} state property
* can be used to tickle it out of sleep.
*
* [Note: Exiting idle wait of processing loop]
*
* The following toy example illustrates the overall mechanism:
*
* let resolveTick
* let tick = new Promise((r) => (resolveTick = r));
*
* const tickNow = () => {
* resolveTick();
* tick = new Promise((r) => (resolveTick = r));
* }
*
* const f = async () => {
* for (let i of [1, 2, 3, 4, 5]) {
* const wait = new Promise((r) => setTimeout(r, i * 1e3));
* await Promise.race([wait, tick]);
* }
* }
*
* f()
*
* setTimeout(tickNow, 2500);
* setTimeout(tickNow, 5000);
* setTimeout(tickNow, 5500);
*
* The `Promise.race([wait, tick])` means that the loop will proceed to the next
* item either if the timeout expires, or if tick resolves. Thus the same
* function can handle both the internally determined processing of backfill
* batches, and the externally triggered processing of live uploads.
*/
const processQueue = async () => {
while (true) {
const item = _state.videoProcessingQueue.shift();
if (!item) break;
try {
await processQueueItem(item);
} catch (e) {
log.error("Video processing failed", e);
// Ignore this unprocessable item. Currently this function only runs
// post upload, so this item will later get processed as part of the
// backfill.
//
// TODO(HLS): When processing the backfill itself, we'll need a way
// to mark this item as failed.
if (!(isDesktop && isVideoProcessingEnabled())) {
assertionFailed(); /* we shouldn't have come here */
return;
}
let bq: typeof _state.liveQueue | undefined;
while (isVideoProcessingEnabled()) {
log.debug(() => ["gen-hls-iter", []]);
const item =
_state.liveQueue.shift() ?? (bq ??= await backfillQueue()).shift();
if (item) {
try {
await processQueueItem(item);
} catch (e) {
log.error("Video processing failed", e);
} finally {
// TODO(HLS): This needs to be more granular in case of errors.
await markProcessedVideoFileID(item.file.id);
}
// Reset the idle wait on any activity.
_state.idleWait = idleWaitInitial;
} else {
// Replenish the backfill queue if possible.
bq = await backfillQueue();
if (!bq.length) {
// There are no more items in either the live queue or backlog.
// Go to sleep (for increasingly longer durations, capped at a
// maximum).
const idleWait = _state.idleWait;
_state.idleWait = Math.min(idleWait * 2, idleWaitMax);
// `tick` allows the sleep to be interrupted when there is
// potential activity.
if (!_state.tick) assertionFailed();
const tick = _state.tick!;
log.debug(() => ["gen-hls", { idleWait }]);
await Promise.race([tick, wait(idleWait)]);
}
}
}
_state.queueProcessor = undefined;
};
/**
* Return the next batch of videos that need to be processed.
*
* If there is nothing pending, return an empty array.
*/
const backfillQueue = async (): Promise<VideoProcessingQueueItem[]> => {
const allCollectionFiles = await getAllLocalFiles();
const processedVideoFileIDs = await savedProcessedVideoFileIDs();
const videoFiles = uniqueFilesByID(
allCollectionFiles.filter((f) => f.metadata.fileType == FileType.video),
);
const pendingVideoFiles = videoFiles.filter(
(f) => !processedVideoFileIDs.has(f.id),
);
const batch = randomSample(pendingVideoFiles, 50);
return batch.map((file) => ({ file }));
};
// TODO(HLS): Store this in DB.
const _processedVideoFileIDs: number[] = [];
const savedProcessedVideoFileIDs = async () => {
// TODO(HLS): make async
await wait(0);
return new Set(_processedVideoFileIDs);
};
const markProcessedVideoFileID = async (fileID: number) => {
// TODO(HLS): make async
await wait(0);
_processedVideoFileIDs.push(fileID);
};
/**
* Generate and upload a streamable variant of the given {@link EnteFile}.
*
@@ -467,7 +612,7 @@ const processQueueItem = async ({
log.info(`Generate HLS for ${fileLogID(file)} | done`);
} finally {
await Promise.all([videoStreamDone(electron, playlistToken)]);
await videoStreamDone(electron, playlistToken);
}
};

View File

@@ -0,0 +1,33 @@
import type { Electron, UtilityProcessType } from "ente-base/types/ipc";
/**
* Obtain a port from the Node.js layer that can be used to communicate with the
* native utility process of type {@link type}.
*/
export const createUtilityProcess = (
electron: Electron,
type: UtilityProcessType,
): Promise<MessagePort> => {
// The main process will do its thing, and send back the port it created to
// us by sending an message on the "utilityProcessPort/<type>" channel via
// the postMessage API. This roundabout way is needed because MessagePorts
// cannot be transferred via the usual send/invoke pattern.
const portEvent = `utilityProcessPort/${type}`;
const port = new Promise<MessagePort>((resolve) => {
const l = ({ source, data, ports }: MessageEvent) => {
// The source check verifies that the message is coming from our own
// preload script. The data is the message that was posted.
if (source == window && data == portEvent) {
window.removeEventListener("message", l);
resolve(ports[0]!);
}
};
window.addEventListener("message", l);
});
electron.triggerCreateUtilityProcess(type);
return port;
};

View File

@@ -8,9 +8,9 @@ import { blobCache } from "ente-base/blob-cache";
import { ensureElectron } from "ente-base/electron";
import log from "ente-base/log";
import { masterKeyFromSession } from "ente-base/session";
import type { Electron } from "ente-base/types/ipc";
import { ComlinkWorker } from "ente-base/worker/comlink-worker";
import { type ProcessableUploadItem } from "ente-gallery/services/upload";
import { createUtilityProcess } from "ente-gallery/utils/native-worker";
import type { EnteFile } from "ente-media/file";
import { FileType } from "ente-media/file-type";
import { throttled } from "ente-utils/promise";
@@ -135,7 +135,7 @@ const createComlinkWorker = async () => {
const delegate = { workerDidUpdateStatus, workerDidUnawaitedIndex };
// Obtain a message port from the Electron layer.
const messagePort = await createMLWorker(electron);
const messagePort = await createUtilityProcess(electron, "ml");
const cw = new ComlinkWorker<typeof MLWorker>(
"ML",
@@ -166,33 +166,6 @@ export const terminateMLWorker = async () => {
}
};
/**
* Obtain a port from the Node.js layer that can be used to communicate with the
* ML worker process.
*/
const createMLWorker = (electron: Electron): Promise<MessagePort> => {
// The main process will do its thing, and send back the port it created to
// us by sending an message on the "createMLWorker/port" channel via the
// postMessage API. This roundabout way is needed because MessagePorts
// cannot be transferred via the usual send/invoke pattern.
const port = new Promise<MessagePort>((resolve) => {
const l = ({ source, data, ports }: MessageEvent) => {
// The source check verifies that the message is coming from our own
// preload script. The data is the message that was posted.
if (source == window && data == "createMLWorker/port") {
window.removeEventListener("message", l);
resolve(ports[0]!);
}
};
window.addEventListener("message", l);
});
electron.createMLWorker();
return port;
};
/**
* Return true if the current client supports ML.
*

View File

@@ -1,7 +1,7 @@
import { assertionFailed } from "ente-base/assert";
import log from "ente-base/log";
import type { EnteFile } from "ente-media/file";
import { shuffled } from "ente-utils/array";
import { randomSample } from "ente-utils/array";
import { getLocalFiles } from "../files";
import {
savedCGroups,
@@ -585,32 +585,6 @@ export const _suggestionsAndChoicesForPerson = async (
return { choices, suggestions };
};
/**
* Return a random sample of {@link n} elements from the given {@link items}.
*
* Functionally this is equivalent to `shuffled(items).slice(0, n)`, except it
* tries to be a bit faster for long arrays when we need only a small sample
* from it. In a few tests, this indeed makes a substantial difference.
*/
const randomSample = <T>(items: T[], n: number) => {
if (items.length <= n) return items;
if (n == 0) return [];
if (n > items.length / 3) {
// Avoid using the random sampling without replacement method if a
// significant proportion of the original items are needed, otherwise we
// might run into long retry loop at the tail end (hitting the same
// indexes again an again).
return shuffled(items).slice(0, n);
}
const ix = new Set<number>();
while (ix.size < n) {
ix.add(Math.floor(Math.random() * items.length));
}
return [...ix].map((i) => items[i]!);
};
/**
* A map specifying the changes to make when the user presses the save button on
* the people suggestions dialog.

View File

@@ -236,13 +236,15 @@ export class MLWorker {
// If there is items remaining,
if (items.length > 0) {
// Index them.
const allSuccess = await indexNextBatch(
const indexedCount = await indexNextBatch(
items,
this.electron!,
this.delegate,
);
if (allSuccess) {
// Everything is running smoothly. Reset the idle duration.
if (indexedCount > 0) {
// We made some progress, so there are no complete blockers
// (e.g. network being offline). Reset the idle duration and
// move on to the next batch (if any).
this.idleDuration = idleDurationStart;
// And tick again.
scheduleTick();
@@ -348,11 +350,7 @@ logUnhandledErrorsAndRejectionsInWorker();
/**
* Index the given batch of items.
*
* Returns `false` to indicate that either an error occurred, or that we cannot
* currently process files since we don't have network connectivity.
*
* Which means that when it returns true, all is well and if there are more
* things pending to process, we should chug along at full speed.
* @returns the count of items which were indexed.
*/
const indexNextBatch = async (
items: IndexableItem[],
@@ -364,7 +362,7 @@ const indexNextBatch = async (
// were able to upload just a bit ago but don't have network now.
if (!self.navigator.onLine) {
log.info("Skipping ML indexing since we are not online");
return false;
return 0;
}
// Keep track if any of the items failed.
@@ -411,14 +409,16 @@ const indexNextBatch = async (
// Clear any cached CLIP indexes, since now we might have new ones.
clearCachedCLIPIndexes();
const indexedCount = items.length - failureCount;
log.info(
failureCount > 0
? `Indexed ${items.length - failureCount} files (${failureCount} failed)`
? `Indexed ${indexedCount} files (${failureCount} failed)`
: `Indexed ${items.length} files`,
);
// Return true if nothing failed.
return failureCount == 0;
// Return the count of indexed files.
return indexedCount;
};
/**

View File

@@ -14,6 +14,35 @@ export const shuffled = <T>(xs: T[]): T[] =>
.sort()
.map(([, x]) => x) as T[];
/**
* Return a random sample of {@link n} elements from the given {@link items}.
*
* Functionally this is equivalent to `shuffled(items).slice(0, n)`, except it
* tries to be a bit faster for long arrays when we need only a small sample
* from it. In a few tests, this indeed makes a substantial difference.
*
* If {@link n} is less than the number of {@link items} then a random shuffle
* of the {@link items} is returned.
*/
export const randomSample = <T>(items: T[], n: number) => {
if (items.length <= n) return shuffled(items);
if (n == 0) return [];
if (n > items.length / 3) {
// Avoid using the random sampling without replacement method if a
// significant proportion of the original items are needed, otherwise we
// might run into long retry loop at the tail end (hitting the same
// indexes again an again).
return shuffled(items).slice(0, n);
}
const ix = new Set<number>();
while (ix.size < n) {
ix.add(Math.floor(Math.random() * items.length));
}
return [...ix].map((i) => items[i]!);
};
/**
* Return the first non-empty string from the given list of strings.
*