Merge remote-tracking branch 'origin/main' into mobile-resumable
This commit is contained in:
commit
c29beab8d6
89 changed files with 1337 additions and 1393 deletions
|
@ -113,12 +113,14 @@
|
|||
"copied": "Kopiert",
|
||||
"pleaseTryAgain": "Bitte versuchen Sie es erneut",
|
||||
"existingUser": "Bestehender Benutzer",
|
||||
"newUser": "Neu bei Ente",
|
||||
"delete": "Löschen",
|
||||
"enterYourPasswordHint": "Geben Sie Ihr Passwort ein",
|
||||
"forgotPassword": "Passwort vergessen",
|
||||
"oops": "Hopla",
|
||||
"suggestFeatures": "Features vorschlagen",
|
||||
"faq": "FAQ",
|
||||
"faq_q_1": "Wie sicher ist Auth?",
|
||||
"faq_q_2": "Kann ich auf meine Codes auf dem Desktop zugreifen?",
|
||||
"faq_a_2": "Sie können auf Ihre Codes im Web via auth.ente.io zugreifen.",
|
||||
"faq_q_3": "Wie kann ich Codes löschen?",
|
||||
|
@ -193,6 +195,7 @@
|
|||
"recoveryKeySaveDescription": "Wir speichern diesen Schlüssel nicht. Sichern sie dieses diesen Schlüssel bestehend aus 24 Wörtern an einem sicheren Platz.",
|
||||
"doThisLater": "Auf später verschieben",
|
||||
"saveKey": "Schlüssel speichern",
|
||||
"save": "Speichern",
|
||||
"back": "Zurück",
|
||||
"createAccount": "Account erstellen",
|
||||
"passwordStrength": "Passwortstärke: {passwordStrengthValue}",
|
||||
|
@ -400,6 +403,7 @@
|
|||
"doNotSignOut": "Nicht abmelden",
|
||||
"hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
|
||||
"hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!",
|
||||
"recoveryKeySaved": "Wiederherstellungsschlüssel im Downloads-Ordner gespeichert!",
|
||||
"waitingForBrowserRequest": "Warten auf Browseranfrage...",
|
||||
"waitingForVerification": "Warte auf Bestätigung...",
|
||||
"passkey": "Passkey",
|
||||
|
|
|
@ -190,6 +190,7 @@
|
|||
"recoveryKeySaveDescription": "Non memorizziamo questa chiave, per favore salva questa chiave di 24 parole in un posto sicuro.",
|
||||
"doThisLater": "Fallo più tardi",
|
||||
"saveKey": "Salva chiave",
|
||||
"save": "Salva",
|
||||
"back": "Indietro",
|
||||
"createAccount": "Crea account",
|
||||
"passwordStrength": "Forza password: {passwordStrengthValue}",
|
||||
|
@ -396,5 +397,6 @@
|
|||
"signOutOtherDevices": "Esci dagli altri dispositivi",
|
||||
"doNotSignOut": "Non uscire",
|
||||
"hearUsWhereTitle": "Dove hai sentito parlare di Ente? (opzionale)",
|
||||
"hearUsExplanation": "Non teniamo traccia delle installazioni dell'app. Sarebbe utile se ci dicessi dove ci hai trovato!"
|
||||
"hearUsExplanation": "Non teniamo traccia delle installazioni dell'app. Sarebbe utile se ci dicessi dove ci hai trovato!",
|
||||
"passkey": "Passkey"
|
||||
}
|
|
@ -78,12 +78,14 @@
|
|||
"data": "Dados",
|
||||
"importCodes": "Importar códigos",
|
||||
"importTypePlainText": "Texto simples",
|
||||
"importTypeEnteEncrypted": "Exportação Ente criptografada",
|
||||
"passwordForDecryptingExport": "Senha para descriptografar a exportação",
|
||||
"passwordEmptyError": "O campo senha não pode estar vazio",
|
||||
"importFromApp": "Importar códigos do {appName}",
|
||||
"importGoogleAuthGuide": "Exporte suas contas do Google Authenticator para um QR code usando a opção \"Transferir contas\". Então, usando outro dispositivo, escaneie o QR code.\n\nDica: Você pode usar a câmera do seu notebook para fotografar o QR code.",
|
||||
"importSelectJsonFile": "Selecione o arquivo JSON",
|
||||
"importSelectAppExport": "Selecione o arquivo de exportação do aplicativo {appName}",
|
||||
"importEnteEncGuide": "Selecione o arquivo JSON criptografado exportado do Ente",
|
||||
"importRaivoGuide": "Use a opção \"Exportar OTPs para arquivo Zip\" nas configurações do Raivo.\n\nExtraia o arquivo zip e importe o arquivo JSON.",
|
||||
"importBitwardenGuide": "Use a opção \"Exportar cofre\" nas configurações do Bitwarden e importe o arquivo JSON não criptografado.",
|
||||
"importAegisGuide": "Use a opção \"Exportar cofre\" nas Configurações do Aegis.\n\nSe o seu cofre estiver criptografado, você precisará inserir a senha do cofre para descriptografá-lo.",
|
||||
|
@ -113,18 +115,22 @@
|
|||
"copied": "Copiado",
|
||||
"pleaseTryAgain": "Por favor, tente novamente",
|
||||
"existingUser": "Usuário Existente",
|
||||
"newUser": "Novo no Ente",
|
||||
"delete": "Excluir",
|
||||
"enterYourPasswordHint": "Insira sua senha",
|
||||
"forgotPassword": "Esqueci a senha",
|
||||
"oops": "Oops",
|
||||
"suggestFeatures": "Sugerir funcionalidades",
|
||||
"faq": "Perguntas frequentes",
|
||||
"faq_q_1": "Quão seguro é o Auth?",
|
||||
"faq_a_1": "Todos os códigos que você faz backup via Auth são armazenados criptografados de ponta a ponta. Isso significa que somente você pode acessar seus códigos. Nossos aplicativos são de código aberto e nossa criptografia foi auditada externamente.",
|
||||
"faq_q_2": "Eu posso acessar meus códigos no computador?",
|
||||
"faq_a_2": "Você pode acessar seus códigos na web em auth.ente.io.",
|
||||
"faq_q_3": "Como faço para excluir códigos?",
|
||||
"faq_a_3": "Você pode excluir um código deslizando para a esquerda sobre esse item.",
|
||||
"faq_q_4": "Como posso apoiar este projeto?",
|
||||
"faq_a_4": "Você pode apoiar o desenvolvimento deste projeto assinando nosso aplicativo de Fotos em ente.io.",
|
||||
"faq_q_5": "Como posso ativar o bloqueio facial no Auth",
|
||||
"faq_a_5": "Você pode ativar o bloqueio facial em Configurações → Segurança → Tela de bloqueio.",
|
||||
"somethingWentWrongMessage": "Algo deu errado. Por favor, tente outra vez",
|
||||
"leaveFamily": "Sair da família",
|
||||
|
@ -344,6 +350,7 @@
|
|||
"deleteCodeAuthMessage": "Autenticar para excluir o código",
|
||||
"showQRAuthMessage": "Autenticar para mostrar o QR code",
|
||||
"confirmAccountDeleteTitle": "Confirmar exclusão de conta",
|
||||
"confirmAccountDeleteMessage": "Esta conta está vinculada a outros aplicativos Ente, se você usa algum.\n\nSeus dados enviados, em todos os aplicativos Ente, serão agendados para exclusão, e sua conta será excluída permanentemente.",
|
||||
"androidBiometricHint": "Verificar identidade",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
"incorrectPasswordTitle": "Felaktigt lösenord",
|
||||
"welcomeBack": "Välkommen tillbaka!",
|
||||
"changePassword": "Ändra lösenord",
|
||||
"importCodes": "Importera koder",
|
||||
"cancel": "Avbryt",
|
||||
"yes": "Ja",
|
||||
"no": "Nej",
|
||||
|
|
|
@ -44,8 +44,8 @@
|
|||
"electron-builder-notarize": "^1.5",
|
||||
"eslint": "^8",
|
||||
"prettier": "^3",
|
||||
"prettier-plugin-organize-imports": "^3.2",
|
||||
"prettier-plugin-packagejson": "^2.4",
|
||||
"prettier-plugin-organize-imports": "^3",
|
||||
"prettier-plugin-packagejson": "^2",
|
||||
"shx": "^0.3",
|
||||
"typescript": "^5"
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
|
||||
*/
|
||||
import { nativeImage } from "electron";
|
||||
import { app, BrowserWindow, Menu, Tray } from "electron/main";
|
||||
import { app, BrowserWindow, Menu, protocol, Tray } from "electron/main";
|
||||
import serveNextAt from "next-electron-server";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
|
@ -27,6 +27,7 @@ import { setupAutoUpdater } from "./main/services/app-update";
|
|||
import autoLauncher from "./main/services/autoLauncher";
|
||||
import { initWatcher } from "./main/services/chokidar";
|
||||
import { userPreferences } from "./main/stores/user-preferences";
|
||||
import { registerStreamProtocol } from "./main/stream";
|
||||
import { isDev } from "./main/util";
|
||||
|
||||
/**
|
||||
|
@ -58,6 +59,21 @@ export const allowWindowClose = (): void => {
|
|||
shouldAllowWindowClose = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Log a standard startup banner.
|
||||
*
|
||||
* This helps us identify app starts and other environment details in the logs.
|
||||
*/
|
||||
const logStartupBanner = () => {
|
||||
const version = isDev ? "dev" : app.getVersion();
|
||||
log.info(`Starting ente-photos-desktop ${version}`);
|
||||
|
||||
const platform = process.platform;
|
||||
const osRelease = os.release();
|
||||
const systemVersion = process.getSystemVersion();
|
||||
log.info("Running on", { platform, osRelease, systemVersion });
|
||||
};
|
||||
|
||||
/**
|
||||
* next-electron-server allows up to directly use the output of `next build` in
|
||||
* production mode and `next dev` in development mode, whilst keeping the rest
|
||||
|
@ -74,18 +90,57 @@ export const allowWindowClose = (): void => {
|
|||
const setupRendererServer = () => serveNextAt(rendererURL);
|
||||
|
||||
/**
|
||||
* Log a standard startup banner.
|
||||
* Register privileged schemes.
|
||||
*
|
||||
* This helps us identify app starts and other environment details in the logs.
|
||||
* We have two privileged schemes:
|
||||
*
|
||||
* 1. "ente", used for serving our web app (@see {@link setupRendererServer}).
|
||||
*
|
||||
* 2. "stream", used for streaming IPC (@see {@link registerStreamProtocol}).
|
||||
*
|
||||
* Both of these need some privileges, however, the documentation for Electron's
|
||||
* [registerSchemesAsPrivileged](https://www.electronjs.org/docs/latest/api/protocol)
|
||||
* says:
|
||||
*
|
||||
* > This method ... can be called only once.
|
||||
*
|
||||
* The library we use for the "ente" scheme, next-electron-server, already calls
|
||||
* it once when we invoke {@link setupRendererServer}.
|
||||
*
|
||||
* In practice calling it multiple times just causes the values to be
|
||||
* overwritten, and the last call wins. So we don't need to modify
|
||||
* next-electron-server to prevent it from calling registerSchemesAsPrivileged.
|
||||
* Instead, we (a) repeat what next-electron-server had done here, and (b)
|
||||
* ensure that we're called after {@link setupRendererServer}.
|
||||
*/
|
||||
const logStartupBanner = () => {
|
||||
const version = isDev ? "dev" : app.getVersion();
|
||||
log.info(`Starting ente-photos-desktop ${version}`);
|
||||
const registerPrivilegedSchemes = () => {
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
// Taken verbatim from next-electron-server's code (index.js)
|
||||
scheme: "ente",
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
allowServiceWorkers: true,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scheme: "stream",
|
||||
privileges: {
|
||||
// TODO(MR): Remove the commented bits if we don't end up
|
||||
// needing them by the time the IPC refactoring is done.
|
||||
|
||||
const platform = process.platform;
|
||||
const osRelease = os.release();
|
||||
const systemVersion = process.getSystemVersion();
|
||||
log.info("Running on", { platform, osRelease, systemVersion });
|
||||
// Prevent the insecure origin issues when fetching this
|
||||
// secure: true,
|
||||
// Allow the web fetch API in the renderer to use this scheme.
|
||||
supportFetchAPI: true,
|
||||
// Allow it to be used with video tags.
|
||||
// stream: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -251,8 +306,10 @@ const main = () => {
|
|||
let mainWindow: BrowserWindow | undefined;
|
||||
|
||||
initLogging();
|
||||
setupRendererServer();
|
||||
logStartupBanner();
|
||||
// The order of the next two calls is important
|
||||
setupRendererServer();
|
||||
registerPrivilegedSchemes();
|
||||
increaseDiskCache();
|
||||
|
||||
app.on("second-instance", () => {
|
||||
|
@ -269,11 +326,11 @@ const main = () => {
|
|||
// Note that some Electron APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
mainWindow = await createMainWindow();
|
||||
const watcher = initWatcher(mainWindow);
|
||||
setupTrayItem(mainWindow);
|
||||
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
|
||||
setupTrayItem(mainWindow);
|
||||
attachIPCHandlers();
|
||||
attachFSWatchIPCHandlers(watcher);
|
||||
attachFSWatchIPCHandlers(initWatcher(mainWindow));
|
||||
registerStreamProtocol();
|
||||
if (!isDev) setupAutoUpdater(mainWindow);
|
||||
handleDownloads(mainWindow);
|
||||
handleExternalLinks(mainWindow);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
/**
|
||||
* @file file system related functions exposed over the context bridge.
|
||||
*/
|
||||
import { createWriteStream, existsSync } from "node:fs";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
export const fsExists = (path: string) => existsSync(path);
|
||||
|
||||
|
@ -17,78 +16,13 @@ export const fsRmdir = (path: string) => fs.rmdir(path);
|
|||
|
||||
export const fsRm = (path: string) => fs.rm(path);
|
||||
|
||||
/**
|
||||
* Write a (web) ReadableStream to a file at the given {@link filePath}.
|
||||
*
|
||||
* The returned promise resolves when the write completes.
|
||||
*
|
||||
* @param filePath The local filesystem path where the file should be written.
|
||||
* @param readableStream A [web
|
||||
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
*/
|
||||
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
|
||||
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
|
||||
export const fsReadTextFile = async (filePath: string) =>
|
||||
fs.readFile(filePath, "utf-8");
|
||||
|
||||
/**
|
||||
* Convert a Web ReadableStream into a Node.js ReadableStream
|
||||
*
|
||||
* This can be used to, for example, write a ReadableStream obtained via
|
||||
* `net.fetch` into a file using the Node.js `fs` APIs
|
||||
*/
|
||||
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
|
||||
const reader = readableStream.getReader();
|
||||
const rs = new Readable();
|
||||
|
||||
rs._read = async () => {
|
||||
try {
|
||||
const result = await reader.read();
|
||||
|
||||
if (!result.done) {
|
||||
rs.push(Buffer.from(result.value));
|
||||
} else {
|
||||
rs.push(null);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
rs.emit("error", e);
|
||||
}
|
||||
};
|
||||
|
||||
return rs;
|
||||
};
|
||||
|
||||
const writeNodeStream = async (
|
||||
filePath: string,
|
||||
fileStream: NodeJS.ReadableStream,
|
||||
) => {
|
||||
const writeable = createWriteStream(filePath);
|
||||
|
||||
fileStream.on("error", (error) => {
|
||||
writeable.destroy(error); // Close the writable stream with an error
|
||||
});
|
||||
|
||||
fileStream.pipe(writeable);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeable.on("finish", resolve);
|
||||
writeable.on("error", async (e: unknown) => {
|
||||
if (existsSync(filePath)) {
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* TODO: Audit below this */
|
||||
|
||||
export const saveStreamToDisk = writeStream;
|
||||
|
||||
export const saveFileToDisk = (path: string, contents: string) =>
|
||||
export const fsWriteFile = (path: string, contents: string) =>
|
||||
fs.writeFile(path, contents);
|
||||
|
||||
export const readTextFile = async (filePath: string) =>
|
||||
fs.readFile(filePath, "utf-8");
|
||||
/* TODO: Audit below this */
|
||||
|
||||
export const isFolder = async (dirPath: string) => {
|
||||
if (!existsSync(dirPath)) return false;
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
import type { FSWatcher } from "chokidar";
|
||||
import { ipcMain } from "electron/main";
|
||||
import type { ElectronFile, FILE_PATH_TYPE, WatchMapping } from "../types/ipc";
|
||||
import type { ElectronFile, FILE_PATH_TYPE, FolderWatch } from "../types/ipc";
|
||||
import {
|
||||
selectDirectory,
|
||||
showUploadDirsDialog,
|
||||
|
@ -20,13 +20,12 @@ import {
|
|||
import {
|
||||
fsExists,
|
||||
fsMkdirIfNeeded,
|
||||
fsReadTextFile,
|
||||
fsRename,
|
||||
fsRm,
|
||||
fsRmdir,
|
||||
fsWriteFile,
|
||||
isFolder,
|
||||
readTextFile,
|
||||
saveFileToDisk,
|
||||
saveStreamToDisk,
|
||||
} from "./fs";
|
||||
import { logToDisk } from "./log";
|
||||
import {
|
||||
|
@ -113,6 +112,26 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version));
|
||||
|
||||
// - FS
|
||||
|
||||
ipcMain.handle("fsExists", (_, path) => fsExists(path));
|
||||
|
||||
ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) =>
|
||||
fsRename(oldPath, newPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath));
|
||||
|
||||
ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path));
|
||||
|
||||
ipcMain.handle("fsRm", (_, path: string) => fsRm(path));
|
||||
|
||||
ipcMain.handle("fsReadTextFile", (_, path: string) => fsReadTextFile(path));
|
||||
|
||||
ipcMain.handle("fsWriteFile", (_, path: string, contents: string) =>
|
||||
fsWriteFile(path, contents),
|
||||
);
|
||||
|
||||
// - Conversion
|
||||
|
||||
ipcMain.handle("convertToJPEG", (_, fileData, filename) =>
|
||||
|
@ -164,34 +183,8 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
ipcMain.handle("showUploadZipDialog", () => showUploadZipDialog());
|
||||
|
||||
// - FS
|
||||
|
||||
ipcMain.handle("fsExists", (_, path) => fsExists(path));
|
||||
|
||||
ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) =>
|
||||
fsRename(oldPath, newPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath));
|
||||
|
||||
ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path));
|
||||
|
||||
ipcMain.handle("fsRm", (_, path: string) => fsRm(path));
|
||||
|
||||
// - FS Legacy
|
||||
|
||||
ipcMain.handle(
|
||||
"saveStreamToDisk",
|
||||
(_, path: string, fileStream: ReadableStream) =>
|
||||
saveStreamToDisk(path, fileStream),
|
||||
);
|
||||
|
||||
ipcMain.handle("saveFileToDisk", (_, path: string, contents: string) =>
|
||||
saveFileToDisk(path, contents),
|
||||
);
|
||||
|
||||
ipcMain.handle("readTextFile", (_, path: string) => readTextFile(path));
|
||||
|
||||
ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath));
|
||||
|
||||
// - Upload
|
||||
|
@ -249,13 +242,13 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
|
|||
|
||||
ipcMain.handle(
|
||||
"updateWatchMappingSyncedFiles",
|
||||
(_, folderPath: string, files: WatchMapping["syncedFiles"]) =>
|
||||
(_, folderPath: string, files: FolderWatch["syncedFiles"]) =>
|
||||
updateWatchMappingSyncedFiles(folderPath, files),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"updateWatchMappingIgnoredFiles",
|
||||
(_, folderPath: string, files: WatchMapping["ignoredFiles"]) =>
|
||||
(_, folderPath: string, files: FolderWatch["ignoredFiles"]) =>
|
||||
updateWatchMappingIgnoredFiles(folderPath, files),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,8 +2,8 @@ import pathToFfmpeg from "ffmpeg-static";
|
|||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { ElectronFile } from "../../types/ipc";
|
||||
import { writeStream } from "../fs";
|
||||
import log from "../log";
|
||||
import { writeStream } from "../stream";
|
||||
import { generateTempFilePath, getTempDirPath } from "../temp";
|
||||
import { execAsync } from "../util";
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ import { existsSync } from "fs";
|
|||
import fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
import { CustomErrors, ElectronFile } from "../../types/ipc";
|
||||
import { writeStream } from "../fs";
|
||||
import log from "../log";
|
||||
import { isPlatform } from "../platform";
|
||||
import { writeStream } from "../stream";
|
||||
import { generateTempFilePath } from "../temp";
|
||||
import { execAsync, isDev } from "../util";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
|
|
|
@ -11,8 +11,8 @@ import fs from "node:fs/promises";
|
|||
import * as ort from "onnxruntime-node";
|
||||
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
|
||||
import { CustomErrors } from "../../types/ipc";
|
||||
import { writeStream } from "../fs";
|
||||
import log from "../log";
|
||||
import { writeStream } from "../stream";
|
||||
import { generateTempFilePath } from "../temp";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
import {
|
||||
|
|
|
@ -15,8 +15,8 @@ import { existsSync } from "fs";
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as ort from "onnxruntime-node";
|
||||
import { writeStream } from "../fs";
|
||||
import log from "../log";
|
||||
import { writeStream } from "../stream";
|
||||
|
||||
/**
|
||||
* Download the model named {@link modelName} if we don't already have it.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { FSWatcher } from "chokidar";
|
||||
import ElectronLog from "electron-log";
|
||||
import { WatchMapping, WatchStoreType } from "../../types/ipc";
|
||||
import { FolderWatch, WatchStoreType } from "../../types/ipc";
|
||||
import { watchStore } from "../stores/watch.store";
|
||||
|
||||
export const addWatchMapping = async (
|
||||
|
@ -28,7 +28,7 @@ export const addWatchMapping = async (
|
|||
setWatchMappings(watchMappings);
|
||||
};
|
||||
|
||||
function isMappingPresent(watchMappings: WatchMapping[], folderPath: string) {
|
||||
function isMappingPresent(watchMappings: FolderWatch[], folderPath: string) {
|
||||
const watchMapping = watchMappings?.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
@ -59,7 +59,7 @@ export const removeWatchMapping = async (
|
|||
|
||||
export function updateWatchMappingSyncedFiles(
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
files: FolderWatch["syncedFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
|
@ -76,7 +76,7 @@ export function updateWatchMappingSyncedFiles(
|
|||
|
||||
export function updateWatchMappingIgnoredFiles(
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
files: FolderWatch["ignoredFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
|
|
116
desktop/src/main/stream.ts
Normal file
116
desktop/src/main/stream.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* @file stream data to-from renderer using a custom protocol handler.
|
||||
*/
|
||||
import { protocol } from "electron/main";
|
||||
import { createWriteStream, existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Readable } from "node:stream";
|
||||
import log from "./log";
|
||||
|
||||
/**
|
||||
* Register a protocol handler that we use for streaming large files between the
|
||||
* main process (node) and the renderer process (browser) layer.
|
||||
*
|
||||
* [Note: IPC streams]
|
||||
*
|
||||
* When running without node integration, there is no direct way to pass streams
|
||||
* across IPC. And passing the entire contents of the file is not feasible for
|
||||
* large video files because of the memory pressure the copying would entail.
|
||||
*
|
||||
* As an alternative, we register a custom protocol handler that can provided a
|
||||
* bi-directional stream. The renderer can stream data to the node side by
|
||||
* streaming the request. The node side can stream to the renderer side by
|
||||
* streaming the response.
|
||||
*
|
||||
* See also: [Note: Transferring large amount of data over IPC]
|
||||
*
|
||||
* Depends on {@link registerPrivilegedSchemes}.
|
||||
*/
|
||||
export const registerStreamProtocol = () => {
|
||||
protocol.handle("stream", async (request: Request) => {
|
||||
const url = request.url;
|
||||
const { host, pathname } = new URL(url);
|
||||
// Convert e.g. "%20" to spaces.
|
||||
const path = decodeURIComponent(pathname);
|
||||
switch (host) {
|
||||
/* stream://write/path/to/file */
|
||||
/* host-pathname----- */
|
||||
case "write":
|
||||
try {
|
||||
await writeStream(path, request.body);
|
||||
return new Response("", { status: 200 });
|
||||
} catch (e) {
|
||||
log.error(`Failed to write stream for ${url}`, e);
|
||||
return new Response(
|
||||
`Failed to write stream: ${e.message}`,
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
default:
|
||||
return new Response("", { status: 404 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Write a (web) ReadableStream to a file at the given {@link filePath}.
|
||||
*
|
||||
* The returned promise resolves when the write completes.
|
||||
*
|
||||
* @param filePath The local filesystem path where the file should be written.
|
||||
* @param readableStream A [web
|
||||
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
*/
|
||||
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
|
||||
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
|
||||
|
||||
/**
|
||||
* Convert a Web ReadableStream into a Node.js ReadableStream
|
||||
*
|
||||
* This can be used to, for example, write a ReadableStream obtained via
|
||||
* `net.fetch` into a file using the Node.js `fs` APIs
|
||||
*/
|
||||
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
|
||||
const reader = readableStream.getReader();
|
||||
const rs = new Readable();
|
||||
|
||||
rs._read = async () => {
|
||||
try {
|
||||
const result = await reader.read();
|
||||
|
||||
if (!result.done) {
|
||||
rs.push(Buffer.from(result.value));
|
||||
} else {
|
||||
rs.push(null);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
rs.emit("error", e);
|
||||
}
|
||||
};
|
||||
|
||||
return rs;
|
||||
};
|
||||
|
||||
const writeNodeStream = async (
|
||||
filePath: string,
|
||||
fileStream: NodeJS.ReadableStream,
|
||||
) => {
|
||||
const writeable = createWriteStream(filePath);
|
||||
|
||||
fileStream.on("error", (error) => {
|
||||
writeable.destroy(error); // Close the writable stream with an error
|
||||
});
|
||||
|
||||
fileStream.pipe(writeable);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeable.on("finish", resolve);
|
||||
writeable.on("error", async (e: unknown) => {
|
||||
if (existsSync(filePath)) {
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -45,7 +45,7 @@ import type {
|
|||
AppUpdateInfo,
|
||||
ElectronFile,
|
||||
FILE_PATH_TYPE,
|
||||
WatchMapping,
|
||||
FolderWatch,
|
||||
} from "./types/ipc";
|
||||
|
||||
// - General
|
||||
|
@ -96,6 +96,8 @@ const skipAppUpdate = (version: string) => {
|
|||
ipcRenderer.send("skipAppUpdate", version);
|
||||
};
|
||||
|
||||
// - FS
|
||||
|
||||
const fsExists = (path: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("fsExists", path);
|
||||
|
||||
|
@ -110,6 +112,12 @@ const fsRmdir = (path: string): Promise<void> =>
|
|||
|
||||
const fsRm = (path: string): Promise<void> => ipcRenderer.invoke("fsRm", path);
|
||||
|
||||
const fsReadTextFile = (path: string): Promise<string> =>
|
||||
ipcRenderer.invoke("fsReadTextFile", path);
|
||||
|
||||
const fsWriteFile = (path: string, contents: string): Promise<void> =>
|
||||
ipcRenderer.invoke("fsWriteFile", path, contents);
|
||||
|
||||
// - AUDIT below this
|
||||
|
||||
// - Conversion
|
||||
|
@ -212,34 +220,23 @@ const addWatchMapping = (
|
|||
const removeWatchMapping = (folderPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke("removeWatchMapping", folderPath);
|
||||
|
||||
const getWatchMappings = (): Promise<WatchMapping[]> =>
|
||||
const getWatchMappings = (): Promise<FolderWatch[]> =>
|
||||
ipcRenderer.invoke("getWatchMappings");
|
||||
|
||||
const updateWatchMappingSyncedFiles = (
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
files: FolderWatch["syncedFiles"],
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files);
|
||||
|
||||
const updateWatchMappingIgnoredFiles = (
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
files: FolderWatch["ignoredFiles"],
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files);
|
||||
|
||||
// - FS Legacy
|
||||
|
||||
const saveStreamToDisk = (
|
||||
path: string,
|
||||
fileStream: ReadableStream,
|
||||
): Promise<void> => ipcRenderer.invoke("saveStreamToDisk", path, fileStream);
|
||||
|
||||
const saveFileToDisk = (path: string, contents: string): Promise<void> =>
|
||||
ipcRenderer.invoke("saveFileToDisk", path, contents);
|
||||
|
||||
const readTextFile = (path: string): Promise<string> =>
|
||||
ipcRenderer.invoke("readTextFile", path);
|
||||
|
||||
const isFolder = (dirPath: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("isFolder", dirPath);
|
||||
|
||||
|
@ -298,7 +295,8 @@ const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
|
|||
// https://www.electronjs.org/docs/latest/api/context-bridge#methods
|
||||
//
|
||||
// The copy itself is relatively fast, but the problem with transfering large
|
||||
// amounts of data is potentially running out of memory during the copy.
|
||||
// amounts of data is potentially running out of memory during the copy. For an
|
||||
// alternative, see [Note: IPC streams].
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
// - General
|
||||
appVersion,
|
||||
|
@ -316,6 +314,17 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
updateOnNextRestart,
|
||||
skipAppUpdate,
|
||||
|
||||
// - FS
|
||||
fs: {
|
||||
exists: fsExists,
|
||||
rename: fsRename,
|
||||
mkdirIfNeeded: fsMkdirIfNeeded,
|
||||
rmdir: fsRmdir,
|
||||
rm: fsRm,
|
||||
readTextFile: fsReadTextFile,
|
||||
writeFile: fsWriteFile,
|
||||
},
|
||||
|
||||
// - Conversion
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
|
@ -341,20 +350,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
updateWatchMappingSyncedFiles,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
|
||||
// - FS
|
||||
fs: {
|
||||
exists: fsExists,
|
||||
rename: fsRename,
|
||||
mkdirIfNeeded: fsMkdirIfNeeded,
|
||||
rmdir: fsRmdir,
|
||||
rm: fsRm,
|
||||
},
|
||||
|
||||
// - FS legacy
|
||||
// TODO: Move these into fs + document + rename if needed
|
||||
saveStreamToDisk,
|
||||
saveFileToDisk,
|
||||
readTextFile,
|
||||
isFolder,
|
||||
|
||||
// - Upload
|
||||
|
|
|
@ -5,6 +5,20 @@
|
|||
* See [Note: types.ts <-> preload.ts <-> ipc.ts]
|
||||
*/
|
||||
|
||||
export interface FolderWatch {
|
||||
rootFolderName: string;
|
||||
uploadStrategy: number;
|
||||
folderPath: string;
|
||||
syncedFiles: FolderWatchSyncedFile[];
|
||||
ignoredFiles: string[];
|
||||
}
|
||||
|
||||
export interface FolderWatchSyncedFile {
|
||||
path: string;
|
||||
uploadedFileID: number;
|
||||
collectionID: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors that have special semantics on the web side.
|
||||
*
|
||||
|
@ -52,22 +66,8 @@ export interface ElectronFile {
|
|||
arrayBuffer: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
interface WatchMappingSyncedFile {
|
||||
path: string;
|
||||
uploadedFileID: number;
|
||||
collectionID: number;
|
||||
}
|
||||
|
||||
export interface WatchMapping {
|
||||
rootFolderName: string;
|
||||
uploadStrategy: number;
|
||||
folderPath: string;
|
||||
syncedFiles: WatchMappingSyncedFile[];
|
||||
ignoredFiles: string[];
|
||||
}
|
||||
|
||||
export interface WatchStoreType {
|
||||
mappings: WatchMapping[];
|
||||
mappings: FolderWatch[];
|
||||
}
|
||||
|
||||
export enum FILE_PATH_TYPE {
|
||||
|
|
|
@ -125,7 +125,7 @@
|
|||
dependencies:
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1":
|
||||
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1":
|
||||
version "4.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
|
||||
integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
|
||||
|
@ -285,7 +285,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
|
||||
integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==
|
||||
|
||||
"@types/json-schema@^7.0.12":
|
||||
"@types/json-schema@^7.0.15":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
@ -303,9 +303,9 @@
|
|||
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
|
||||
|
||||
"@types/node@*", "@types/node@^20.9.0":
|
||||
version "20.11.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f"
|
||||
integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==
|
||||
version "20.12.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384"
|
||||
integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
|
@ -334,7 +334,7 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/semver@^7.5.0":
|
||||
"@types/semver@^7.5.8":
|
||||
version "7.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
|
||||
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
|
||||
|
@ -352,90 +352,90 @@
|
|||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^7":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz#de61c3083842fc6ac889d2fc83c9a96b55ab8328"
|
||||
integrity sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz#1f5df5cda490a0bcb6fbdd3382e19f1241024242"
|
||||
integrity sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.5.1"
|
||||
"@typescript-eslint/scope-manager" "7.4.0"
|
||||
"@typescript-eslint/type-utils" "7.4.0"
|
||||
"@typescript-eslint/utils" "7.4.0"
|
||||
"@typescript-eslint/visitor-keys" "7.4.0"
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "7.6.0"
|
||||
"@typescript-eslint/type-utils" "7.6.0"
|
||||
"@typescript-eslint/utils" "7.6.0"
|
||||
"@typescript-eslint/visitor-keys" "7.6.0"
|
||||
debug "^4.3.4"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.2.4"
|
||||
ignore "^5.3.1"
|
||||
natural-compare "^1.4.0"
|
||||
semver "^7.5.4"
|
||||
ts-api-utils "^1.0.1"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/parser@^7":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.4.0.tgz#540f4321de1e52b886c0fa68628af1459954c1f1"
|
||||
integrity sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.6.0.tgz#0aca5de3045d68b36e88903d15addaf13d040a95"
|
||||
integrity sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "7.4.0"
|
||||
"@typescript-eslint/types" "7.4.0"
|
||||
"@typescript-eslint/typescript-estree" "7.4.0"
|
||||
"@typescript-eslint/visitor-keys" "7.4.0"
|
||||
"@typescript-eslint/scope-manager" "7.6.0"
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
"@typescript-eslint/typescript-estree" "7.6.0"
|
||||
"@typescript-eslint/visitor-keys" "7.6.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@7.4.0":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz#acfc69261f10ece7bf7ece1734f1713392c3655f"
|
||||
integrity sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==
|
||||
"@typescript-eslint/scope-manager@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz#1e9972f654210bd7500b31feadb61a233f5b5e9d"
|
||||
integrity sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "7.4.0"
|
||||
"@typescript-eslint/visitor-keys" "7.4.0"
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
"@typescript-eslint/visitor-keys" "7.6.0"
|
||||
|
||||
"@typescript-eslint/type-utils@7.4.0":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz#cfcaab21bcca441c57da5d3a1153555e39028cbd"
|
||||
integrity sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==
|
||||
"@typescript-eslint/type-utils@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz#644f75075f379827d25fe0713e252ccd4e4a428c"
|
||||
integrity sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "7.4.0"
|
||||
"@typescript-eslint/utils" "7.4.0"
|
||||
"@typescript-eslint/typescript-estree" "7.6.0"
|
||||
"@typescript-eslint/utils" "7.6.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^1.0.1"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/types@7.4.0":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.4.0.tgz#ee9dafa75c99eaee49de6dcc9348b45d354419b6"
|
||||
integrity sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==
|
||||
"@typescript-eslint/types@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.6.0.tgz#53dba7c30c87e5f10a731054266dd905f1fbae38"
|
||||
integrity sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@7.4.0":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz#12dbcb4624d952f72c10a9f4431284fca24624f4"
|
||||
integrity sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==
|
||||
"@typescript-eslint/typescript-estree@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz#112a3775563799fd3f011890ac8322f80830ac17"
|
||||
integrity sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "7.4.0"
|
||||
"@typescript-eslint/visitor-keys" "7.4.0"
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
"@typescript-eslint/visitor-keys" "7.6.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
minimatch "9.0.3"
|
||||
semver "^7.5.4"
|
||||
ts-api-utils "^1.0.1"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/utils@7.4.0":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.4.0.tgz#d889a0630cab88bddedaf7c845c64a00576257bd"
|
||||
integrity sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==
|
||||
"@typescript-eslint/utils@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.6.0.tgz#e400d782280b6f724c8a1204269d984c79202282"
|
||||
integrity sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@types/json-schema" "^7.0.12"
|
||||
"@types/semver" "^7.5.0"
|
||||
"@typescript-eslint/scope-manager" "7.4.0"
|
||||
"@typescript-eslint/types" "7.4.0"
|
||||
"@typescript-eslint/typescript-estree" "7.4.0"
|
||||
semver "^7.5.4"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
"@types/semver" "^7.5.8"
|
||||
"@typescript-eslint/scope-manager" "7.6.0"
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
"@typescript-eslint/typescript-estree" "7.6.0"
|
||||
semver "^7.6.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@7.4.0":
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz#0c8ff2c1f8a6fe8d7d1a57ebbd4a638e86a60a94"
|
||||
integrity sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==
|
||||
"@typescript-eslint/visitor-keys@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz#d1ce13145844379021e1f9bd102c1d78946f4e76"
|
||||
integrity sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "7.4.0"
|
||||
eslint-visitor-keys "^3.4.1"
|
||||
"@typescript-eslint/types" "7.6.0"
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
version "1.2.0"
|
||||
|
@ -1140,9 +1140,9 @@ ejs@^3.1.8:
|
|||
jake "^10.8.5"
|
||||
|
||||
electron-builder-notarize@^1.5:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.5.1.tgz#e00b868a67ef20a77f00017606626f24fdbdc445"
|
||||
integrity sha512-xS7s9gE+1AcJIuJ4DU/LqCrmRypE1zOR/6b66egKzgP/UVh9YSa7rINos34gF/KcueNDQU39HcXcCEKiEI5wPQ==
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.5.2.tgz#540185b57a336fc6eec01bfe092a3b4764459255"
|
||||
integrity sha512-vo6RGgIFYxMk2yp59N4NsvmAYfB7ncYi6gV9Fcq2TVKxEn2tPXrSjIKB2e/pu+5iXIY6BHNZNXa75F3DHgOOLA==
|
||||
dependencies:
|
||||
dotenv "^8.2.0"
|
||||
electron-notarize "^1.1.1"
|
||||
|
@ -1215,9 +1215,9 @@ electron-updater@^6.1:
|
|||
tiny-typed-emitter "^2.1.0"
|
||||
|
||||
electron@^29:
|
||||
version "29.1.5"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-29.1.5.tgz#b745b4d201c1ac9f84d6aa034126288dde34d5a1"
|
||||
integrity sha512-1uWGRw/ffA62lcrklxGUgVxVtOHojsg/nwsYr+/F9cVjipZJn8iPv/ABGIIexhmUqWcho8BqfTJ4osCBa29gBg==
|
||||
version "29.3.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-29.3.0.tgz#8e65cb08e9c0952c66d3196e1b5c811c43b8c5b0"
|
||||
integrity sha512-ZxFKm0/v48GSoBuO3DdnMlCYXefEUKUHLMsKxyXY4nZGgzbBKpF/X8haZa2paNj23CLfsCKBOtfc2vsEQiOOsA==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^20.9.0"
|
||||
|
@ -1835,7 +1835,7 @@ ieee754@^1.1.13:
|
|||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
ignore@^5.2.0, ignore@^5.2.4:
|
||||
ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
|
||||
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
|
||||
|
@ -2190,13 +2190,6 @@ mimic-response@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
|
||||
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
||||
|
||||
minimatch@9.0.3, minimatch@^9.0.1:
|
||||
version "9.0.3"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
|
||||
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
|
@ -2211,6 +2204,20 @@ minimatch@^5.0.1, minimatch@^5.1.1:
|
|||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.1:
|
||||
version "9.0.3"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
|
||||
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.4:
|
||||
version "9.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"
|
||||
integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.3, minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
|
@ -2482,17 +2489,17 @@ prelude-ls@^1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier-plugin-organize-imports@^3.2:
|
||||
prettier-plugin-organize-imports@^3:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e"
|
||||
integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==
|
||||
|
||||
prettier-plugin-packagejson@^2.4:
|
||||
version "2.4.12"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.4.12.tgz#eeb917dad83ae42d0caccc9f26d3728b5c4f2434"
|
||||
integrity sha512-hifuuOgw5rHHTdouw9VrhT8+Nd7UwxtL1qco8dUfd4XUFQL6ia3xyjSxhPQTsGnSYFraTWy5Omb+MZm/OWDTpQ==
|
||||
prettier-plugin-packagejson@^2:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.0.tgz#23d2cb8b1f7840702d35e3a5078e564ea0bc63e0"
|
||||
integrity sha512-6XkH3rpin5QEQodBSVNg+rBo4r91g/1mCaRwS1YGdQJZ6jwqrg2UchBsIG9tpS1yK1kNBvOt84OILsX8uHzBGg==
|
||||
dependencies:
|
||||
sort-package-json "2.8.0"
|
||||
sort-package-json "2.10.0"
|
||||
synckit "0.9.0"
|
||||
|
||||
prettier@^3:
|
||||
|
@ -2711,7 +2718,7 @@ semver@^6.2.0:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4:
|
||||
semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0:
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
|
||||
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
|
||||
|
@ -2800,10 +2807,10 @@ sort-object-keys@^1.1.3:
|
|||
resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45"
|
||||
integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==
|
||||
|
||||
sort-package-json@2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.8.0.tgz#6a46439ad0fef77f091e678e103f03ecbea575c8"
|
||||
integrity sha512-PxeNg93bTJWmDGnu0HADDucoxfFiKkIr73Kv85EBThlI1YQPdc0XovBgg2llD0iABZbu2SlKo8ntGmOP9wOj/g==
|
||||
sort-package-json@2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.10.0.tgz#6be07424bf3b7db9fbb1bdd69e7945f301026d8a"
|
||||
integrity sha512-MYecfvObMwJjjJskhxYfuOADkXp1ZMMnCFC8yhp+9HDsk7HhR336hd7eiBs96lTXfiqmUNI+WQCeCMRBhl251g==
|
||||
dependencies:
|
||||
detect-indent "^7.0.1"
|
||||
detect-newline "^4.0.0"
|
||||
|
@ -2811,6 +2818,7 @@ sort-package-json@2.8.0:
|
|||
git-hooks-list "^3.0.0"
|
||||
globby "^13.1.2"
|
||||
is-plain-obj "^4.1.0"
|
||||
semver "^7.6.0"
|
||||
sort-object-keys "^1.1.3"
|
||||
|
||||
source-map-support@^0.5.19:
|
||||
|
@ -3018,7 +3026,7 @@ truncate-utf8-bytes@^1.0.0:
|
|||
dependencies:
|
||||
utf8-byte-length "^1.0.1"
|
||||
|
||||
ts-api-utils@^1.0.1:
|
||||
ts-api-utils@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
|
||||
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
|
||||
|
|
|
@ -22,6 +22,7 @@ linter:
|
|||
- use_key_in_widget_constructors
|
||||
- cancel_subscriptions
|
||||
|
||||
|
||||
- avoid_empty_else
|
||||
- exhaustive_cases
|
||||
|
||||
|
@ -59,6 +60,7 @@ analyzer:
|
|||
prefer_final_locals: warning
|
||||
unnecessary_const: error
|
||||
cancel_subscriptions: error
|
||||
unrelated_type_equality_checks: error
|
||||
|
||||
|
||||
unawaited_futures: warning # convert to warning after fixing existing issues
|
||||
|
|
|
@ -3,12 +3,9 @@ PODS:
|
|||
- Flutter
|
||||
- battery_info (0.0.1):
|
||||
- Flutter
|
||||
- bonsoir_darwin (3.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
|
@ -171,7 +168,6 @@ PODS:
|
|||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- ReachabilitySwift (5.2.1)
|
||||
- receive_sharing_intent (1.6.8):
|
||||
- Flutter
|
||||
- screen_brightness_ios (0.1.0):
|
||||
|
@ -231,8 +227,7 @@ PODS:
|
|||
DEPENDENCIES:
|
||||
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
|
||||
- battery_info (from `.symlinks/plugins/battery_info/ios`)
|
||||
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
|
@ -296,7 +291,6 @@ SPEC REPOS:
|
|||
- onnxruntime-objc
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- ReachabilitySwift
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- Sentry
|
||||
|
@ -309,10 +303,8 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/background_fetch/ios"
|
||||
battery_info:
|
||||
:path: ".symlinks/plugins/battery_info/ios"
|
||||
bonsoir_darwin:
|
||||
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_saver:
|
||||
|
@ -409,8 +401,7 @@ EXTERNAL SOURCES:
|
|||
SPEC CHECKSUMS:
|
||||
background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a
|
||||
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
|
||||
bonsoir_darwin: 127bdc632fdc154ae2f277a4d5c86a6212bc75be
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: 797fd7297b7e1be954432743a0b3f90038e45a71
|
||||
|
@ -458,7 +449,6 @@ SPEC CHECKSUMS:
|
|||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66
|
||||
receive_sharing_intent: 6837b01768e567fe8562182397bf43d63d8c6437
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb
|
||||
|
|
|
@ -285,7 +285,6 @@
|
|||
"${BUILT_PRODUCTS_DIR}/Mantle/Mantle.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
|
||||
|
@ -293,7 +292,6 @@
|
|||
"${BUILT_PRODUCTS_DIR}/Toast/Toast.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/bonsoir_darwin/bonsoir_darwin.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
|
||||
|
@ -369,7 +367,6 @@
|
|||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mantle.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
|
||||
|
@ -377,7 +374,6 @@
|
|||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bonsoir_darwin.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
|
||||
|
|
|
@ -13,18 +13,13 @@ import 'package:media_extension/media_extension_action_types.dart';
|
|||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/collection/collection_items.dart";
|
||||
import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/favorites_service.dart";
|
||||
import "package:photos/services/home_widget_service.dart";
|
||||
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/ui/tabs/home_widget.dart';
|
||||
import "package:photos/ui/viewer/actions/file_viewer.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import "package:photos/utils/intent_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class EnteApp extends StatefulWidget {
|
||||
final Future<void> Function(String) runBackgroundTask;
|
||||
|
@ -66,39 +61,14 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
|||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_checkForWidgetLaunch();
|
||||
hw.HomeWidget.widgetClicked.listen(_launchedFromWidget);
|
||||
}
|
||||
|
||||
void _checkForWidgetLaunch() {
|
||||
hw.HomeWidget.initiallyLaunchedFromHomeWidget().then(_launchedFromWidget);
|
||||
}
|
||||
|
||||
Future<void> _launchedFromWidget(Uri? uri) async {
|
||||
if (uri == null) return;
|
||||
final collectionID =
|
||||
await FavoritesService.instance.getFavoriteCollectionID();
|
||||
if (collectionID == null) {
|
||||
return;
|
||||
}
|
||||
final collection = CollectionsService.instance.getCollectionByID(
|
||||
collectionID,
|
||||
hw.HomeWidget.initiallyLaunchedFromHomeWidget().then(
|
||||
(uri) => HomeWidgetService.instance.onLaunchFromWidget(uri, context),
|
||||
);
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
unawaited(HomeWidgetService.instance.initHomeWidget());
|
||||
|
||||
final thumbnail = await CollectionsService.instance.getCover(collection);
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
CollectionPage(
|
||||
CollectionWithThumbnail(
|
||||
collection,
|
||||
thumbnail,
|
||||
),
|
||||
),
|
||||
),
|
||||
hw.HomeWidget.widgetClicked.listen(
|
||||
(uri) => HomeWidgetService.instance.onLaunchFromWidget(uri, context),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
1
mobile/lib/generated/intl/messages_cs.dart
generated
1
mobile/lib/generated/intl/messages_cs.dart
generated
|
@ -55,6 +55,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"Modify your query, or try searching for"),
|
||||
"moveToHiddenAlbum":
|
||||
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"selectALocation":
|
||||
MessageLookupByLibrary.simpleMessage("Select a location"),
|
||||
"selectALocationFirst":
|
||||
|
|
1
mobile/lib/generated/intl/messages_de.dart
generated
1
mobile/lib/generated/intl/messages_de.dart
generated
|
@ -1213,6 +1213,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scanne diesen Code mit \ndeiner Authentifizierungs-App"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Alben"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_en.dart
generated
1
mobile/lib/generated/intl/messages_en.dart
generated
|
@ -1175,6 +1175,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scan this barcode with\nyour authenticator app"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Albums"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_es.dart
generated
1
mobile/lib/generated/intl/messages_es.dart
generated
|
@ -1044,6 +1044,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Escanea este código QR con tu aplicación de autenticación"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchByAlbumNameHint":
|
||||
MessageLookupByLibrary.simpleMessage("Nombre del álbum"),
|
||||
"searchByExamples": MessageLookupByLibrary.simpleMessage(
|
||||
|
|
1
mobile/lib/generated/intl/messages_fr.dart
generated
1
mobile/lib/generated/intl/messages_fr.dart
generated
|
@ -1182,6 +1182,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scannez ce code-barres avec\nvotre application d\'authentification"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Albums"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_it.dart
generated
1
mobile/lib/generated/intl/messages_it.dart
generated
|
@ -1137,6 +1137,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scansione questo codice QR\ncon la tua app di autenticazione"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchByAlbumNameHint":
|
||||
MessageLookupByLibrary.simpleMessage("Nome album"),
|
||||
"searchByExamples": MessageLookupByLibrary.simpleMessage(
|
||||
|
|
1
mobile/lib/generated/intl/messages_ko.dart
generated
1
mobile/lib/generated/intl/messages_ko.dart
generated
|
@ -55,6 +55,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"Modify your query, or try searching for"),
|
||||
"moveToHiddenAlbum":
|
||||
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"selectALocation":
|
||||
MessageLookupByLibrary.simpleMessage("Select a location"),
|
||||
"selectALocationFirst":
|
||||
|
|
1
mobile/lib/generated/intl/messages_nl.dart
generated
1
mobile/lib/generated/intl/messages_nl.dart
generated
|
@ -1206,6 +1206,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scan deze barcode met\nje authenticator app"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Albums"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_no.dart
generated
1
mobile/lib/generated/intl/messages_no.dart
generated
|
@ -77,6 +77,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"Modify your query, or try searching for"),
|
||||
"moveToHiddenAlbum":
|
||||
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"selectALocation":
|
||||
MessageLookupByLibrary.simpleMessage("Select a location"),
|
||||
"selectALocationFirst":
|
||||
|
|
1
mobile/lib/generated/intl/messages_pl.dart
generated
1
mobile/lib/generated/intl/messages_pl.dart
generated
|
@ -171,6 +171,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"resetPasswordTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Zresetuj hasło"),
|
||||
"saveKey": MessageLookupByLibrary.simpleMessage("Zapisz klucz"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"selectALocation":
|
||||
MessageLookupByLibrary.simpleMessage("Select a location"),
|
||||
"selectALocationFirst":
|
||||
|
|
1
mobile/lib/generated/intl/messages_pt.dart
generated
1
mobile/lib/generated/intl/messages_pt.dart
generated
|
@ -1217,6 +1217,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Escaneie este código de barras com\nseu aplicativo autenticador"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Álbuns"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_zh.dart
generated
1
mobile/lib/generated/intl/messages_zh.dart
generated
|
@ -988,6 +988,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanCode": MessageLookupByLibrary.simpleMessage("扫描二维码/条码"),
|
||||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage("用您的身份验证器应用\n扫描此条码"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection": MessageLookupByLibrary.simpleMessage("相册"),
|
||||
"searchByAlbumNameHint": MessageLookupByLibrary.simpleMessage("相册名称"),
|
||||
"searchByExamples": MessageLookupByLibrary.simpleMessage(
|
||||
|
|
10
mobile/lib/generated/l10n.dart
generated
10
mobile/lib/generated/l10n.dart
generated
|
@ -8553,6 +8553,16 @@ class S {
|
|||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Search`
|
||||
String get search {
|
||||
return Intl.message(
|
||||
'Search',
|
||||
name: 'search',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1203,5 +1203,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1211,5 +1211,6 @@
|
|||
"invalidEndpointMessage": "Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.",
|
||||
"endpointUpdatedMessage": "Endpoint updated successfully",
|
||||
"customEndpoint": "Connected to {endpoint}",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -979,5 +979,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1160,5 +1160,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1122,5 +1122,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -17,5 +17,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1198,5 +1198,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -31,5 +31,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -118,5 +118,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1211,5 +1211,6 @@
|
|||
"invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.",
|
||||
"endpointUpdatedMessage": "Endpoint atualizado com sucesso",
|
||||
"customEndpoint": "Conectado a {endpoint}",
|
||||
"createCollaborativeLink": "Criar link colaborativo"
|
||||
"createCollaborativeLink": "Criar link colaborativo",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1211,5 +1211,6 @@
|
|||
"invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
|
||||
"endpointUpdatedMessage": "端点更新成功",
|
||||
"customEndpoint": "已连接至 {endpoint}",
|
||||
"createCollaborativeLink": "创建协作链接"
|
||||
"createCollaborativeLink": "创建协作链接",
|
||||
"search": "Search"
|
||||
}
|
|
@ -85,13 +85,24 @@ class EnteFile {
|
|||
|
||||
static int parseFileCreationTime(String? fileTitle, AssetEntity asset) {
|
||||
int creationTime = asset.createDateTime.microsecondsSinceEpoch;
|
||||
final int modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
||||
if (creationTime >= jan011981Time) {
|
||||
// assuming that fileSystem is returning correct creationTime.
|
||||
// During upload, this might get overridden with exif Creation time
|
||||
// When the assetModifiedTime is less than creationTime, than just use
|
||||
// that as creationTime. This is to handle cases where file might be
|
||||
// copied to the fileSystem from somewhere else See #https://superuser.com/a/1091147
|
||||
if (modificationTime >= jan011981Time &&
|
||||
modificationTime < creationTime) {
|
||||
_logger.info(
|
||||
'LocalID: ${asset.id} modification time is less than creation time. Using modification time as creation time',
|
||||
);
|
||||
creationTime = modificationTime;
|
||||
}
|
||||
return creationTime;
|
||||
} else {
|
||||
if (asset.modifiedDateTime.microsecondsSinceEpoch >= jan011981Time) {
|
||||
creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
||||
if (modificationTime >= jan011981Time) {
|
||||
creationTime = modificationTime;
|
||||
} else {
|
||||
creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
|
||||
}
|
||||
|
@ -106,7 +117,6 @@ class EnteFile {
|
|||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,14 @@ import "package:logging/logging.dart";
|
|||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/models/collection/collection_items.dart";
|
||||
import "package:photos/models/file/file_type.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/favorites_service.dart";
|
||||
import "package:photos/ui/viewer/file/detail_page.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
import "package:photos/utils/preload_util.dart";
|
||||
|
||||
class HomeWidgetService {
|
||||
|
@ -171,4 +176,49 @@ class HomeWidgetService {
|
|||
);
|
||||
_logger.info(">>> SlideshowWidget cleared");
|
||||
}
|
||||
|
||||
Future<void> onLaunchFromWidget(Uri? uri, BuildContext context) async {
|
||||
if (uri == null) return;
|
||||
|
||||
final collectionID =
|
||||
await FavoritesService.instance.getFavoriteCollectionID();
|
||||
if (collectionID == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final collection = CollectionsService.instance.getCollectionByID(
|
||||
collectionID,
|
||||
);
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final thumbnail = await CollectionsService.instance.getCover(collection);
|
||||
|
||||
final previousGeneratedId =
|
||||
await hw.HomeWidget.getWidgetData<int>("home_widget_last_img");
|
||||
|
||||
final res = previousGeneratedId != null
|
||||
? await FilesDB.instance.getFile(
|
||||
previousGeneratedId,
|
||||
)
|
||||
: null;
|
||||
|
||||
routeToPage(
|
||||
context,
|
||||
CollectionPage(
|
||||
CollectionWithThumbnail(
|
||||
collection,
|
||||
thumbnail,
|
||||
),
|
||||
),
|
||||
).ignore();
|
||||
|
||||
if (res == null) return;
|
||||
|
||||
final page = DetailPage(
|
||||
DetailPageConfiguration(List.unmodifiable([res]), null, 0, "collection"),
|
||||
);
|
||||
routeToPage(context, page, forceCustomPageRoute: true).ignore();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import "dart:io";
|
|||
import "package:connectivity_plus/connectivity_plus.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/errors.dart";
|
||||
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/event.dart";
|
||||
import "package:photos/services/remote_assets_service.dart";
|
||||
|
@ -23,7 +22,7 @@ abstract class MLFramework {
|
|||
MLFramework(this.shouldDownloadOverMobileData) {
|
||||
Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen((ConnectivityResult result) async {
|
||||
.listen((List<ConnectivityResult> result) async {
|
||||
_logger.info("Connectivity changed to $result");
|
||||
if (_state == InitializationState.waitingForNetwork &&
|
||||
await _canDownload()) {
|
||||
|
@ -135,9 +134,11 @@ abstract class MLFramework {
|
|||
}
|
||||
|
||||
Future<bool> _canDownload() async {
|
||||
final connectivityResult = await (Connectivity().checkConnectivity());
|
||||
return connectivityResult != ConnectivityResult.mobile ||
|
||||
shouldDownloadOverMobileData;
|
||||
final List<ConnectivityResult> connections =
|
||||
await (Connectivity().checkConnectivity());
|
||||
final bool isConnectedToMobile =
|
||||
connections.contains(ConnectivityResult.mobile);
|
||||
return !isConnectedToMobile || shouldDownloadOverMobileData;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,9 @@ class SyncService {
|
|||
sync();
|
||||
});
|
||||
|
||||
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
|
||||
Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen((List<ConnectivityResult> result) {
|
||||
_logger.info("Connectivity change detected " + result.toString());
|
||||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
sync();
|
||||
|
|
|
@ -5,6 +5,7 @@ import "package:flutter/scheduler.dart";
|
|||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/clear_and_unfocus_search_bar_event.dart";
|
||||
import "package:photos/events/tab_changed_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/search/index_of_indexed_stack.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
|
@ -130,17 +131,14 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
color: colorScheme.backgroundBase,
|
||||
child: Container(
|
||||
color: colorScheme.fillFaint,
|
||||
child: TextFormField(
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
// Below parameters are to disable auto-suggestion
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
// Above parameters are to disable auto-suggestion
|
||||
decoration: InputDecoration(
|
||||
//TODO: Extract string
|
||||
hintText: "Search",
|
||||
hintText: S.of(context).search,
|
||||
filled: true,
|
||||
fillColor: getEnteColorScheme(context).fillFaint,
|
||||
border: const UnderlineInputBorder(
|
||||
|
@ -161,6 +159,9 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
minHeight: 44,
|
||||
minWidth: 44,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
prefixIcon: Hero(
|
||||
tag: "search_icon",
|
||||
child: Icon(
|
||||
|
@ -168,6 +169,7 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
color: colorScheme.strokeFaint,
|
||||
),
|
||||
),
|
||||
|
||||
/*Using valueListenableBuilder inside a stateful widget because this widget is only rebuild when
|
||||
setState is called when deboucncing is over and the spinner needs to be shown while debouncing */
|
||||
suffixIcon: ValueListenableBuilder(
|
||||
|
|
|
@ -363,9 +363,10 @@ class FileUploader {
|
|||
if (isForceUpload) {
|
||||
return;
|
||||
}
|
||||
final connectivityResult = await (Connectivity().checkConnectivity());
|
||||
final List<ConnectivityResult> connections =
|
||||
await (Connectivity().checkConnectivity());
|
||||
bool canUploadUnderCurrentNetworkConditions = true;
|
||||
if (connectivityResult == ConnectivityResult.mobile) {
|
||||
if (connections.any((element) => element == ConnectivityResult.mobile)) {
|
||||
canUploadUnderCurrentNetworkConditions =
|
||||
Configuration.instance.shouldBackupOverMobileData();
|
||||
}
|
||||
|
|
|
@ -113,38 +113,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
bonsoir:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir
|
||||
sha256: "800d77c0581fff06cc43ef2b7723dfe5ee9b899ab0fdf80fb1c7b8829a5deb5c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0+1"
|
||||
bonsoir_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_android
|
||||
sha256: "7207c36fd7e0f3c7c2d8cf353f02bd640d96e2387d575837f8ac051c9cbf4aa7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0+1"
|
||||
bonsoir_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_darwin
|
||||
sha256: "7211042c85da2d6efa80c0976bbd9568f2b63624097779847548ed4530675ade"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
bonsoir_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_platform_interface
|
||||
sha256: "64d57cd52bd477b4891e9b9d419e6408da171ed9e0efc8aa716e7e343d5d93ad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -241,14 +209,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
cast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cast
|
||||
sha256: b70f6be547a53481dffec93ad3cc4974fae5ed707f0b677d4a50c329d7299b98
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -326,18 +286,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
|
||||
sha256: ebe15d94de9dd7c31dc2ac54e42780acdf3384b1497c69290c9f3c5b0279fc57
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
version: "6.0.2"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
|
||||
sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.4"
|
||||
version: "2.0.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -745,10 +705,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: a701df4866f9a38bb8e4450a54c143bbeeb0ce2381e7df5a36e1006f3b43bb28
|
||||
sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "17.0.1"
|
||||
version: "17.0.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1769,14 +1729,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -12,7 +12,7 @@ description: ente photos application
|
|||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.8.81+601
|
||||
version: 0.8.82+602
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
@ -27,7 +27,6 @@ dependencies:
|
|||
battery_info: ^1.1.1
|
||||
bip39: ^1.0.6
|
||||
cached_network_image: ^3.0.0
|
||||
cast: ^2.0.0
|
||||
chewie:
|
||||
git:
|
||||
url: https://github.com/ente-io/chewie.git
|
||||
|
@ -37,11 +36,7 @@ dependencies:
|
|||
collection: # dart
|
||||
computer:
|
||||
git: "https://github.com/ente-io/computer.git"
|
||||
connectivity_plus:
|
||||
git:
|
||||
url: https://github.com/ente-io/plus_plugins.git
|
||||
ref: check_mobile_first
|
||||
path: packages/connectivity_plus/connectivity_plus/
|
||||
connectivity_plus: ^6.0.2
|
||||
cross_file: ^0.3.3
|
||||
crypto: ^3.0.2
|
||||
cupertino_icons: ^1.0.0
|
||||
|
@ -175,7 +170,6 @@ dependencies:
|
|||
xml: ^6.3.0
|
||||
|
||||
dependency_overrides:
|
||||
connectivity_plus: ^4.0.0
|
||||
# Remove this after removing dependency from flutter_sodium.
|
||||
# Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0
|
||||
ffi: 2.1.0
|
||||
|
|
|
@ -73,8 +73,11 @@ http:
|
|||
|
||||
# Specify the base endpoints for various apps
|
||||
apps:
|
||||
public-albums: "https://albums.ente.io"
|
||||
|
||||
# Default is https://albums.ente.io
|
||||
#
|
||||
# If you're running a self hosted instance and wish to serve public links,
|
||||
# set this to the URL where your albums web app is running.
|
||||
public-albums:
|
||||
|
||||
# Database connection parameters
|
||||
db:
|
||||
|
|
|
@ -23,7 +23,7 @@ type PublicCollectionRepository struct {
|
|||
// NewPublicCollectionRepository ..
|
||||
func NewPublicCollectionRepository(db *sql.DB, albumHost string) *PublicCollectionRepository {
|
||||
if albumHost == "" {
|
||||
panic("albumHost can not be empty")
|
||||
albumHost = "https://albums.ente.io"
|
||||
}
|
||||
return &PublicCollectionRepository{
|
||||
DB: db,
|
||||
|
|
|
@ -1,50 +1,24 @@
|
|||
import { SlideshowContext } from "pages/slideshow";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function PhotoAuditorium({
|
||||
url,
|
||||
nextSlideUrl,
|
||||
}: {
|
||||
interface PhotoAuditoriumProps {
|
||||
url: string;
|
||||
nextSlideUrl: string;
|
||||
}) {
|
||||
const { showNextSlide } = useContext(SlideshowContext);
|
||||
|
||||
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
|
||||
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
|
||||
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
|
||||
|
||||
showNextSlide: () => void;
|
||||
}
|
||||
export const PhotoAuditorium: React.FC<PhotoAuditoriumProps> = ({
|
||||
url,
|
||||
nextSlideUrl,
|
||||
showNextSlide,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
let timeout2: NodeJS.Timeout;
|
||||
|
||||
if (nextSlidePrerendered) {
|
||||
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
|
||||
const delayTime = Math.max(10000 - elapsedTime, 0);
|
||||
|
||||
if (elapsedTime >= 10000) {
|
||||
setShowPreloadedNextSlide(true);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
setShowPreloadedNextSlide(true);
|
||||
}, delayTime);
|
||||
}
|
||||
|
||||
if (showNextSlide) {
|
||||
timeout2 = setTimeout(() => {
|
||||
showNextSlide();
|
||||
setNextSlidePrerendered(false);
|
||||
setPrerenderTime(null);
|
||||
setShowPreloadedNextSlide(false);
|
||||
}, delayTime);
|
||||
}
|
||||
}
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
showNextSlide();
|
||||
}, 10000);
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
if (timeout2) clearTimeout(timeout2);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
|
||||
}, [showNextSlide]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -69,27 +43,22 @@ export default function PhotoAuditorium({
|
|||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "none" : "block",
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={nextSlideUrl}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "block" : "none",
|
||||
display: "none",
|
||||
}}
|
||||
onLoad={() => {
|
||||
setNextSlidePrerendered(true);
|
||||
setPrerenderTime(Date.now());
|
||||
/>
|
||||
<img
|
||||
src={url}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
import { SlideshowContext } from "pages/slideshow";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
export default function PhotoAuditorium({
|
||||
url,
|
||||
nextSlideUrl,
|
||||
}: {
|
||||
url: string;
|
||||
nextSlideUrl: string;
|
||||
}) {
|
||||
const { showNextSlide } = useContext(SlideshowContext);
|
||||
|
||||
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
|
||||
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
|
||||
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
let timeout2: NodeJS.Timeout;
|
||||
|
||||
if (nextSlidePrerendered) {
|
||||
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
|
||||
const delayTime = Math.max(10000 - elapsedTime, 0);
|
||||
|
||||
if (elapsedTime >= 10000) {
|
||||
setShowPreloadedNextSlide(true);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
setShowPreloadedNextSlide(true);
|
||||
}, delayTime);
|
||||
}
|
||||
|
||||
if (showNextSlide) {
|
||||
timeout2 = setTimeout(() => {
|
||||
showNextSlide();
|
||||
setNextSlidePrerendered(false);
|
||||
setPrerenderTime(null);
|
||||
setShowPreloadedNextSlide(false);
|
||||
}, delayTime);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
if (timeout2) clearTimeout(timeout2);
|
||||
};
|
||||
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
backgroundImage: `url(${url})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundBlendMode: "multiply",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "none" : "block",
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={nextSlideUrl}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "block" : "none",
|
||||
}}
|
||||
onLoad={() => {
|
||||
setNextSlidePrerendered(true);
|
||||
setPrerenderTime(Date.now());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import mime from "mime-types";
|
||||
import { SlideshowContext } from "pages/slideshow";
|
||||
import { useContext, useEffect, useRef } from "react";
|
||||
|
||||
export default function VideoAuditorium({
|
||||
name,
|
||||
url,
|
||||
}: {
|
||||
name: string;
|
||||
url: string;
|
||||
}) {
|
||||
const { showNextSlide } = useContext(SlideshowContext);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
attemptPlay();
|
||||
}, [url, videoRef]);
|
||||
|
||||
const attemptPlay = async () => {
|
||||
if (videoRef.current) {
|
||||
try {
|
||||
await videoRef.current.play();
|
||||
} catch {
|
||||
showNextSlide();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
controls
|
||||
style={{
|
||||
maxWidth: "100vw",
|
||||
maxHeight: "100vh",
|
||||
}}
|
||||
onError={showNextSlide}
|
||||
onEnded={showNextSlide}
|
||||
>
|
||||
<source src={url} type={mime.lookup(name)} />
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import { FILE_TYPE } from "constants/file";
|
||||
import PhotoAuditorium from "./PhotoAuditorium";
|
||||
// import VideoAuditorium from './VideoAuditorium';
|
||||
|
||||
interface fileProp {
|
||||
fileName: string;
|
||||
fileURL: string;
|
||||
type: FILE_TYPE;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
file1: fileProp;
|
||||
file2: fileProp;
|
||||
}
|
||||
|
||||
export default function Theatre(props: IProps) {
|
||||
switch (props.file1.type && props.file2.type) {
|
||||
case FILE_TYPE.IMAGE:
|
||||
return (
|
||||
<PhotoAuditorium
|
||||
url={props.file1.fileURL}
|
||||
nextSlideUrl={props.file2.fileURL}
|
||||
/>
|
||||
);
|
||||
// case FILE_TYPE.VIDEO:
|
||||
// return (
|
||||
// <VideoAuditorium name={props.fileName} url={props.fileURL} />
|
||||
// );
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import log from "@/next/log";
|
||||
import PairedSuccessfullyOverlay from "components/PairedSuccessfullyOverlay";
|
||||
import Theatre from "components/Theatre";
|
||||
import { PhotoAuditorium } from "components/PhotoAuditorium";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import { useRouter } from "next/router";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
getCastCollection,
|
||||
getLocalFiles,
|
||||
|
@ -13,25 +13,20 @@ import { Collection } from "types/collection";
|
|||
import { EnteFile } from "types/file";
|
||||
import { getPreviewableImage, isRawFileFromFileName } from "utils/file";
|
||||
|
||||
export const SlideshowContext = createContext<{
|
||||
showNextSlide: () => void;
|
||||
}>(null);
|
||||
|
||||
const renderableFileURLCache = new Map<number, string>();
|
||||
|
||||
export default function Slideshow() {
|
||||
const [collectionFiles, setCollectionFiles] = useState<EnteFile[]>([]);
|
||||
|
||||
const [currentFile, setCurrentFile] = useState<EnteFile | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [nextFile, setNextFile] = useState<EnteFile | undefined>(undefined);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [castToken, setCastToken] = useState<string>("");
|
||||
const [castCollection, setCastCollection] = useState<
|
||||
Collection | undefined
|
||||
>(undefined);
|
||||
>();
|
||||
const [collectionFiles, setCollectionFiles] = useState<EnteFile[]>([]);
|
||||
const [currentFileId, setCurrentFileId] = useState<number | undefined>();
|
||||
const [currentFileURL, setCurrentFileURL] = useState<string | undefined>();
|
||||
const [nextFileURL, setNextFileURL] = useState<string | undefined>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const syncCastFiles = async (token: string) => {
|
||||
try {
|
||||
|
@ -72,29 +67,16 @@ export default function Slideshow() {
|
|||
|
||||
const isFileEligibleForCast = (file: EnteFile) => {
|
||||
const fileType = file.metadata.fileType;
|
||||
if (fileType !== FILE_TYPE.IMAGE && fileType !== FILE_TYPE.LIVE_PHOTO) {
|
||||
if (fileType !== FILE_TYPE.IMAGE && fileType !== FILE_TYPE.LIVE_PHOTO)
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileSizeLimit = 100 * 1024 * 1024;
|
||||
if (file.info.fileSize > 100 * 1024 * 1024) return false;
|
||||
|
||||
if (file.info.fileSize > fileSizeLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const name = file.metadata.title;
|
||||
|
||||
if (fileType === FILE_TYPE.IMAGE) {
|
||||
if (isRawFileFromFileName(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (isRawFileFromFileName(file.metadata.title)) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const castToken = window.localStorage.getItem("castToken");
|
||||
|
@ -117,9 +99,9 @@ export default function Slideshow() {
|
|||
showNextSlide();
|
||||
}, [collectionFiles]);
|
||||
|
||||
const showNextSlide = () => {
|
||||
const showNextSlide = async () => {
|
||||
const currentIndex = collectionFiles.findIndex(
|
||||
(file) => file.id === currentFile?.id,
|
||||
(file) => file.id === currentFileId,
|
||||
);
|
||||
|
||||
const nextIndex = (currentIndex + 1) % collectionFiles.length;
|
||||
|
@ -128,63 +110,44 @@ export default function Slideshow() {
|
|||
const nextFile = collectionFiles[nextIndex];
|
||||
const nextNextFile = collectionFiles[nextNextIndex];
|
||||
|
||||
setCurrentFile(nextFile);
|
||||
setNextFile(nextNextFile);
|
||||
let nextURL = renderableFileURLCache.get(nextFile.id);
|
||||
let nextNextURL = renderableFileURLCache.get(nextNextFile.id);
|
||||
|
||||
if (!nextURL) {
|
||||
try {
|
||||
const blob = await getPreviewableImage(nextFile, castToken);
|
||||
const url = URL.createObjectURL(blob);
|
||||
renderableFileURLCache.set(nextFile.id, url);
|
||||
nextURL = url;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextNextURL) {
|
||||
try {
|
||||
const blob = await getPreviewableImage(nextNextFile, castToken);
|
||||
const url = URL.createObjectURL(blob);
|
||||
renderableFileURLCache.set(nextNextFile.id, url);
|
||||
nextNextURL = url;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
setCurrentFileId(nextFile.id);
|
||||
setCurrentFileURL(nextURL);
|
||||
setNextFileURL(nextNextURL);
|
||||
};
|
||||
|
||||
const [renderableFileURL, setRenderableFileURL] = useState<string>("");
|
||||
|
||||
const getRenderableFileURL = async () => {
|
||||
if (!currentFile) return;
|
||||
|
||||
const cacheValue = renderableFileURLCache.get(currentFile.id);
|
||||
if (cacheValue) {
|
||||
setRenderableFileURL(cacheValue);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await getPreviewableImage(
|
||||
currentFile as EnteFile,
|
||||
castToken,
|
||||
);
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
renderableFileURLCache.set(currentFile?.id, url);
|
||||
|
||||
setRenderableFileURL(url);
|
||||
} catch (e) {
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentFile) {
|
||||
getRenderableFileURL();
|
||||
}
|
||||
}, [currentFile]);
|
||||
if (loading) return <PairedSuccessfullyOverlay />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlideshowContext.Provider value={{ showNextSlide }}>
|
||||
<Theatre
|
||||
file1={{
|
||||
fileName: currentFile?.metadata.title,
|
||||
fileURL: renderableFileURL,
|
||||
type: currentFile?.metadata.fileType,
|
||||
}}
|
||||
file2={{
|
||||
fileName: nextFile?.metadata.title,
|
||||
fileURL: renderableFileURL,
|
||||
type: nextFile?.metadata.fileType,
|
||||
}}
|
||||
/>
|
||||
</SlideshowContext.Provider>
|
||||
{loading && <PairedSuccessfullyOverlay />}
|
||||
</>
|
||||
<PhotoAuditorium
|
||||
url={currentFileURL}
|
||||
nextSlideUrl={nextFileURL}
|
||||
showNextSlide={showNextSlide}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ export default function AlbumCastDialog(props: Props) {
|
|||
setFieldError,
|
||||
) => {
|
||||
try {
|
||||
await doCast(value);
|
||||
await doCast(value.trim());
|
||||
props.onHide();
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
|
|
|
@ -57,7 +57,8 @@ export default function SearchInput(props: Iprops) {
|
|||
const appContext = useContext(AppContext);
|
||||
const handleChange = (value: SearchOption) => {
|
||||
setValue(value);
|
||||
setQuery(value.label);
|
||||
setQuery(value?.label);
|
||||
|
||||
blur();
|
||||
};
|
||||
const handleInputChange = (value: string, actionMeta: InputActionMeta) => {
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
import { getAccountsURL } from "@ente/shared/network/api";
|
||||
import { THEME_COLOR } from "@ente/shared/themes/constants";
|
||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
||||
import WatchFolder from "components/WatchFolder";
|
||||
import { WatchFolder } from "components/WatchFolder";
|
||||
import isElectron from "is-electron";
|
||||
import { getAccountsToken } from "services/userService";
|
||||
import { getDownloadAppMessage } from "utils/ui";
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
savePublicCollectionUploaderName,
|
||||
} from "services/publicCollectionService";
|
||||
import uploadManager from "services/upload/uploadManager";
|
||||
import watchFolderService from "services/watchFolder/watchFolderService";
|
||||
import watchFolderService from "services/watch";
|
||||
import { NotificationAttributes } from "types/Notification";
|
||||
import { Collection } from "types/collection";
|
||||
import {
|
||||
|
|
364
web/apps/photos/src/components/WatchFolder.tsx
Normal file
364
web/apps/photos/src/components/WatchFolder.tsx
Normal file
|
@ -0,0 +1,364 @@
|
|||
import {
|
||||
FlexWrapper,
|
||||
HorizontalFlex,
|
||||
SpaceBetweenFlex,
|
||||
VerticallyCentered,
|
||||
} from "@ente/shared/components/Container";
|
||||
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
|
||||
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
||||
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined";
|
||||
import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined";
|
||||
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
|
||||
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import UploadStrategyChoiceModal from "components/Upload/UploadStrategyChoiceModal";
|
||||
import { PICKED_UPLOAD_TYPE, UPLOAD_STRATEGY } from "constants/upload";
|
||||
import { t } from "i18next";
|
||||
import { AppContext } from "pages/_app";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import watchFolderService from "services/watch";
|
||||
import { WatchMapping } from "types/watchFolder";
|
||||
import { getImportSuggestion } from "utils/upload";
|
||||
|
||||
interface WatchFolderProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
|
||||
const [mappings, setMappings] = useState<WatchMapping[]>([]);
|
||||
const [inputFolderPath, setInputFolderPath] = useState("");
|
||||
const [choiceModalOpen, setChoiceModalOpen] = useState(false);
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
const electron = globalThis.electron;
|
||||
|
||||
useEffect(() => {
|
||||
if (!electron) return;
|
||||
watchFolderService.getWatchMappings().then((m) => setMappings(m));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
appContext.watchFolderFiles &&
|
||||
appContext.watchFolderFiles.length > 0
|
||||
) {
|
||||
handleFolderDrop(appContext.watchFolderFiles);
|
||||
appContext.setWatchFolderFiles(null);
|
||||
}
|
||||
}, [appContext.watchFolderFiles]);
|
||||
|
||||
const handleFolderDrop = async (folders: FileList) => {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder: any = folders[i];
|
||||
const path = (folder.path as string).replace(/\\/g, "/");
|
||||
if (await watchFolderService.isFolder(path)) {
|
||||
await addFolderForWatching(path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addFolderForWatching = async (path: string) => {
|
||||
if (!electron) return;
|
||||
|
||||
setInputFolderPath(path);
|
||||
const files = await electron.getDirFiles(path);
|
||||
const analysisResult = getImportSuggestion(
|
||||
PICKED_UPLOAD_TYPE.FOLDERS,
|
||||
files,
|
||||
);
|
||||
if (analysisResult.hasNestedFolders) {
|
||||
setChoiceModalOpen(true);
|
||||
} else {
|
||||
handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION, path);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddFolderClick = async () => {
|
||||
await handleFolderSelection();
|
||||
};
|
||||
|
||||
const handleFolderSelection = async () => {
|
||||
const folderPath = await watchFolderService.selectFolder();
|
||||
if (folderPath) {
|
||||
await addFolderForWatching(folderPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddWatchMapping = async (
|
||||
uploadStrategy: UPLOAD_STRATEGY,
|
||||
folderPath?: string,
|
||||
) => {
|
||||
folderPath = folderPath || inputFolderPath;
|
||||
await watchFolderService.addWatchMapping(
|
||||
folderPath.substring(folderPath.lastIndexOf("/") + 1),
|
||||
folderPath,
|
||||
uploadStrategy,
|
||||
);
|
||||
setInputFolderPath("");
|
||||
setMappings(await watchFolderService.getWatchMappings());
|
||||
};
|
||||
|
||||
const handleRemoveWatchMapping = (mapping: WatchMapping) => {
|
||||
watchFolderService
|
||||
.mappingsAfterRemovingFolder(mapping.folderPath)
|
||||
.then((ms) => setMappings(ms));
|
||||
};
|
||||
|
||||
const closeChoiceModal = () => setChoiceModalOpen(false);
|
||||
|
||||
const uploadToSingleCollection = () => {
|
||||
closeChoiceModal();
|
||||
handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION);
|
||||
};
|
||||
|
||||
const uploadToMultipleCollection = () => {
|
||||
closeChoiceModal();
|
||||
handleAddWatchMapping(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
|
||||
>
|
||||
<DialogTitleWithCloseButton
|
||||
onClose={onClose}
|
||||
sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
|
||||
>
|
||||
{t("WATCHED_FOLDERS")}
|
||||
</DialogTitleWithCloseButton>
|
||||
<DialogContent sx={{ flex: 1 }}>
|
||||
<Stack spacing={1} p={1.5} height={"100%"}>
|
||||
<MappingList
|
||||
mappings={mappings}
|
||||
handleRemoveWatchMapping={handleRemoveWatchMapping}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
color="accent"
|
||||
onClick={handleAddFolderClick}
|
||||
>
|
||||
<span>+</span>
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "8px",
|
||||
}}
|
||||
></span>
|
||||
{t("ADD_FOLDER")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<UploadStrategyChoiceModal
|
||||
open={choiceModalOpen}
|
||||
onClose={closeChoiceModal}
|
||||
uploadToSingleCollection={uploadToSingleCollection}
|
||||
uploadToMultipleCollection={uploadToMultipleCollection}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MappingsContainer = styled(Box)(() => ({
|
||||
height: "278px",
|
||||
overflow: "auto",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
}));
|
||||
|
||||
const NoMappingsContainer = styled(VerticallyCentered)({
|
||||
textAlign: "left",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: "32px",
|
||||
});
|
||||
|
||||
const EntryContainer = styled(Box)({
|
||||
marginLeft: "12px",
|
||||
marginRight: "6px",
|
||||
marginBottom: "12px",
|
||||
});
|
||||
|
||||
interface MappingListProps {
|
||||
mappings: WatchMapping[];
|
||||
handleRemoveWatchMapping: (value: WatchMapping) => void;
|
||||
}
|
||||
|
||||
const MappingList: React.FC<MappingListProps> = ({
|
||||
mappings,
|
||||
handleRemoveWatchMapping,
|
||||
}) => {
|
||||
return mappings.length === 0 ? (
|
||||
<NoMappingsContent />
|
||||
) : (
|
||||
<MappingsContainer>
|
||||
{mappings.map((mapping) => {
|
||||
return (
|
||||
<MappingEntry
|
||||
key={mapping.rootFolderName}
|
||||
mapping={mapping}
|
||||
handleRemoveMapping={handleRemoveWatchMapping}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MappingsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const NoMappingsContent: React.FC = () => {
|
||||
return (
|
||||
<NoMappingsContainer>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="large" fontWeight={"bold"}>
|
||||
{t("NO_FOLDERS_ADDED")}
|
||||
</Typography>
|
||||
<Typography py={0.5} variant={"small"} color="text.muted">
|
||||
{t("FOLDERS_AUTOMATICALLY_MONITORED")}
|
||||
</Typography>
|
||||
<Typography variant={"small"} color="text.muted">
|
||||
<FlexWrapper gap={1}>
|
||||
<CheckmarkIcon />
|
||||
{t("UPLOAD_NEW_FILES_TO_ENTE")}
|
||||
</FlexWrapper>
|
||||
</Typography>
|
||||
<Typography variant={"small"} color="text.muted">
|
||||
<FlexWrapper gap={1}>
|
||||
<CheckmarkIcon />
|
||||
{t("REMOVE_DELETED_FILES_FROM_ENTE")}
|
||||
</FlexWrapper>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</NoMappingsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckmarkIcon: React.FC = () => {
|
||||
return (
|
||||
<CheckIcon
|
||||
fontSize="small"
|
||||
sx={{
|
||||
display: "inline",
|
||||
fontSize: "15px",
|
||||
|
||||
color: (theme) => theme.palette.secondary.main,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface MappingEntryProps {
|
||||
mapping: WatchMapping;
|
||||
handleRemoveMapping: (mapping: WatchMapping) => void;
|
||||
}
|
||||
|
||||
const MappingEntry: React.FC<MappingEntryProps> = ({
|
||||
mapping,
|
||||
handleRemoveMapping,
|
||||
}) => {
|
||||
const appContext = React.useContext(AppContext);
|
||||
|
||||
const stopWatching = () => {
|
||||
handleRemoveMapping(mapping);
|
||||
};
|
||||
|
||||
const confirmStopWatching = () => {
|
||||
appContext.setDialogMessage({
|
||||
title: t("STOP_WATCHING_FOLDER"),
|
||||
content: t("STOP_WATCHING_DIALOG_MESSAGE"),
|
||||
close: {
|
||||
text: t("CANCEL"),
|
||||
variant: "secondary",
|
||||
},
|
||||
proceed: {
|
||||
action: stopWatching,
|
||||
text: t("YES_STOP"),
|
||||
variant: "critical",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SpaceBetweenFlex>
|
||||
<HorizontalFlex>
|
||||
{mapping &&
|
||||
mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
|
||||
<Tooltip title={t("UPLOADED_TO_SINGLE_COLLECTION")}>
|
||||
<FolderOpenIcon />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={t("UPLOADED_TO_SEPARATE_COLLECTIONS")}>
|
||||
<FolderCopyOutlinedIcon />
|
||||
</Tooltip>
|
||||
)}
|
||||
<EntryContainer>
|
||||
<EntryHeading mapping={mapping} />
|
||||
<Typography color="text.muted" variant="small">
|
||||
{mapping.folderPath}
|
||||
</Typography>
|
||||
</EntryContainer>
|
||||
</HorizontalFlex>
|
||||
<MappingEntryOptions confirmStopWatching={confirmStopWatching} />
|
||||
</SpaceBetweenFlex>
|
||||
);
|
||||
};
|
||||
|
||||
interface EntryHeadingProps {
|
||||
mapping: WatchMapping;
|
||||
}
|
||||
|
||||
const EntryHeading: React.FC<EntryHeadingProps> = ({ mapping }) => {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<FlexWrapper gap={1}>
|
||||
<Typography>{mapping.rootFolderName}</Typography>
|
||||
{appContext.isFolderSyncRunning &&
|
||||
watchFolderService.isMappingSyncInProgress(mapping) && (
|
||||
<CircularProgress size={12} />
|
||||
)}
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
interface MappingEntryOptionsProps {
|
||||
confirmStopWatching: () => void;
|
||||
}
|
||||
|
||||
const MappingEntryOptions: React.FC<MappingEntryOptionsProps> = ({
|
||||
confirmStopWatching,
|
||||
}) => {
|
||||
return (
|
||||
<OverflowMenu
|
||||
menuPaperProps={{
|
||||
sx: {
|
||||
backgroundColor: (theme) =>
|
||||
theme.colors.background.elevated2,
|
||||
},
|
||||
}}
|
||||
ariaControls={"watch-mapping-option"}
|
||||
triggerButtonIcon={<MoreHorizIcon />}
|
||||
>
|
||||
<OverflowMenuOption
|
||||
color="critical"
|
||||
onClick={confirmStopWatching}
|
||||
startIcon={<DoNotDisturbOutlinedIcon />}
|
||||
>
|
||||
{t("STOP_WATCHING")}
|
||||
</OverflowMenuOption>
|
||||
</OverflowMenu>
|
||||
);
|
||||
};
|
|
@ -1,152 +0,0 @@
|
|||
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
|
||||
import { Button, Dialog, DialogContent, Stack } from "@mui/material";
|
||||
import UploadStrategyChoiceModal from "components/Upload/UploadStrategyChoiceModal";
|
||||
import { PICKED_UPLOAD_TYPE, UPLOAD_STRATEGY } from "constants/upload";
|
||||
import { t } from "i18next";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import watchFolderService from "services/watchFolder/watchFolderService";
|
||||
import { WatchMapping } from "types/watchFolder";
|
||||
import { getImportSuggestion } from "utils/upload";
|
||||
import { MappingList } from "./mappingList";
|
||||
|
||||
interface Iprops {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function WatchFolder({ open, onClose }: Iprops) {
|
||||
const [mappings, setMappings] = useState<WatchMapping[]>([]);
|
||||
const [inputFolderPath, setInputFolderPath] = useState("");
|
||||
const [choiceModalOpen, setChoiceModalOpen] = useState(false);
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
const electron = globalThis.electron;
|
||||
|
||||
useEffect(() => {
|
||||
if (!electron) return;
|
||||
watchFolderService.getWatchMappings().then((m) => setMappings(m));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
appContext.watchFolderFiles &&
|
||||
appContext.watchFolderFiles.length > 0
|
||||
) {
|
||||
handleFolderDrop(appContext.watchFolderFiles);
|
||||
appContext.setWatchFolderFiles(null);
|
||||
}
|
||||
}, [appContext.watchFolderFiles]);
|
||||
|
||||
const handleFolderDrop = async (folders: FileList) => {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder: any = folders[i];
|
||||
const path = (folder.path as string).replace(/\\/g, "/");
|
||||
if (await watchFolderService.isFolder(path)) {
|
||||
await addFolderForWatching(path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addFolderForWatching = async (path: string) => {
|
||||
if (!electron) return;
|
||||
|
||||
setInputFolderPath(path);
|
||||
const files = await electron.getDirFiles(path);
|
||||
const analysisResult = getImportSuggestion(
|
||||
PICKED_UPLOAD_TYPE.FOLDERS,
|
||||
files,
|
||||
);
|
||||
if (analysisResult.hasNestedFolders) {
|
||||
setChoiceModalOpen(true);
|
||||
} else {
|
||||
handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION, path);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddFolderClick = async () => {
|
||||
await handleFolderSelection();
|
||||
};
|
||||
|
||||
const handleFolderSelection = async () => {
|
||||
const folderPath = await watchFolderService.selectFolder();
|
||||
if (folderPath) {
|
||||
await addFolderForWatching(folderPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddWatchMapping = async (
|
||||
uploadStrategy: UPLOAD_STRATEGY,
|
||||
folderPath?: string,
|
||||
) => {
|
||||
folderPath = folderPath || inputFolderPath;
|
||||
await watchFolderService.addWatchMapping(
|
||||
folderPath.substring(folderPath.lastIndexOf("/") + 1),
|
||||
folderPath,
|
||||
uploadStrategy,
|
||||
);
|
||||
setInputFolderPath("");
|
||||
setMappings(await watchFolderService.getWatchMappings());
|
||||
};
|
||||
|
||||
const handleRemoveWatchMapping = async (mapping: WatchMapping) => {
|
||||
await watchFolderService.removeWatchMapping(mapping.folderPath);
|
||||
setMappings(await watchFolderService.getWatchMappings());
|
||||
};
|
||||
|
||||
const closeChoiceModal = () => setChoiceModalOpen(false);
|
||||
|
||||
const uploadToSingleCollection = () => {
|
||||
closeChoiceModal();
|
||||
handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION);
|
||||
};
|
||||
|
||||
const uploadToMultipleCollection = () => {
|
||||
closeChoiceModal();
|
||||
handleAddWatchMapping(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
|
||||
>
|
||||
<DialogTitleWithCloseButton
|
||||
onClose={onClose}
|
||||
sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
|
||||
>
|
||||
{t("WATCHED_FOLDERS")}
|
||||
</DialogTitleWithCloseButton>
|
||||
<DialogContent sx={{ flex: 1 }}>
|
||||
<Stack spacing={1} p={1.5} height={"100%"}>
|
||||
<MappingList
|
||||
mappings={mappings}
|
||||
handleRemoveWatchMapping={handleRemoveWatchMapping}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
color="accent"
|
||||
onClick={handleAddFolderClick}
|
||||
>
|
||||
<span>+</span>
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "8px",
|
||||
}}
|
||||
></span>
|
||||
{t("ADD_FOLDER")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<UploadStrategyChoiceModal
|
||||
open={choiceModalOpen}
|
||||
onClose={closeChoiceModal}
|
||||
uploadToSingleCollection={uploadToSingleCollection}
|
||||
uploadToMultipleCollection={uploadToMultipleCollection}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
import { CircularProgress, Typography } from "@mui/material";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext } from "react";
|
||||
import watchFolderService from "services/watchFolder/watchFolderService";
|
||||
import { WatchMapping } from "types/watchFolder";
|
||||
|
||||
interface Iprops {
|
||||
mapping: WatchMapping;
|
||||
}
|
||||
|
||||
export function EntryHeading({ mapping }: Iprops) {
|
||||
const appContext = useContext(AppContext);
|
||||
return (
|
||||
<FlexWrapper gap={1}>
|
||||
<Typography>{mapping.rootFolderName}</Typography>
|
||||
{appContext.isFolderSyncRunning &&
|
||||
watchFolderService.isMappingSyncInProgress(mapping) && (
|
||||
<CircularProgress size={12} />
|
||||
)}
|
||||
</FlexWrapper>
|
||||
);
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import {
|
||||
HorizontalFlex,
|
||||
SpaceBetweenFlex,
|
||||
} from "@ente/shared/components/Container";
|
||||
import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined";
|
||||
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
|
||||
import { Tooltip, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { AppContext } from "pages/_app";
|
||||
import React from "react";
|
||||
import { WatchMapping } from "types/watchFolder";
|
||||
import { EntryContainer } from "../styledComponents";
|
||||
|
||||
import { UPLOAD_STRATEGY } from "constants/upload";
|
||||
import { EntryHeading } from "./entryHeading";
|
||||
import MappingEntryOptions from "./mappingEntryOptions";
|
||||
|
||||
interface Iprops {
|
||||
mapping: WatchMapping;
|
||||
handleRemoveMapping: (mapping: WatchMapping) => void;
|
||||
}
|
||||
|
||||
export function MappingEntry({ mapping, handleRemoveMapping }: Iprops) {
|
||||
const appContext = React.useContext(AppContext);
|
||||
|
||||
const stopWatching = () => {
|
||||
handleRemoveMapping(mapping);
|
||||
};
|
||||
|
||||
const confirmStopWatching = () => {
|
||||
appContext.setDialogMessage({
|
||||
title: t("STOP_WATCHING_FOLDER"),
|
||||
content: t("STOP_WATCHING_DIALOG_MESSAGE"),
|
||||
close: {
|
||||
text: t("CANCEL"),
|
||||
variant: "secondary",
|
||||
},
|
||||
proceed: {
|
||||
action: stopWatching,
|
||||
text: t("YES_STOP"),
|
||||
variant: "critical",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SpaceBetweenFlex>
|
||||
<HorizontalFlex>
|
||||
{mapping &&
|
||||
mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
|
||||
<Tooltip title={t("UPLOADED_TO_SINGLE_COLLECTION")}>
|
||||
<FolderOpenIcon />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={t("UPLOADED_TO_SEPARATE_COLLECTIONS")}>
|
||||
<FolderCopyOutlinedIcon />
|
||||
</Tooltip>
|
||||
)}
|
||||
<EntryContainer>
|
||||
<EntryHeading mapping={mapping} />
|
||||
<Typography color="text.muted" variant="small">
|
||||
{mapping.folderPath}
|
||||
</Typography>
|
||||
</EntryContainer>
|
||||
</HorizontalFlex>
|
||||
<MappingEntryOptions confirmStopWatching={confirmStopWatching} />
|
||||
</SpaceBetweenFlex>
|
||||
);
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import { t } from "i18next";
|
||||
|
||||
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
||||
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
|
||||
import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined";
|
||||
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||
|
||||
interface Iprops {
|
||||
confirmStopWatching: () => void;
|
||||
}
|
||||
|
||||
export default function MappingEntryOptions({ confirmStopWatching }: Iprops) {
|
||||
return (
|
||||
<OverflowMenu
|
||||
menuPaperProps={{
|
||||
sx: {
|
||||
backgroundColor: (theme) =>
|
||||
theme.colors.background.elevated2,
|
||||
},
|
||||
}}
|
||||
ariaControls={"watch-mapping-option"}
|
||||
triggerButtonIcon={<MoreHorizIcon />}
|
||||
>
|
||||
<OverflowMenuOption
|
||||
color="critical"
|
||||
onClick={confirmStopWatching}
|
||||
startIcon={<DoNotDisturbOutlinedIcon />}
|
||||
>
|
||||
{t("STOP_WATCHING")}
|
||||
</OverflowMenuOption>
|
||||
</OverflowMenu>
|
||||
);
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { WatchMapping } from "types/watchFolder";
|
||||
import { MappingEntry } from "../mappingEntry";
|
||||
import { MappingsContainer } from "../styledComponents";
|
||||
import { NoMappingsContent } from "./noMappingsContent/noMappingsContent";
|
||||
interface Iprops {
|
||||
mappings: WatchMapping[];
|
||||
handleRemoveWatchMapping: (value: WatchMapping) => void;
|
||||
}
|
||||
|
||||
export function MappingList({ mappings, handleRemoveWatchMapping }: Iprops) {
|
||||
return mappings.length === 0 ? (
|
||||
<NoMappingsContent />
|
||||
) : (
|
||||
<MappingsContainer>
|
||||
{mappings.map((mapping) => {
|
||||
return (
|
||||
<MappingEntry
|
||||
key={mapping.rootFolderName}
|
||||
mapping={mapping}
|
||||
handleRemoveMapping={handleRemoveWatchMapping}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MappingsContainer>
|
||||
);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import CheckIcon from "@mui/icons-material/Check";
|
||||
|
||||
export function CheckmarkIcon() {
|
||||
return (
|
||||
<CheckIcon
|
||||
fontSize="small"
|
||||
sx={{
|
||||
display: "inline",
|
||||
fontSize: "15px",
|
||||
|
||||
color: (theme) => theme.palette.secondary.main,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import { Stack, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
|
||||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
import { NoMappingsContainer } from "../../styledComponents";
|
||||
import { CheckmarkIcon } from "./checkmarkIcon";
|
||||
|
||||
export function NoMappingsContent() {
|
||||
return (
|
||||
<NoMappingsContainer>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="large" fontWeight={"bold"}>
|
||||
{t("NO_FOLDERS_ADDED")}
|
||||
</Typography>
|
||||
<Typography py={0.5} variant={"small"} color="text.muted">
|
||||
{t("FOLDERS_AUTOMATICALLY_MONITORED")}
|
||||
</Typography>
|
||||
<Typography variant={"small"} color="text.muted">
|
||||
<FlexWrapper gap={1}>
|
||||
<CheckmarkIcon />
|
||||
{t("UPLOAD_NEW_FILES_TO_ENTE")}
|
||||
</FlexWrapper>
|
||||
</Typography>
|
||||
<Typography variant={"small"} color="text.muted">
|
||||
<FlexWrapper gap={1}>
|
||||
<CheckmarkIcon />
|
||||
{t("REMOVE_DELETED_FILES_FROM_ENTE")}
|
||||
</FlexWrapper>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</NoMappingsContainer>
|
||||
);
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import { Box } from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
export const MappingsContainer = styled(Box)(() => ({
|
||||
height: "278px",
|
||||
overflow: "auto",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "4px",
|
||||
},
|
||||
}));
|
||||
|
||||
export const NoMappingsContainer = styled(VerticallyCentered)({
|
||||
textAlign: "left",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: "32px",
|
||||
});
|
||||
|
||||
export const EntryContainer = styled(Box)({
|
||||
marginLeft: "12px",
|
||||
marginRight: "6px",
|
||||
marginBottom: "12px",
|
||||
});
|
|
@ -7,7 +7,6 @@ import { CustomError } from "@ente/shared/error";
|
|||
import { Events, eventBus } from "@ente/shared/events";
|
||||
import { Remote } from "comlink";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import isElectron from "is-electron";
|
||||
import { EnteFile } from "types/file";
|
||||
import {
|
||||
generateStreamFromArrayBuffer,
|
||||
|
@ -89,11 +88,12 @@ class DownloadManagerImpl {
|
|||
e,
|
||||
);
|
||||
}
|
||||
try {
|
||||
if (isElectron()) this.fileCache = await openCache("files");
|
||||
} catch (e) {
|
||||
log.error("Failed to open file cache, will continue without it", e);
|
||||
}
|
||||
// TODO (MR): Revisit full file caching cf disk space usage
|
||||
// try {
|
||||
// if (isElectron()) this.fileCache = await openCache("files");
|
||||
// } catch (e) {
|
||||
// log.error("Failed to open file cache, will continue without it", e);
|
||||
// }
|
||||
this.cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
this.ready = true;
|
||||
eventBus.on(Events.LOGOUT, this.logoutHandler.bind(this), this);
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
mergeMetadata,
|
||||
} from "utils/file";
|
||||
import { safeDirectoryName, safeFileName } from "utils/native-fs";
|
||||
import { writeStream } from "utils/native-stream";
|
||||
import { getAllLocalCollections } from "../collectionService";
|
||||
import downloadManager from "../download";
|
||||
import { getAllLocalFiles } from "../fileService";
|
||||
|
@ -884,7 +885,7 @@ class ExportService {
|
|||
try {
|
||||
const exportRecord = await this.getExportRecord(folder);
|
||||
const newRecord: ExportRecord = { ...exportRecord, ...newData };
|
||||
await ensureElectron().saveFileToDisk(
|
||||
await ensureElectron().fs.writeFile(
|
||||
`${folder}/${exportRecordFileName}`,
|
||||
JSON.stringify(newRecord, null, 2),
|
||||
);
|
||||
|
@ -907,8 +908,7 @@ class ExportService {
|
|||
if (!(await fs.exists(exportRecordJSONPath))) {
|
||||
return this.createEmptyExportRecord(exportRecordJSONPath);
|
||||
}
|
||||
const recordFile =
|
||||
await electron.readTextFile(exportRecordJSONPath);
|
||||
const recordFile = await fs.readTextFile(exportRecordJSONPath);
|
||||
try {
|
||||
return JSON.parse(recordFile);
|
||||
} catch (e) {
|
||||
|
@ -993,7 +993,7 @@ class ExportService {
|
|||
fileExportName,
|
||||
file,
|
||||
);
|
||||
await electron.saveStreamToDisk(
|
||||
await writeStream(
|
||||
`${collectionExportPath}/${fileExportName}`,
|
||||
updatedFileStream,
|
||||
);
|
||||
|
@ -1044,7 +1044,7 @@ class ExportService {
|
|||
imageExportName,
|
||||
file,
|
||||
);
|
||||
await electron.saveStreamToDisk(
|
||||
await writeStream(
|
||||
`${collectionExportPath}/${imageExportName}`,
|
||||
imageStream,
|
||||
);
|
||||
|
@ -1056,7 +1056,7 @@ class ExportService {
|
|||
file,
|
||||
);
|
||||
try {
|
||||
await electron.saveStreamToDisk(
|
||||
await writeStream(
|
||||
`${collectionExportPath}/${videoExportName}`,
|
||||
videoStream,
|
||||
);
|
||||
|
@ -1077,7 +1077,7 @@ class ExportService {
|
|||
fileExportName: string,
|
||||
file: EnteFile,
|
||||
) {
|
||||
await ensureElectron().saveFileToDisk(
|
||||
await ensureElectron().fs.writeFile(
|
||||
getFileMetadataExportPath(collectionExportPath, fileExportName),
|
||||
getGoogleLikeMetadataFile(fileExportName, file),
|
||||
);
|
||||
|
@ -1106,7 +1106,7 @@ class ExportService {
|
|||
|
||||
private createEmptyExportRecord = async (exportRecordJSONPath: string) => {
|
||||
const exportRecord: ExportRecord = NULL_EXPORT_RECORD;
|
||||
await ensureElectron().saveFileToDisk(
|
||||
await ensureElectron().fs.writeFile(
|
||||
exportRecordJSONPath,
|
||||
JSON.stringify(exportRecord, null, 2),
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
getPublicCollectionUID,
|
||||
} from "services/publicCollectionService";
|
||||
import { getDisableCFUploadProxyFlag } from "services/userService";
|
||||
import watchFolderService from "services/watchFolder/watchFolderService";
|
||||
import watchFolderService from "services/watch";
|
||||
import { Collection } from "types/collection";
|
||||
import { EncryptedEnteFile, EnteFile } from "types/file";
|
||||
import { SetFiles } from "types/gallery";
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* @file Interface with the Node.js layer of our desktop app to provide the
|
||||
* watch folders functionality.
|
||||
*/
|
||||
|
||||
import { ensureElectron } from "@/next/electron";
|
||||
import log from "@/next/log";
|
||||
import { UPLOAD_RESULT, UPLOAD_STRATEGY } from "constants/upload";
|
||||
|
@ -12,17 +17,11 @@ import {
|
|||
WatchMappingSyncedFile,
|
||||
} from "types/watchFolder";
|
||||
import { groupFilesBasedOnCollectionID } from "utils/file";
|
||||
import { getValidFilesToUpload } from "utils/watch";
|
||||
import { removeFromCollection } from "../collectionService";
|
||||
import { getLocalFiles } from "../fileService";
|
||||
import { getParentFolderName } from "./utils";
|
||||
import {
|
||||
diskFileAddedCallback,
|
||||
diskFileRemovedCallback,
|
||||
diskFolderRemovedCallback,
|
||||
} from "./watchFolderEventHandlers";
|
||||
import { isSystemFile } from "utils/upload";
|
||||
import { removeFromCollection } from "./collectionService";
|
||||
import { getLocalFiles } from "./fileService";
|
||||
|
||||
class watchFolderService {
|
||||
class WatchFolderService {
|
||||
private eventQueue: EventQueueItem[] = [];
|
||||
private currentEvent: EventQueueItem;
|
||||
private currentlySyncedMapping: WatchMapping;
|
||||
|
@ -196,12 +195,9 @@ class watchFolderService {
|
|||
}
|
||||
}
|
||||
|
||||
async removeWatchMapping(folderPath: string) {
|
||||
try {
|
||||
await ensureElectron().removeWatchMapping(folderPath);
|
||||
} catch (e) {
|
||||
log.error("error while removing watch mapping", e);
|
||||
}
|
||||
async mappingsAfterRemovingFolder(folderPath: string) {
|
||||
await ensureElectron().removeWatchMapping(folderPath);
|
||||
return await this.getWatchMappings();
|
||||
}
|
||||
|
||||
async getWatchMappings(): Promise<WatchMapping[]> {
|
||||
|
@ -641,4 +637,104 @@ class watchFolderService {
|
|||
}
|
||||
}
|
||||
|
||||
export default new watchFolderService();
|
||||
const watchFolderService = new WatchFolderService();
|
||||
|
||||
export default watchFolderService;
|
||||
|
||||
const getParentFolderName = (filePath: string) => {
|
||||
const folderPath = filePath.substring(0, filePath.lastIndexOf("/"));
|
||||
const folderName = folderPath.substring(folderPath.lastIndexOf("/") + 1);
|
||||
return folderName;
|
||||
};
|
||||
|
||||
async function diskFileAddedCallback(file: ElectronFile) {
|
||||
try {
|
||||
const collectionNameAndFolderPath =
|
||||
await watchFolderService.getCollectionNameAndFolderPath(file.path);
|
||||
|
||||
if (!collectionNameAndFolderPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { collectionName, folderPath } = collectionNameAndFolderPath;
|
||||
|
||||
const event: EventQueueItem = {
|
||||
type: "upload",
|
||||
collectionName,
|
||||
folderPath,
|
||||
files: [file],
|
||||
};
|
||||
watchFolderService.pushEvent(event);
|
||||
log.info(
|
||||
`added (upload) to event queue, collectionName:${event.collectionName} folderPath:${event.folderPath}, filesCount: ${event.files.length}`,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("error while calling diskFileAddedCallback", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function diskFileRemovedCallback(filePath: string) {
|
||||
try {
|
||||
const collectionNameAndFolderPath =
|
||||
await watchFolderService.getCollectionNameAndFolderPath(filePath);
|
||||
|
||||
if (!collectionNameAndFolderPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { collectionName, folderPath } = collectionNameAndFolderPath;
|
||||
|
||||
const event: EventQueueItem = {
|
||||
type: "trash",
|
||||
collectionName,
|
||||
folderPath,
|
||||
paths: [filePath],
|
||||
};
|
||||
watchFolderService.pushEvent(event);
|
||||
log.info(
|
||||
`added (trash) to event queue collectionName:${event.collectionName} folderPath:${event.folderPath} , pathsCount: ${event.paths.length}`,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("error while calling diskFileRemovedCallback", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function diskFolderRemovedCallback(folderPath: string) {
|
||||
try {
|
||||
const mappings = await watchFolderService.getWatchMappings();
|
||||
const mapping = mappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
if (!mapping) {
|
||||
log.info(`folder not found in mappings, ${folderPath}`);
|
||||
throw Error(`Watch mapping not found`);
|
||||
}
|
||||
watchFolderService.pushTrashedDir(folderPath);
|
||||
log.info(`added trashedDir, ${folderPath}`);
|
||||
} catch (e) {
|
||||
log.error("error while calling diskFolderRemovedCallback", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function getValidFilesToUpload(
|
||||
files: ElectronFile[],
|
||||
mapping: WatchMapping,
|
||||
) {
|
||||
const uniqueFilePaths = new Set<string>();
|
||||
return files.filter((file) => {
|
||||
if (!isSystemFile(file) && !isSyncedOrIgnoredFile(file, mapping)) {
|
||||
if (!uniqueFilePaths.has(file.path)) {
|
||||
uniqueFilePaths.add(file.path);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function isSyncedOrIgnoredFile(file: ElectronFile, mapping: WatchMapping) {
|
||||
return (
|
||||
mapping.ignoredFiles.includes(file.path) ||
|
||||
mapping.syncedFiles.find((f) => f.path === file.path)
|
||||
);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export const getParentFolderName = (filePath: string) => {
|
||||
const folderPath = filePath.substring(0, filePath.lastIndexOf("/"));
|
||||
const folderName = folderPath.substring(folderPath.lastIndexOf("/") + 1);
|
||||
return folderName;
|
||||
};
|
|
@ -1,73 +0,0 @@
|
|||
import log from "@/next/log";
|
||||
import { ElectronFile } from "types/upload";
|
||||
import { EventQueueItem } from "types/watchFolder";
|
||||
import watchFolderService from "./watchFolderService";
|
||||
|
||||
export async function diskFileAddedCallback(file: ElectronFile) {
|
||||
try {
|
||||
const collectionNameAndFolderPath =
|
||||
await watchFolderService.getCollectionNameAndFolderPath(file.path);
|
||||
|
||||
if (!collectionNameAndFolderPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { collectionName, folderPath } = collectionNameAndFolderPath;
|
||||
|
||||
const event: EventQueueItem = {
|
||||
type: "upload",
|
||||
collectionName,
|
||||
folderPath,
|
||||
files: [file],
|
||||
};
|
||||
watchFolderService.pushEvent(event);
|
||||
log.info(
|
||||
`added (upload) to event queue, collectionName:${event.collectionName} folderPath:${event.folderPath}, filesCount: ${event.files.length}`,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("error while calling diskFileAddedCallback", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function diskFileRemovedCallback(filePath: string) {
|
||||
try {
|
||||
const collectionNameAndFolderPath =
|
||||
await watchFolderService.getCollectionNameAndFolderPath(filePath);
|
||||
|
||||
if (!collectionNameAndFolderPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { collectionName, folderPath } = collectionNameAndFolderPath;
|
||||
|
||||
const event: EventQueueItem = {
|
||||
type: "trash",
|
||||
collectionName,
|
||||
folderPath,
|
||||
paths: [filePath],
|
||||
};
|
||||
watchFolderService.pushEvent(event);
|
||||
log.info(
|
||||
`added (trash) to event queue collectionName:${event.collectionName} folderPath:${event.folderPath} , pathsCount: ${event.paths.length}`,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("error while calling diskFileRemovedCallback", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function diskFolderRemovedCallback(folderPath: string) {
|
||||
try {
|
||||
const mappings = await watchFolderService.getWatchMappings();
|
||||
const mapping = mappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
if (!mapping) {
|
||||
log.info(`folder not found in mappings, ${folderPath}`);
|
||||
throw Error(`Watch mapping not found`);
|
||||
}
|
||||
watchFolderService.pushTrashedDir(folderPath);
|
||||
log.info(`added trashedDir, ${folderPath}`);
|
||||
} catch (e) {
|
||||
log.error("error while calling diskFolderRemovedCallback", e);
|
||||
}
|
||||
}
|
|
@ -53,6 +53,7 @@ import { VISIBILITY_STATE } from "types/magicMetadata";
|
|||
import { FileTypeInfo } from "types/upload";
|
||||
import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata";
|
||||
import { safeFileName } from "utils/native-fs";
|
||||
import { writeStream } from "utils/native-stream";
|
||||
|
||||
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
|
||||
|
||||
|
@ -798,55 +799,47 @@ async function downloadFileDesktop(
|
|||
electron: Electron,
|
||||
fileReader: FileReader,
|
||||
file: EnteFile,
|
||||
downloadPath: string,
|
||||
downloadDir: string,
|
||||
) {
|
||||
const fileStream = (await DownloadManager.getFile(
|
||||
const fs = electron.fs;
|
||||
const stream = (await DownloadManager.getFile(
|
||||
file,
|
||||
)) as ReadableStream<Uint8Array>;
|
||||
const updatedFileStream = await getUpdatedEXIFFileForDownload(
|
||||
const updatedStream = await getUpdatedEXIFFileForDownload(
|
||||
fileReader,
|
||||
file,
|
||||
fileStream,
|
||||
stream,
|
||||
);
|
||||
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const fileBlob = await new Response(updatedFileStream).blob();
|
||||
const fileBlob = await new Response(updatedStream).blob();
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const imageExportName = await safeFileName(
|
||||
downloadPath,
|
||||
downloadDir,
|
||||
livePhoto.imageNameTitle,
|
||||
electron.fs.exists,
|
||||
fs.exists,
|
||||
);
|
||||
const imageStream = generateStreamFromArrayBuffer(livePhoto.image);
|
||||
await electron.saveStreamToDisk(
|
||||
`${downloadPath}/${imageExportName}`,
|
||||
imageStream,
|
||||
);
|
||||
await writeStream(`${downloadDir}/${imageExportName}`, imageStream);
|
||||
try {
|
||||
const videoExportName = await safeFileName(
|
||||
downloadPath,
|
||||
downloadDir,
|
||||
livePhoto.videoNameTitle,
|
||||
electron.fs.exists,
|
||||
fs.exists,
|
||||
);
|
||||
const videoStream = generateStreamFromArrayBuffer(livePhoto.video);
|
||||
await electron.saveStreamToDisk(
|
||||
`${downloadPath}/${videoExportName}`,
|
||||
videoStream,
|
||||
);
|
||||
await writeStream(`${downloadDir}/${videoExportName}`, videoStream);
|
||||
} catch (e) {
|
||||
await electron.fs.rm(`${downloadPath}/${imageExportName}`);
|
||||
await fs.rm(`${downloadDir}/${imageExportName}`);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
const fileExportName = await safeFileName(
|
||||
downloadPath,
|
||||
downloadDir,
|
||||
file.metadata.title,
|
||||
electron.fs.exists,
|
||||
);
|
||||
await electron.saveStreamToDisk(
|
||||
`${downloadPath}/${fileExportName}`,
|
||||
updatedFileStream,
|
||||
fs.exists,
|
||||
);
|
||||
await writeStream(`${downloadDir}/${fileExportName}`, updatedStream);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
58
web/apps/photos/src/utils/native-stream.ts
Normal file
58
web/apps/photos/src/utils/native-stream.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* @file Streaming IPC communication with the Node.js layer of our desktop app.
|
||||
*
|
||||
* NOTE: These functions only work when we're running in our desktop app.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Write the given stream to a file on the local machine.
|
||||
*
|
||||
* **This only works when we're running in our desktop app**. It uses the
|
||||
* "stream://" protocol handler exposed by our custom code in the Node.js layer.
|
||||
* See: [Note: IPC streams].
|
||||
*
|
||||
* @param path The path on the local machine where to write the file to.
|
||||
* @param stream The stream which should be written into the file.
|
||||
* */
|
||||
export const writeStream = async (path: string, stream: ReadableStream) => {
|
||||
// TODO(MR): This doesn't currently work.
|
||||
//
|
||||
// Not sure what I'm doing wrong here; I've opened an issue upstream
|
||||
// https://github.com/electron/electron/issues/41872
|
||||
//
|
||||
// A gist with a minimal reproduction
|
||||
// https://gist.github.com/mnvr/e08d9f4876fb8400b7615347b4d268eb
|
||||
//
|
||||
// Meanwhile, write the complete body in one go (this'll eventually run into
|
||||
// memory failures with large files - just a temporary stopgap to get the
|
||||
// code to work).
|
||||
|
||||
/*
|
||||
// The duplex parameter needs to be set to 'half' when streaming requests.
|
||||
//
|
||||
// Currently browsers, and specifically in our case, since this code runs
|
||||
// only within our desktop (Electron) app, Chromium, don't support 'full'
|
||||
// duplex mode (i.e. streaming both the request and the response).
|
||||
// https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests
|
||||
const req = new Request(`stream://write${path}`, {
|
||||
// GET can't have a body
|
||||
method: "POST",
|
||||
body: stream,
|
||||
// @ts-expect-error TypeScript's libdom.d.ts does not include the
|
||||
// "duplex" parameter, e.g. see
|
||||
// https://github.com/node-fetch/node-fetch/issues/1769.
|
||||
duplex: "half",
|
||||
});
|
||||
*/
|
||||
|
||||
const req = new Request(`stream://write${path}`, {
|
||||
method: "POST",
|
||||
body: await new Response(stream).blob(),
|
||||
});
|
||||
|
||||
const res = await fetch(req);
|
||||
if (!res.ok)
|
||||
throw new Error(
|
||||
`Failed to write stream to ${path}: HTTP ${res.status}`,
|
||||
);
|
||||
};
|
|
@ -1,26 +0,0 @@
|
|||
import { ElectronFile } from "types/upload";
|
||||
import { WatchMapping } from "types/watchFolder";
|
||||
import { isSystemFile } from "utils/upload";
|
||||
|
||||
function isSyncedOrIgnoredFile(file: ElectronFile, mapping: WatchMapping) {
|
||||
return (
|
||||
mapping.ignoredFiles.includes(file.path) ||
|
||||
mapping.syncedFiles.find((f) => f.path === file.path)
|
||||
);
|
||||
}
|
||||
|
||||
export function getValidFilesToUpload(
|
||||
files: ElectronFile[],
|
||||
mapping: WatchMapping,
|
||||
) {
|
||||
const uniqueFilePaths = new Set<string>();
|
||||
return files.filter((file) => {
|
||||
if (!isSystemFile(file) && !isSyncedOrIgnoredFile(file, mapping)) {
|
||||
if (!uniqueFilePaths.has(file.path)) {
|
||||
uniqueFilePaths.add(file.path);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
|
@ -2,8 +2,8 @@
|
|||
"HERO_SLIDE_1_TITLE": "<div>Private Sicherungen</div><div>für deine Erinnerungen</div>",
|
||||
"HERO_SLIDE_1": "Standardmäßig Ende-zu-Ende verschlüsselt",
|
||||
"HERO_SLIDE_2_TITLE": "<div>Sicher gespeichert</div><div>in einem Luftschutzbunker</div>",
|
||||
"HERO_SLIDE_2": "Entwickelt um zu bewahren",
|
||||
"HERO_SLIDE_3_TITLE": "<div>Verfügbar</div><div> überall</div>",
|
||||
"HERO_SLIDE_2": "Entwickelt um zu überleben",
|
||||
"HERO_SLIDE_3_TITLE": "<div>Überall</div><div> verfügbar</div>",
|
||||
"HERO_SLIDE_3": "Android, iOS, Web, Desktop",
|
||||
"LOGIN": "Anmelden",
|
||||
"SIGN_UP": "Registrieren",
|
||||
|
@ -168,7 +168,7 @@
|
|||
"UPDATE_PAYMENT_METHOD": "Zahlungsmethode aktualisieren",
|
||||
"MONTHLY": "Monatlich",
|
||||
"YEARLY": "Jährlich",
|
||||
"update_subscription_title": "",
|
||||
"update_subscription_title": "Tarifänderung bestätigen",
|
||||
"UPDATE_SUBSCRIPTION_MESSAGE": "Sind Sie sicher, dass Sie Ihren Tarif ändern möchten?",
|
||||
"UPDATE_SUBSCRIPTION": "Plan ändern",
|
||||
"CANCEL_SUBSCRIPTION": "Abonnement kündigen",
|
||||
|
@ -278,15 +278,15 @@
|
|||
"LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Ihr Browser oder ein Addon blockiert Ente vor der Speicherung von Daten im lokalen Speicher. Bitte versuchen Sie, den Browser-Modus zu wechseln und die Seite neu zu laden.",
|
||||
"SEND_OTT": "OTP senden",
|
||||
"EMAIl_ALREADY_OWNED": "Diese E-Mail wird bereits verwendet",
|
||||
"ETAGS_BLOCKED": "",
|
||||
"LIVE_PHOTOS_DETECTED": "",
|
||||
"ETAGS_BLOCKED": "<p>Die folgenden Dateien konnten aufgrund deiner Browser-Konfiguration nicht hochgeladen werden.</p><p>Bitte deaktiviere alle Add-ons, die Ente daran hindern könnten, <code>eTags</code> zum Hochladen großer Dateien zu verwenden oder verwende unsere <a>Desktop-App</a> für ein zuverlässigeres Import-Erlebnis.</p>",
|
||||
"LIVE_PHOTOS_DETECTED": "Die Foto- und Videodateien deiner Live-Fotos wurden in einer einzigen Datei zusammengeführt",
|
||||
"RETRY_FAILED": "Fehlgeschlagene Uploads erneut probieren",
|
||||
"FAILED_UPLOADS": "Fehlgeschlagene Uploads ",
|
||||
"SKIPPED_FILES": "Ignorierte Uploads",
|
||||
"THUMBNAIL_GENERATION_FAILED_UPLOADS": "Das Vorschaubild konnte nicht erzeugt werden",
|
||||
"UNSUPPORTED_FILES": "Nicht unterstützte Dateien",
|
||||
"SUCCESSFUL_UPLOADS": "Erfolgreiche Uploads",
|
||||
"SKIPPED_INFO": "",
|
||||
"SKIPPED_INFO": "Diese wurden übersprungen, da es Dateien mit gleichen Namen im selben Album gibt",
|
||||
"UNSUPPORTED_INFO": "Ente unterstützt diese Dateiformate noch nicht",
|
||||
"BLOCKED_UPLOADS": "Blockierte Uploads",
|
||||
"INPROGRESS_METADATA_EXTRACTION": "In Bearbeitung",
|
||||
|
@ -315,20 +315,20 @@
|
|||
"REMOVE_FROM_COLLECTION": "Aus Album entfernen",
|
||||
"TRASH": "Papierkorb",
|
||||
"MOVE_TO_TRASH": "In Papierkorb verschieben",
|
||||
"TRASH_FILES_MESSAGE": "",
|
||||
"TRASH_FILE_MESSAGE": "",
|
||||
"TRASH_FILES_MESSAGE": "Ausgewählte Dateien werden aus allen Alben entfernt und in den Papierkorb verschoben.",
|
||||
"TRASH_FILE_MESSAGE": "Die Datei wird aus allen Alben entfernt und in den Papierkorb verschoben.",
|
||||
"DELETE_PERMANENTLY": "Dauerhaft löschen",
|
||||
"RESTORE": "Wiederherstellen",
|
||||
"RESTORE_TO_COLLECTION": "In Album wiederherstellen",
|
||||
"EMPTY_TRASH": "Papierkorb leeren",
|
||||
"EMPTY_TRASH_TITLE": "Papierkorb leeren?",
|
||||
"EMPTY_TRASH_MESSAGE": "",
|
||||
"EMPTY_TRASH_MESSAGE": "Diese Dateien werden dauerhaft aus Ihrem Ente-Konto gelöscht.",
|
||||
"LEAVE_SHARED_ALBUM": "Ja, verlassen",
|
||||
"LEAVE_ALBUM": "Album verlassen",
|
||||
"LEAVE_SHARED_ALBUM_TITLE": "Geteiltes Album verlassen?",
|
||||
"LEAVE_SHARED_ALBUM_MESSAGE": "",
|
||||
"LEAVE_SHARED_ALBUM_MESSAGE": "Du wirst das Album verlassen und es wird nicht mehr für dich sichtbar sein.",
|
||||
"NOT_FILE_OWNER": "Dateien in einem freigegebenen Album können nicht gelöscht werden",
|
||||
"CONFIRM_SELF_REMOVE_MESSAGE": "",
|
||||
"CONFIRM_SELF_REMOVE_MESSAGE": "Ausgewählte Elemente werden aus diesem Album entfernt. Elemente, die sich nur in diesem Album befinden, werden nach Unkategorisiert verschoben.",
|
||||
"CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Einige der Elemente, die du entfernst, wurden von anderen Nutzern hinzugefügt und du wirst den Zugriff auf sie verlieren.",
|
||||
"SORT_BY_CREATION_TIME_ASCENDING": "Ältestem",
|
||||
"SORT_BY_UPDATION_TIME_DESCENDING": "Zuletzt aktualisiert",
|
||||
|
@ -337,8 +337,8 @@
|
|||
"FIX_CREATION_TIME_IN_PROGRESS": "Zeit wird repariert",
|
||||
"CREATION_TIME_UPDATED": "Datei-Zeit aktualisiert",
|
||||
"UPDATE_CREATION_TIME_NOT_STARTED": "Wählen Sie die Option, die Sie verwenden möchten",
|
||||
"UPDATE_CREATION_TIME_COMPLETED": "",
|
||||
"UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "",
|
||||
"UPDATE_CREATION_TIME_COMPLETED": "Alle Dateien erfolgreich aktualisiert",
|
||||
"UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "Aktualisierung der Dateizeit für einige Dateien fehlgeschlagen, bitte versuche es erneut",
|
||||
"CAPTION_CHARACTER_LIMIT": "Maximal 5000 Zeichen",
|
||||
"DATE_TIME_ORIGINAL": "",
|
||||
"DATE_TIME_DIGITIZED": "",
|
||||
|
@ -358,10 +358,10 @@
|
|||
"participants_one": "1 Teilnehmer",
|
||||
"participants_other": "{{count, number}} Teilnehmer",
|
||||
"ADD_VIEWERS": "Betrachter hinzufügen",
|
||||
"CHANGE_PERMISSIONS_TO_VIEWER": "",
|
||||
"CHANGE_PERMISSIONS_TO_COLLABORATOR": "",
|
||||
"CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} wird nicht in der Lage sein, weitere Fotos zum Album</p> <p>hinzuzufügen. {{selectedEmail}} wird weiterhin die eigenen Fotos aus dem Album entfernen können</p>",
|
||||
"CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} wird Fotos zum Album hinzufügen können",
|
||||
"CONVERT_TO_VIEWER": "Ja, zu \"Beobachter\" ändern",
|
||||
"CONVERT_TO_COLLABORATOR": "",
|
||||
"CONVERT_TO_COLLABORATOR": "Ja, in Kollaborateur umwandeln",
|
||||
"CHANGE_PERMISSION": "Berechtigung ändern?",
|
||||
"REMOVE_PARTICIPANT": "Entfernen?",
|
||||
"CONFIRM_REMOVE": "Ja, entfernen",
|
||||
|
@ -408,11 +408,11 @@
|
|||
"STOP_ALL_UPLOADS_MESSAGE": "",
|
||||
"STOP_UPLOADS_HEADER": "Hochladen stoppen?",
|
||||
"YES_STOP_UPLOADS": "Ja, Hochladen stoppen",
|
||||
"STOP_DOWNLOADS_HEADER": "",
|
||||
"YES_STOP_DOWNLOADS": "",
|
||||
"STOP_ALL_DOWNLOADS_MESSAGE": "",
|
||||
"STOP_DOWNLOADS_HEADER": "Downloads anhalten?",
|
||||
"YES_STOP_DOWNLOADS": "Ja, Downloads anhalten",
|
||||
"STOP_ALL_DOWNLOADS_MESSAGE": "Bist du dir sicher, dass du alle laufenden Downloads anhalten möchtest?",
|
||||
"albums_one": "1 Album",
|
||||
"albums_other": "",
|
||||
"albums_other": "{{count, number}} Alben",
|
||||
"ALL_ALBUMS": "Alle Alben",
|
||||
"ALBUMS": "Alben",
|
||||
"ALL_HIDDEN_ALBUMS": "",
|
||||
|
@ -424,7 +424,7 @@
|
|||
"COPIED": "Kopiert",
|
||||
"WATCH_FOLDERS": "",
|
||||
"UPGRADE_NOW": "Jetzt upgraden",
|
||||
"RENEW_NOW": "",
|
||||
"RENEW_NOW": "Jetzt erneuern",
|
||||
"STORAGE": "Speicher",
|
||||
"USED": "verwendet",
|
||||
"YOU": "Sie",
|
||||
|
@ -432,10 +432,10 @@
|
|||
"FREE": "frei",
|
||||
"OF": "von",
|
||||
"WATCHED_FOLDERS": "",
|
||||
"NO_FOLDERS_ADDED": "",
|
||||
"NO_FOLDERS_ADDED": "Noch keine Ordner hinzugefügt!",
|
||||
"FOLDERS_AUTOMATICALLY_MONITORED": "",
|
||||
"UPLOAD_NEW_FILES_TO_ENTE": "",
|
||||
"REMOVE_DELETED_FILES_FROM_ENTE": "",
|
||||
"REMOVE_DELETED_FILES_FROM_ENTE": "Gelöschte Dateien aus Ente entfernen",
|
||||
"ADD_FOLDER": "Ordner hinzufügen",
|
||||
"STOP_WATCHING": "",
|
||||
"STOP_WATCHING_FOLDER": "",
|
||||
|
@ -455,48 +455,48 @@
|
|||
"CURRENT_USAGE": "Aktuelle Nutzung ist <strong>{{usage}}</strong>",
|
||||
"WEAK_DEVICE": "",
|
||||
"DRAG_AND_DROP_HINT": "",
|
||||
"CONFIRM_ACCOUNT_DELETION_MESSAGE": "",
|
||||
"CONFIRM_ACCOUNT_DELETION_MESSAGE": "Ihre hochgeladenen Daten werden zur Löschung vorgemerkt, und Ihr Konto wird endgültig gelöscht.<br/><br/>Dieser Vorgang kann nicht rückgängig gemacht werden.",
|
||||
"AUTHENTICATE": "Authentifizieren",
|
||||
"UPLOADED_TO_SINGLE_COLLECTION": "",
|
||||
"UPLOADED_TO_SEPARATE_COLLECTIONS": "",
|
||||
"NEVERMIND": "Egal",
|
||||
"UPDATE_AVAILABLE": "Neue Version verfügbar",
|
||||
"UPDATE_INSTALLABLE_MESSAGE": "",
|
||||
"UPDATE_INSTALLABLE_MESSAGE": "Eine neue Version von Ente ist für die Installation bereit.",
|
||||
"INSTALL_NOW": "Jetzt installieren",
|
||||
"INSTALL_ON_NEXT_LAUNCH": "Beim nächsten Start installieren",
|
||||
"UPDATE_AVAILABLE_MESSAGE": "",
|
||||
"DOWNLOAD_AND_INSTALL": "",
|
||||
"UPDATE_AVAILABLE_MESSAGE": "Eine neue Version von Ente wurde veröffentlicht, aber sie kann nicht automatisch heruntergeladen und installiert werden.",
|
||||
"DOWNLOAD_AND_INSTALL": "Herunterladen und installieren",
|
||||
"IGNORE_THIS_VERSION": "Diese Version ignorieren",
|
||||
"TODAY": "Heute",
|
||||
"YESTERDAY": "Gestern",
|
||||
"NAME_PLACEHOLDER": "Name...",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Alben können nicht aus Datei/Ordnermix erstellt werden",
|
||||
"ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "",
|
||||
"CHOSE_THEME": "",
|
||||
"ML_SEARCH": "",
|
||||
"CHOSE_THEME": "Design auswählen",
|
||||
"ML_SEARCH": "Gesichtserkennung",
|
||||
"ENABLE_ML_SEARCH_DESCRIPTION": "",
|
||||
"ML_MORE_DETAILS": "",
|
||||
"ENABLE_FACE_SEARCH": "",
|
||||
"ENABLE_FACE_SEARCH_TITLE": "",
|
||||
"ENABLE_FACE_SEARCH_DESCRIPTION": "",
|
||||
"ML_MORE_DETAILS": "Weitere Details",
|
||||
"ENABLE_FACE_SEARCH": "Gesichtserkennung aktivieren",
|
||||
"ENABLE_FACE_SEARCH_TITLE": "Gesichtserkennung aktivieren?",
|
||||
"ENABLE_FACE_SEARCH_DESCRIPTION": "<p>Wenn du die Gesichtserkennung aktivierst, wird Ente Gesichtsgeometrie aus deinen Fotos extrahieren. Dies wird auf deinem Gerät geschehen, und alle erzeugten biometrischen Daten werden Ende-zu-verschlüsselt.<p/><p><a>Bitte klicke hier für weitere Informationen über diese Funktion in unserer Datenschutzerklärung</a></p>",
|
||||
"DISABLE_BETA": "Beta deaktivieren",
|
||||
"DISABLE_FACE_SEARCH": "",
|
||||
"DISABLE_FACE_SEARCH_TITLE": "",
|
||||
"DISABLE_FACE_SEARCH": "Gesichtserkennung deaktivieren",
|
||||
"DISABLE_FACE_SEARCH_TITLE": "Gesichtserkennung deaktivieren?",
|
||||
"DISABLE_FACE_SEARCH_DESCRIPTION": "",
|
||||
"ADVANCED": "Erweitert",
|
||||
"FACE_SEARCH_CONFIRMATION": "",
|
||||
"LABS": "",
|
||||
"FACE_SEARCH_CONFIRMATION": "Ich verstehe und möchte Ente erlauben, Gesichtsgeometrie zu verarbeiten",
|
||||
"LABS": "Experimente",
|
||||
"YOURS": "",
|
||||
"PASSPHRASE_STRENGTH_WEAK": "Passwortstärke: Schwach",
|
||||
"PASSPHRASE_STRENGTH_MODERATE": "",
|
||||
"PASSPHRASE_STRENGTH_MODERATE": "Passwortstärke: Moderat",
|
||||
"PASSPHRASE_STRENGTH_STRONG": "Passwortstärke: Stark",
|
||||
"PREFERENCES": "Einstellungen",
|
||||
"LANGUAGE": "Sprache",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST": "",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST": "Ungültiges Exportverzeichnis",
|
||||
"EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "",
|
||||
"SUBSCRIPTION_VERIFICATION_ERROR": "",
|
||||
"SUBSCRIPTION_VERIFICATION_ERROR": "Verifizierung des Abonnements fehlgeschlagen",
|
||||
"STORAGE_UNITS": {
|
||||
"B": "",
|
||||
"B": "B",
|
||||
"KB": "KB",
|
||||
"MB": "MB",
|
||||
"GB": "GB",
|
||||
|
@ -520,8 +520,8 @@
|
|||
"PUBLIC_COLLECT_SUBTEXT": "",
|
||||
"STOP_EXPORT": "Stop",
|
||||
"EXPORT_PROGRESS": "",
|
||||
"MIGRATING_EXPORT": "",
|
||||
"RENAMING_COLLECTION_FOLDERS": "",
|
||||
"MIGRATING_EXPORT": "Vorbereiten...",
|
||||
"RENAMING_COLLECTION_FOLDERS": "Albumordner umbenennen...",
|
||||
"TRASHING_DELETED_FILES": "",
|
||||
"TRASHING_DELETED_COLLECTIONS": "",
|
||||
"CONTINUOUS_EXPORT": "",
|
||||
|
@ -536,12 +536,12 @@
|
|||
"NOT_LISTED": ""
|
||||
},
|
||||
"DELETE_ACCOUNT_FEEDBACK_LABEL": "",
|
||||
"DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "",
|
||||
"CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "",
|
||||
"DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback",
|
||||
"CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Ja, ich möchte dieses Konto und alle enthaltenen Daten endgültig und unwiderruflich löschen",
|
||||
"CONFIRM_DELETE_ACCOUNT": "Kontolöschung bestätigen",
|
||||
"FEEDBACK_REQUIRED": "",
|
||||
"FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "",
|
||||
"RECOVER_TWO_FACTOR": "",
|
||||
"FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "Was macht der andere Dienst besser?",
|
||||
"RECOVER_TWO_FACTOR": "Zwei-Faktor wiederherstellen",
|
||||
"at": "",
|
||||
"AUTH_NEXT": "Weiter",
|
||||
"AUTH_DOWNLOAD_MOBILE_APP": "",
|
||||
|
@ -556,48 +556,48 @@
|
|||
"SELECT_COLLECTION": "Album auswählen",
|
||||
"PIN_ALBUM": "Album anheften",
|
||||
"UNPIN_ALBUM": "Album lösen",
|
||||
"DOWNLOAD_COMPLETE": "",
|
||||
"DOWNLOADING_COLLECTION": "",
|
||||
"DOWNLOAD_FAILED": "",
|
||||
"DOWNLOAD_PROGRESS": "",
|
||||
"CHRISTMAS": "",
|
||||
"CHRISTMAS_EVE": "",
|
||||
"DOWNLOAD_COMPLETE": "Download abgeschlossen",
|
||||
"DOWNLOADING_COLLECTION": "Lade {{name}} herunter",
|
||||
"DOWNLOAD_FAILED": "Herunterladen fehlgeschlagen",
|
||||
"DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} Dateien",
|
||||
"CHRISTMAS": "Weihnachten",
|
||||
"CHRISTMAS_EVE": "Heiligabend",
|
||||
"NEW_YEAR": "",
|
||||
"NEW_YEAR_EVE": "",
|
||||
"IMAGE": "",
|
||||
"VIDEO": "",
|
||||
"LIVE_PHOTO": "",
|
||||
"CONVERT": "",
|
||||
"IMAGE": "Bild",
|
||||
"VIDEO": "Video",
|
||||
"LIVE_PHOTO": "Live-Foto",
|
||||
"CONVERT": "Konvertieren",
|
||||
"CONFIRM_EDITOR_CLOSE_MESSAGE": "",
|
||||
"CONFIRM_EDITOR_CLOSE_DESCRIPTION": "",
|
||||
"BRIGHTNESS": "",
|
||||
"CONTRAST": "",
|
||||
"SATURATION": "",
|
||||
"BLUR": "",
|
||||
"INVERT_COLORS": "",
|
||||
"ASPECT_RATIO": "",
|
||||
"SQUARE": "",
|
||||
"ROTATE_LEFT": "",
|
||||
"ROTATE_RIGHT": "",
|
||||
"FLIP_VERTICALLY": "",
|
||||
"FLIP_HORIZONTALLY": "",
|
||||
"BRIGHTNESS": "Helligkeit",
|
||||
"CONTRAST": "Kontrast",
|
||||
"SATURATION": "Sättigung",
|
||||
"BLUR": "Weichzeichnen",
|
||||
"INVERT_COLORS": "Farben invertieren",
|
||||
"ASPECT_RATIO": "Seitenverhältnis",
|
||||
"SQUARE": "Quadrat",
|
||||
"ROTATE_LEFT": "Nach links drehen",
|
||||
"ROTATE_RIGHT": "Nach rechts drehen",
|
||||
"FLIP_VERTICALLY": "Vertikal spiegeln",
|
||||
"FLIP_HORIZONTALLY": "Horizontal spiegeln",
|
||||
"DOWNLOAD_EDITED": "",
|
||||
"SAVE_A_COPY_TO_ENTE": "",
|
||||
"RESTORE_ORIGINAL": "",
|
||||
"TRANSFORM": "",
|
||||
"COLORS": "",
|
||||
"FLIP": "",
|
||||
"ROTATION": "",
|
||||
"RESET": "",
|
||||
"PHOTO_EDITOR": "",
|
||||
"SAVE_A_COPY_TO_ENTE": "Kopie in Ente speichern",
|
||||
"RESTORE_ORIGINAL": "Original wiederherstellen",
|
||||
"TRANSFORM": "Transformieren",
|
||||
"COLORS": "Farben",
|
||||
"FLIP": "Spiegeln",
|
||||
"ROTATION": "Drehen",
|
||||
"RESET": "Zurücksetzen",
|
||||
"PHOTO_EDITOR": "Foto-Editor",
|
||||
"FASTER_UPLOAD": "",
|
||||
"FASTER_UPLOAD_DESCRIPTION": "",
|
||||
"MAGIC_SEARCH_STATUS": "",
|
||||
"INDEXED_ITEMS": "",
|
||||
"CAST_ALBUM_TO_TV": "",
|
||||
"ENTER_CAST_PIN_CODE": "",
|
||||
"PAIR_DEVICE_TO_TV": "",
|
||||
"TV_NOT_FOUND": "",
|
||||
"INDEXED_ITEMS": "Indizierte Elemente",
|
||||
"CAST_ALBUM_TO_TV": "Album auf Fernseher wiedergeben",
|
||||
"ENTER_CAST_PIN_CODE": "Gib den Code auf dem Fernseher unten ein, um dieses Gerät zu koppeln.",
|
||||
"PAIR_DEVICE_TO_TV": "Geräte koppeln",
|
||||
"TV_NOT_FOUND": "Fernseher nicht gefunden. Hast du die PIN korrekt eingegeben?",
|
||||
"AUTO_CAST_PAIR": "",
|
||||
"AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "",
|
||||
"PAIR_WITH_PIN": "",
|
||||
|
@ -605,21 +605,21 @@
|
|||
"PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "",
|
||||
"VISIT_CAST_ENTE_IO": "",
|
||||
"CAST_AUTO_PAIR_FAILED": "",
|
||||
"FREEHAND": "",
|
||||
"FREEHAND": "Freihand",
|
||||
"APPLY_CROP": "",
|
||||
"PHOTO_EDIT_REQUIRED_TO_SAVE": "",
|
||||
"PASSKEYS": "",
|
||||
"DELETE_PASSKEY": "",
|
||||
"DELETE_PASSKEY_CONFIRMATION": "",
|
||||
"RENAME_PASSKEY": "",
|
||||
"ADD_PASSKEY": "",
|
||||
"ENTER_PASSKEY_NAME": "",
|
||||
"PASSKEYS_DESCRIPTION": "",
|
||||
"CREATED_AT": "",
|
||||
"PASSKEY_LOGIN_FAILED": "",
|
||||
"PASSKEY_LOGIN_URL_INVALID": "",
|
||||
"PASSKEY_LOGIN_ERRORED": "",
|
||||
"TRY_AGAIN": "",
|
||||
"PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "",
|
||||
"LOGIN_WITH_PASSKEY": ""
|
||||
"PASSKEYS": "Passkeys",
|
||||
"DELETE_PASSKEY": "Passkey löschen",
|
||||
"DELETE_PASSKEY_CONFIRMATION": "Bist du sicher, dass du diesen Passkey löschen willst? Dieser Vorgang ist nicht umkehrbar.",
|
||||
"RENAME_PASSKEY": "Passkey umbenennen",
|
||||
"ADD_PASSKEY": "Passkey hinzufügen",
|
||||
"ENTER_PASSKEY_NAME": "Passkey-Namen eingeben",
|
||||
"PASSKEYS_DESCRIPTION": "Passkeys sind ein moderner und sicherer zweiter Faktor für dein Ente-Konto. Sie nutzen die biometrische Authentifizierung des Geräts für Komfort und Sicherheit.",
|
||||
"CREATED_AT": "Erstellt am",
|
||||
"PASSKEY_LOGIN_FAILED": "Passkey-Anmeldung fehlgeschlagen",
|
||||
"PASSKEY_LOGIN_URL_INVALID": "Die Anmelde-URL ist ungültig.",
|
||||
"PASSKEY_LOGIN_ERRORED": "Ein Fehler trat auf beim Anmelden mit dem Passkey auf.",
|
||||
"TRY_AGAIN": "Erneut versuchen",
|
||||
"PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Folge den Schritten in deinem Browser, um mit dem Anmelden fortzufahren.",
|
||||
"LOGIN_WITH_PASSKEY": "Mit Passkey anmelden"
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"ENTER_NAME": "Ange namn",
|
||||
"PUBLIC_UPLOADER_NAME_MESSAGE": "",
|
||||
"ENTER_EMAIL": "Ange e-postadress",
|
||||
"EMAIL_ERROR": "",
|
||||
"EMAIL_ERROR": "Ange en giltig e-postadress",
|
||||
"REQUIRED": "",
|
||||
"EMAIL_SENT": "",
|
||||
"CHECK_INBOX": "",
|
||||
|
@ -80,7 +80,7 @@
|
|||
"DOWNLOAD_HIDDEN_ITEMS": "",
|
||||
"COPY_OPTION": "",
|
||||
"TOGGLE_FULLSCREEN": "",
|
||||
"ZOOM_IN_OUT": "",
|
||||
"ZOOM_IN_OUT": "Zooma in/ut",
|
||||
"PREVIOUS": "",
|
||||
"NEXT": "",
|
||||
"TITLE_PHOTOS": "",
|
||||
|
|
|
@ -59,11 +59,21 @@ const nextConfig = {
|
|||
GIT_SHA: gitSHA(),
|
||||
},
|
||||
|
||||
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
|
||||
// Customize the webpack configuration used by Next.js
|
||||
webpack: (config, { isServer }) => {
|
||||
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
|
||||
if (!isServer) {
|
||||
config.resolve.fallback.fs = false;
|
||||
}
|
||||
|
||||
// Suppress the warning "Critical dependency: require function is used
|
||||
// in a way in which dependencies cannot be statically extracted" when
|
||||
// import heic-convert.
|
||||
//
|
||||
// Upstream issue, which currently doesn't have a workaround.
|
||||
// https://github.com/catdad-experiments/libheif-js/issues/23
|
||||
config.ignoreWarnings = [{ module: /libheif-js/ }];
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -26,20 +26,6 @@ export interface DataStream {
|
|||
chunkCount: number;
|
||||
}
|
||||
|
||||
export interface WatchMappingSyncedFile {
|
||||
path: string;
|
||||
uploadedFileID: number;
|
||||
collectionID: number;
|
||||
}
|
||||
|
||||
export interface WatchMapping {
|
||||
rootFolderName: string;
|
||||
folderPath: string;
|
||||
uploadStrategy: UPLOAD_STRATEGY;
|
||||
syncedFiles: WatchMappingSyncedFile[];
|
||||
ignoredFiles: string[];
|
||||
}
|
||||
|
||||
export interface EventQueueItem {
|
||||
type: "upload" | "trash";
|
||||
folderPath: string;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
//
|
||||
// See [Note: types.ts <-> preload.ts <-> ipc.ts]
|
||||
|
||||
import type { ElectronFile, WatchMapping } from "./file";
|
||||
import type { ElectronFile } from "./file";
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
autoUpdatable: boolean;
|
||||
|
@ -188,6 +188,17 @@ export interface Electron {
|
|||
* Delete the file at {@link path}.
|
||||
*/
|
||||
rm: (path: string) => Promise<void>;
|
||||
|
||||
/** Read the string contents of a file at {@link path}. */
|
||||
readTextFile: (path: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Write a string to a file, replacing the file if it already exists.
|
||||
*
|
||||
* @param path The path of the file.
|
||||
* @param contents The string contents to write.
|
||||
*/
|
||||
writeFile: (path: string, contents: string) => Promise<void>;
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -287,25 +298,19 @@ export interface Electron {
|
|||
|
||||
removeWatchMapping: (folderPath: string) => Promise<void>;
|
||||
|
||||
getWatchMappings: () => Promise<WatchMapping[]>;
|
||||
getWatchMappings: () => Promise<FolderWatch[]>;
|
||||
|
||||
updateWatchMappingSyncedFiles: (
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
files: FolderWatch["syncedFiles"],
|
||||
) => Promise<void>;
|
||||
|
||||
updateWatchMappingIgnoredFiles: (
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
files: FolderWatch["ignoredFiles"],
|
||||
) => Promise<void>;
|
||||
|
||||
// - FS legacy
|
||||
saveStreamToDisk: (
|
||||
path: string,
|
||||
fileStream: ReadableStream,
|
||||
) => Promise<void>;
|
||||
saveFileToDisk: (path: string, contents: string) => Promise<void>;
|
||||
readTextFile: (path: string) => Promise<string>;
|
||||
isFolder: (dirPath: string) => Promise<boolean>;
|
||||
|
||||
// - Upload
|
||||
|
@ -327,3 +332,30 @@ export interface Electron {
|
|||
setToUploadCollection: (collectionName: string) => Promise<void>;
|
||||
getDirFiles: (dirPath: string) => Promise<ElectronFile[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A top level folder that was selected by the user for watching.
|
||||
*
|
||||
* The user can set up multiple such watches. Each of these can in turn be
|
||||
* syncing multiple on disk folders to one or more (dependening on the
|
||||
* {@link uploadStrategy}) Ente albums.
|
||||
*
|
||||
* This type is passed across the IPC boundary. It is persisted on the Node.js
|
||||
* side.
|
||||
*/
|
||||
export interface FolderWatch {
|
||||
rootFolderName: string;
|
||||
uploadStrategy: number;
|
||||
folderPath: string;
|
||||
syncedFiles: FolderWatchSyncedFile[];
|
||||
ignoredFiles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An on-disk file that was synced as part of a folder watch.
|
||||
*/
|
||||
export interface FolderWatchSyncedFile {
|
||||
path: string;
|
||||
uploadedFileID: number;
|
||||
collectionID: number;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
export async function sleep(time: number) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(null), time);
|
||||
});
|
||||
/**
|
||||
* Wait for {@link ms} milliseconds
|
||||
*
|
||||
* This function is a promisified `setTimeout`. It returns a promise that
|
||||
* resolves after {@link ms} milliseconds.
|
||||
*/
|
||||
export async function sleep(ms: number) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function downloadAsFile(filename: string, content: string) {
|
||||
|
|
Loading…
Add table
Reference in a new issue