diff --git a/auth/lib/l10n/arb/app_de.arb b/auth/lib/l10n/arb/app_de.arb index a7cd56043..f3ea23b51 100644 --- a/auth/lib/l10n/arb/app_de.arb +++ b/auth/lib/l10n/arb/app_de.arb @@ -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", diff --git a/auth/lib/l10n/arb/app_it.arb b/auth/lib/l10n/arb/app_it.arb index 473b3a2b3..e35fd11dc 100644 --- a/auth/lib/l10n/arb/app_it.arb +++ b/auth/lib/l10n/arb/app_it.arb @@ -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" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_pt.arb b/auth/lib/l10n/arb/app_pt.arb index 06e2c0bb4..b27a018fb 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -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." diff --git a/auth/lib/l10n/arb/app_sv.arb b/auth/lib/l10n/arb/app_sv.arb index d1dc39e05..cfb41d7bd 100644 --- a/auth/lib/l10n/arb/app_sv.arb +++ b/auth/lib/l10n/arb/app_sv.arb @@ -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", diff --git a/desktop/package.json b/desktop/package.json index 032953d8d..69d54f75b 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -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" }, diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 4837210c8..8526e2363 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -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); diff --git a/desktop/src/main/fs.ts b/desktop/src/main/fs.ts index 11ab36049..36de710c3 100644 --- a/desktop/src/main/fs.ts +++ b/desktop/src/main/fs.ts @@ -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; diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 36e13ec60..a5de4514f 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -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), ); }; diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index e9639a26f..2597bae60 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -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"; diff --git a/desktop/src/main/services/imageProcessor.ts b/desktop/src/main/services/imageProcessor.ts index 890e0e634..696119d80 100644 --- a/desktop/src/main/services/imageProcessor.ts +++ b/desktop/src/main/services/imageProcessor.ts @@ -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"; diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index 63fa75148..46af2552b 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -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 { diff --git a/desktop/src/main/services/ml.ts b/desktop/src/main/services/ml.ts index 10402db21..60e8241e1 100644 --- a/desktop/src/main/services/ml.ts +++ b/desktop/src/main/services/ml.ts @@ -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. diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 8a3414c58..1d466d415 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -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( diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts new file mode 100644 index 000000000..8ddb80dc6 --- /dev/null +++ b/desktop/src/main/stream.ts @@ -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); + }); + }); +}; diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 1a344e832..2749fa50d 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -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 => ipcRenderer.invoke("fsExists", path); @@ -110,6 +112,12 @@ const fsRmdir = (path: string): Promise => const fsRm = (path: string): Promise => ipcRenderer.invoke("fsRm", path); +const fsReadTextFile = (path: string): Promise => + ipcRenderer.invoke("fsReadTextFile", path); + +const fsWriteFile = (path: string, contents: string): Promise => + ipcRenderer.invoke("fsWriteFile", path, contents); + // - AUDIT below this // - Conversion @@ -212,34 +220,23 @@ const addWatchMapping = ( const removeWatchMapping = (folderPath: string): Promise => ipcRenderer.invoke("removeWatchMapping", folderPath); -const getWatchMappings = (): Promise => +const getWatchMappings = (): Promise => ipcRenderer.invoke("getWatchMappings"); const updateWatchMappingSyncedFiles = ( folderPath: string, - files: WatchMapping["syncedFiles"], + files: FolderWatch["syncedFiles"], ): Promise => ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files); const updateWatchMappingIgnoredFiles = ( folderPath: string, - files: WatchMapping["ignoredFiles"], + files: FolderWatch["ignoredFiles"], ): Promise => ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files); // - FS Legacy -const saveStreamToDisk = ( - path: string, - fileStream: ReadableStream, -): Promise => ipcRenderer.invoke("saveStreamToDisk", path, fileStream); - -const saveFileToDisk = (path: string, contents: string): Promise => - ipcRenderer.invoke("saveFileToDisk", path, contents); - -const readTextFile = (path: string): Promise => - ipcRenderer.invoke("readTextFile", path); - const isFolder = (dirPath: string): Promise => ipcRenderer.invoke("isFolder", dirPath); @@ -298,7 +295,8 @@ const getDirFiles = (dirPath: string): Promise => // 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 diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index 3dba231f2..3dae605a8 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -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; } -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 { diff --git a/desktop/yarn.lock b/desktop/yarn.lock index a4cc12cfe..a5b86f1eb 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -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== diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index cebeb1391..f30be877b 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -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 diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 09f9ded3c..102c04e6a 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -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 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 72c5ef5cf..89c492629 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -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", diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index d4b852540..bc406d7c3 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -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 Function(String) runBackgroundTask; @@ -66,39 +61,14 @@ class _EnteAppState extends State with WidgetsBindingObserver { void didChangeDependencies() { super.didChangeDependencies(); _checkForWidgetLaunch(); - hw.HomeWidget.widgetClicked.listen(_launchedFromWidget); } void _checkForWidgetLaunch() { - hw.HomeWidget.initiallyLaunchedFromHomeWidget().then(_launchedFromWidget); - } - - Future _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), ); } diff --git a/mobile/lib/generated/intl/messages_cs.dart b/mobile/lib/generated/intl/messages_cs.dart index 86ecd6893..8db8489d3 100644 --- a/mobile/lib/generated/intl/messages_cs.dart +++ b/mobile/lib/generated/intl/messages_cs.dart @@ -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": diff --git a/mobile/lib/generated/intl/messages_de.dart b/mobile/lib/generated/intl/messages_de.dart index 9005de2dc..442cae919 100644 --- a/mobile/lib/generated/intl/messages_de.dart +++ b/mobile/lib/generated/intl/messages_de.dart @@ -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": diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 59180d26c..eef309aa5 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -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": diff --git a/mobile/lib/generated/intl/messages_es.dart b/mobile/lib/generated/intl/messages_es.dart index 5bba2d9a0..a6294d4a4 100644 --- a/mobile/lib/generated/intl/messages_es.dart +++ b/mobile/lib/generated/intl/messages_es.dart @@ -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( diff --git a/mobile/lib/generated/intl/messages_fr.dart b/mobile/lib/generated/intl/messages_fr.dart index 5f21ec77b..82125afcc 100644 --- a/mobile/lib/generated/intl/messages_fr.dart +++ b/mobile/lib/generated/intl/messages_fr.dart @@ -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": diff --git a/mobile/lib/generated/intl/messages_it.dart b/mobile/lib/generated/intl/messages_it.dart index c20931418..e6db5b380 100644 --- a/mobile/lib/generated/intl/messages_it.dart +++ b/mobile/lib/generated/intl/messages_it.dart @@ -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( diff --git a/mobile/lib/generated/intl/messages_ko.dart b/mobile/lib/generated/intl/messages_ko.dart index 15b4acf26..c91d849f6 100644 --- a/mobile/lib/generated/intl/messages_ko.dart +++ b/mobile/lib/generated/intl/messages_ko.dart @@ -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": diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index a86943e50..5f2b0903b 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -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": diff --git a/mobile/lib/generated/intl/messages_no.dart b/mobile/lib/generated/intl/messages_no.dart index 294292a3d..0e5bd97b2 100644 --- a/mobile/lib/generated/intl/messages_no.dart +++ b/mobile/lib/generated/intl/messages_no.dart @@ -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": diff --git a/mobile/lib/generated/intl/messages_pl.dart b/mobile/lib/generated/intl/messages_pl.dart index fea153d71..b3a922b0a 100644 --- a/mobile/lib/generated/intl/messages_pl.dart +++ b/mobile/lib/generated/intl/messages_pl.dart @@ -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": diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index 3168451df..0747cfa82 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -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": diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index 73d2ae0f5..7f4c0accf 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -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( diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 89b71a76a..3fa9c2209 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -8553,6 +8553,16 @@ class S { args: [], ); } + + /// `Search` + String get search { + return Intl.message( + 'Search', + name: 'search', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_cs.arb b/mobile/lib/l10n/intl_cs.arb index 6b7a4933b..e7d374725 100644 --- a/mobile/lib/l10n/intl_cs.arb +++ b/mobile/lib/l10n/intl_cs.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_de.arb b/mobile/lib/l10n/intl_de.arb index 8bb844df3..0e5807e1e 100644 --- a/mobile/lib/l10n/intl_de.arb +++ b/mobile/lib/l10n/intl_de.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 9d1c7bcf9..7115c6950 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_es.arb b/mobile/lib/l10n/intl_es.arb index 7dff21036..6515371fa 100644 --- a/mobile/lib/l10n/intl_es.arb +++ b/mobile/lib/l10n/intl_es.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_fr.arb b/mobile/lib/l10n/intl_fr.arb index d44d093c1..1d8e5f6d3 100644 --- a/mobile/lib/l10n/intl_fr.arb +++ b/mobile/lib/l10n/intl_fr.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_it.arb b/mobile/lib/l10n/intl_it.arb index 9e884ed9e..c9655dd06 100644 --- a/mobile/lib/l10n/intl_it.arb +++ b/mobile/lib/l10n/intl_it.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ko.arb b/mobile/lib/l10n/intl_ko.arb index 6b7a4933b..e7d374725 100644 --- a/mobile/lib/l10n/intl_ko.arb +++ b/mobile/lib/l10n/intl_ko.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_nl.arb b/mobile/lib/l10n/intl_nl.arb index 120e4a207..98302c20c 100644 --- a/mobile/lib/l10n/intl_nl.arb +++ b/mobile/lib/l10n/intl_nl.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_no.arb b/mobile/lib/l10n/intl_no.arb index 0b777b353..8908eadb0 100644 --- a/mobile/lib/l10n/intl_no.arb +++ b/mobile/lib/l10n/intl_no.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pl.arb b/mobile/lib/l10n/intl_pl.arb index d358d4d2c..13d740614 100644 --- a/mobile/lib/l10n/intl_pl.arb +++ b/mobile/lib/l10n/intl_pl.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index 37b1041a9..765ce3e1d 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index 439643162..4a991c773 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -1211,5 +1211,6 @@ "invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。", "endpointUpdatedMessage": "端点更新成功", "customEndpoint": "已连接至 {endpoint}", - "createCollaborativeLink": "创建协作链接" + "createCollaborativeLink": "创建协作链接", + "search": "Search" } \ No newline at end of file diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index 607988a1b..75a40c99b 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -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; } diff --git a/mobile/lib/services/home_widget_service.dart b/mobile/lib/services/home_widget_service.dart index 33ef5d2bb..7b44310c2 100644 --- a/mobile/lib/services/home_widget_service.dart +++ b/mobile/lib/services/home_widget_service.dart @@ -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 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("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(); + } } diff --git a/mobile/lib/services/machine_learning/semantic_search/frameworks/ml_framework.dart b/mobile/lib/services/machine_learning/semantic_search/frameworks/ml_framework.dart index 3abd57db7..d3736d768 100644 --- a/mobile/lib/services/machine_learning/semantic_search/frameworks/ml_framework.dart +++ b/mobile/lib/services/machine_learning/semantic_search/frameworks/ml_framework.dart @@ -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 result) async { _logger.info("Connectivity changed to $result"); if (_state == InitializationState.waitingForNetwork && await _canDownload()) { @@ -135,9 +134,11 @@ abstract class MLFramework { } Future _canDownload() async { - final connectivityResult = await (Connectivity().checkConnectivity()); - return connectivityResult != ConnectivityResult.mobile || - shouldDownloadOverMobileData; + final List connections = + await (Connectivity().checkConnectivity()); + final bool isConnectedToMobile = + connections.contains(ConnectivityResult.mobile); + return !isConnectedToMobile || shouldDownloadOverMobileData; } } diff --git a/mobile/lib/services/sync_service.dart b/mobile/lib/services/sync_service.dart index 057e600df..873270f34 100644 --- a/mobile/lib/services/sync_service.dart +++ b/mobile/lib/services/sync_service.dart @@ -45,7 +45,9 @@ class SyncService { sync(); }); - Connectivity().onConnectivityChanged.listen((ConnectivityResult result) { + Connectivity() + .onConnectivityChanged + .listen((List result) { _logger.info("Connectivity change detected " + result.toString()); if (Configuration.instance.hasConfiguredAccount()) { sync(); diff --git a/mobile/lib/ui/viewer/search/search_widget.dart b/mobile/lib/ui/viewer/search/search_widget.dart index 2beaa1ec1..1c6c7b693 100644 --- a/mobile/lib/ui/viewer/search/search_widget.dart +++ b/mobile/lib/ui/viewer/search/search_widget.dart @@ -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 { 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 { 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 { 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( diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 3a2ff8c03..1b420a5b4 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -363,9 +363,10 @@ class FileUploader { if (isForceUpload) { return; } - final connectivityResult = await (Connectivity().checkConnectivity()); + final List connections = + await (Connectivity().checkConnectivity()); bool canUploadUnderCurrentNetworkConditions = true; - if (connectivityResult == ConnectivityResult.mobile) { + if (connections.any((element) => element == ConnectivityResult.mobile)) { canUploadUnderCurrentNetworkConditions = Configuration.instance.shouldBackupOverMobileData(); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index ccb0775d7..393dadc23 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -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: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 3be873cc5..89bee933b 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -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 diff --git a/server/configurations/local.yaml b/server/configurations/local.yaml index 0e456a53a..7785f5601 100644 --- a/server/configurations/local.yaml +++ b/server/configurations/local.yaml @@ -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: diff --git a/server/pkg/repo/public_collection.go b/server/pkg/repo/public_collection.go index d4aa81bf2..6c6106a77 100644 --- a/server/pkg/repo/public_collection.go +++ b/server/pkg/repo/public_collection.go @@ -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, diff --git a/web/apps/cast/src/components/PhotoAuditorium.tsx b/web/apps/cast/src/components/PhotoAuditorium.tsx index 0042dfe95..6aa2c3990 100644 --- a/web/apps/cast/src/components/PhotoAuditorium.tsx +++ b/web/apps/cast/src/components/PhotoAuditorium.tsx @@ -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(null); - + showNextSlide: () => void; +} +export const PhotoAuditorium: React.FC = ({ + 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 (
- { - setNextSlidePrerendered(true); - setPrerenderTime(Date.now()); + /> +
); -} +}; diff --git a/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx b/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx deleted file mode 100644 index 0042dfe95..000000000 --- a/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx +++ /dev/null @@ -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(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 ( -
-
- - { - setNextSlidePrerendered(true); - setPrerenderTime(Date.now()); - }} - /> -
-
- ); -} diff --git a/web/apps/cast/src/components/Theatre/VideoAuditorium.tsx b/web/apps/cast/src/components/Theatre/VideoAuditorium.tsx deleted file mode 100644 index 2bf5ed490..000000000 --- a/web/apps/cast/src/components/Theatre/VideoAuditorium.tsx +++ /dev/null @@ -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(null); - - useEffect(() => { - attemptPlay(); - }, [url, videoRef]); - - const attemptPlay = async () => { - if (videoRef.current) { - try { - await videoRef.current.play(); - } catch { - showNextSlide(); - } - } - }; - - return ( -
- -
- ); -} diff --git a/web/apps/cast/src/components/Theatre/index.tsx b/web/apps/cast/src/components/Theatre/index.tsx deleted file mode 100644 index f7cac9c54..000000000 --- a/web/apps/cast/src/components/Theatre/index.tsx +++ /dev/null @@ -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 ( - - ); - // case FILE_TYPE.VIDEO: - // return ( - // - // ); - } -} diff --git a/web/apps/cast/src/pages/slideshow.tsx b/web/apps/cast/src/pages/slideshow.tsx index 692e61154..774bbd4da 100644 --- a/web/apps/cast/src/pages/slideshow.tsx +++ b/web/apps/cast/src/pages/slideshow.tsx @@ -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(); export default function Slideshow() { - const [collectionFiles, setCollectionFiles] = useState([]); - - const [currentFile, setCurrentFile] = useState( - undefined, - ); - const [nextFile, setNextFile] = useState(undefined); - const [loading, setLoading] = useState(true); const [castToken, setCastToken] = useState(""); const [castCollection, setCastCollection] = useState< Collection | undefined - >(undefined); + >(); + const [collectionFiles, setCollectionFiles] = useState([]); + const [currentFileId, setCurrentFileId] = useState(); + const [currentFileURL, setCurrentFileURL] = useState(); + const [nextFileURL, setNextFileURL] = useState(); + + 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(""); - - 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 ; return ( - <> - - - - {loading && } - + ); } diff --git a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx index 8a5cb2c90..fdabffe84 100644 --- a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx +++ b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx @@ -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; diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx index ca52b9cad..d7cf151e6 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx @@ -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) => { diff --git a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx index 904eab747..c9c734cd9 100644 --- a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx +++ b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx @@ -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"; diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 4d81b1612..bb3d4fd9d 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -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 { diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx new file mode 100644 index 000000000..b5ff00b29 --- /dev/null +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -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 = ({ open, onClose }) => { + const [mappings, setMappings] = useState([]); + 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 ( + <> + + + {t("WATCHED_FOLDERS")} + + + + + + + + + + + ); +}; + +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 = ({ + mappings, + handleRemoveWatchMapping, +}) => { + return mappings.length === 0 ? ( + + ) : ( + + {mappings.map((mapping) => { + return ( + + ); + })} + + ); +}; + +const NoMappingsContent: React.FC = () => { + return ( + + + + {t("NO_FOLDERS_ADDED")} + + + {t("FOLDERS_AUTOMATICALLY_MONITORED")} + + + + + {t("UPLOAD_NEW_FILES_TO_ENTE")} + + + + + + {t("REMOVE_DELETED_FILES_FROM_ENTE")} + + + + + ); +}; + +const CheckmarkIcon: React.FC = () => { + return ( + theme.palette.secondary.main, + }} + /> + ); +}; + +interface MappingEntryProps { + mapping: WatchMapping; + handleRemoveMapping: (mapping: WatchMapping) => void; +} + +const MappingEntry: React.FC = ({ + 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 ( + + + {mapping && + mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? ( + + + + ) : ( + + + + )} + + + + {mapping.folderPath} + + + + + + ); +}; + +interface EntryHeadingProps { + mapping: WatchMapping; +} + +const EntryHeading: React.FC = ({ mapping }) => { + const appContext = useContext(AppContext); + return ( + + {mapping.rootFolderName} + {appContext.isFolderSyncRunning && + watchFolderService.isMappingSyncInProgress(mapping) && ( + + )} + + ); +}; + +interface MappingEntryOptionsProps { + confirmStopWatching: () => void; +} + +const MappingEntryOptions: React.FC = ({ + confirmStopWatching, +}) => { + return ( + + theme.colors.background.elevated2, + }, + }} + ariaControls={"watch-mapping-option"} + triggerButtonIcon={} + > + } + > + {t("STOP_WATCHING")} + + + ); +}; diff --git a/web/apps/photos/src/components/WatchFolder/index.tsx b/web/apps/photos/src/components/WatchFolder/index.tsx deleted file mode 100644 index 4ccfd4138..000000000 --- a/web/apps/photos/src/components/WatchFolder/index.tsx +++ /dev/null @@ -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([]); - 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 ( - <> - - - {t("WATCHED_FOLDERS")} - - - - - - - - - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingEntry/entryHeading.tsx b/web/apps/photos/src/components/WatchFolder/mappingEntry/entryHeading.tsx deleted file mode 100644 index b34e4277f..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingEntry/entryHeading.tsx +++ /dev/null @@ -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 ( - - {mapping.rootFolderName} - {appContext.isFolderSyncRunning && - watchFolderService.isMappingSyncInProgress(mapping) && ( - - )} - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx b/web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx deleted file mode 100644 index 819394699..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx +++ /dev/null @@ -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 ( - - - {mapping && - mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? ( - - - - ) : ( - - - - )} - - - - {mapping.folderPath} - - - - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingEntry/mappingEntryOptions.tsx b/web/apps/photos/src/components/WatchFolder/mappingEntry/mappingEntryOptions.tsx deleted file mode 100644 index 4f3cdc56d..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingEntry/mappingEntryOptions.tsx +++ /dev/null @@ -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 ( - - theme.colors.background.elevated2, - }, - }} - ariaControls={"watch-mapping-option"} - triggerButtonIcon={} - > - } - > - {t("STOP_WATCHING")} - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingList/index.tsx b/web/apps/photos/src/components/WatchFolder/mappingList/index.tsx deleted file mode 100644 index f2c7b781c..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingList/index.tsx +++ /dev/null @@ -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 ? ( - - ) : ( - - {mappings.map((mapping) => { - return ( - - ); - })} - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/checkmarkIcon.tsx b/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/checkmarkIcon.tsx deleted file mode 100644 index aedd79404..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/checkmarkIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import CheckIcon from "@mui/icons-material/Check"; - -export function CheckmarkIcon() { - return ( - theme.palette.secondary.main, - }} - /> - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/noMappingsContent.tsx b/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/noMappingsContent.tsx deleted file mode 100644 index a5af6aff9..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/noMappingsContent.tsx +++ /dev/null @@ -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 ( - - - - {t("NO_FOLDERS_ADDED")} - - - {t("FOLDERS_AUTOMATICALLY_MONITORED")} - - - - - {t("UPLOAD_NEW_FILES_TO_ENTE")} - - - - - - {t("REMOVE_DELETED_FILES_FROM_ENTE")} - - - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/styledComponents.tsx b/web/apps/photos/src/components/WatchFolder/styledComponents.tsx deleted file mode 100644 index d507bbaa8..000000000 --- a/web/apps/photos/src/components/WatchFolder/styledComponents.tsx +++ /dev/null @@ -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", -}); diff --git a/web/apps/photos/src/services/download/index.ts b/web/apps/photos/src/services/download/index.ts index 124d5b4b2..41af5c055 100644 --- a/web/apps/photos/src/services/download/index.ts +++ b/web/apps/photos/src/services/download/index.ts @@ -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); diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index f7a0c3f3e..7d6279882 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -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), ); diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 82b761091..d222999d8 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -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"; diff --git a/web/apps/photos/src/services/watchFolder/watchFolderService.ts b/web/apps/photos/src/services/watch.ts similarity index 85% rename from web/apps/photos/src/services/watchFolder/watchFolderService.ts rename to web/apps/photos/src/services/watch.ts index 791aed445..2d5ef0228 100644 --- a/web/apps/photos/src/services/watchFolder/watchFolderService.ts +++ b/web/apps/photos/src/services/watch.ts @@ -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 { @@ -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(); + 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) + ); +} diff --git a/web/apps/photos/src/services/watchFolder/utils.ts b/web/apps/photos/src/services/watchFolder/utils.ts deleted file mode 100644 index bd6ceb853..000000000 --- a/web/apps/photos/src/services/watchFolder/utils.ts +++ /dev/null @@ -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; -}; diff --git a/web/apps/photos/src/services/watchFolder/watchFolderEventHandlers.ts b/web/apps/photos/src/services/watchFolder/watchFolderEventHandlers.ts deleted file mode 100644 index ba4ad62ee..000000000 --- a/web/apps/photos/src/services/watchFolder/watchFolderEventHandlers.ts +++ /dev/null @@ -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); - } -} diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 8f72cb450..785921cc9 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -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; - 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); } } diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts new file mode 100644 index 000000000..7dba1acf9 --- /dev/null +++ b/web/apps/photos/src/utils/native-stream.ts @@ -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}`, + ); +}; diff --git a/web/apps/photos/src/utils/watch/index.ts b/web/apps/photos/src/utils/watch/index.ts deleted file mode 100644 index eb16780dd..000000000 --- a/web/apps/photos/src/utils/watch/index.ts +++ /dev/null @@ -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(); - return files.filter((file) => { - if (!isSystemFile(file) && !isSyncedOrIgnoredFile(file, mapping)) { - if (!uniqueFilePaths.has(file.path)) { - uniqueFilePaths.add(file.path); - return true; - } - } - return false; - }); -} diff --git a/web/packages/next/locales/de-DE/translation.json b/web/packages/next/locales/de-DE/translation.json index 7a7a2a3d9..38b877fd4 100644 --- a/web/packages/next/locales/de-DE/translation.json +++ b/web/packages/next/locales/de-DE/translation.json @@ -2,8 +2,8 @@ "HERO_SLIDE_1_TITLE": "
Private Sicherungen
für deine Erinnerungen
", "HERO_SLIDE_1": "Standardmäßig Ende-zu-Ende verschlüsselt", "HERO_SLIDE_2_TITLE": "
Sicher gespeichert
in einem Luftschutzbunker
", - "HERO_SLIDE_2": "Entwickelt um zu bewahren", - "HERO_SLIDE_3_TITLE": "
Verfügbar
überall
", + "HERO_SLIDE_2": "Entwickelt um zu überleben", + "HERO_SLIDE_3_TITLE": "
Überall
verfügbar
", "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": "

Die folgenden Dateien konnten aufgrund deiner Browser-Konfiguration nicht hochgeladen werden.

Bitte deaktiviere alle Add-ons, die Ente daran hindern könnten, eTags zum Hochladen großer Dateien zu verwenden oder verwende unsere Desktop-App für ein zuverlässigeres Import-Erlebnis.

", + "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": "

{{selectedEmail}} wird nicht in der Lage sein, weitere Fotos zum Album

hinzuzufügen. {{selectedEmail}} wird weiterhin die eigenen Fotos aus dem Album entfernen können

", + "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 {{usage}}", "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.

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": "

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.

Bitte klicke hier für weitere Informationen über diese Funktion in unserer Datenschutzerklärung

", "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" } diff --git a/web/packages/next/locales/sv-SE/translation.json b/web/packages/next/locales/sv-SE/translation.json index afc9b4ec4..77462524d 100644 --- a/web/packages/next/locales/sv-SE/translation.json +++ b/web/packages/next/locales/sv-SE/translation.json @@ -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": "", diff --git a/web/packages/next/next.config.base.js b/web/packages/next/next.config.base.js index f0d1481b4..a3076fa5c 100644 --- a/web/packages/next/next.config.base.js +++ b/web/packages/next/next.config.base.js @@ -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; }, }; diff --git a/web/packages/next/types/file.ts b/web/packages/next/types/file.ts index e7d3ced5a..dc8a148e9 100644 --- a/web/packages/next/types/file.ts +++ b/web/packages/next/types/file.ts @@ -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; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 69b0c3593..85986b639 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -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; + + /** Read the string contents of a file at {@link path}. */ + readTextFile: (path: string) => Promise; + + /** + * 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; }; /* @@ -287,25 +298,19 @@ export interface Electron { removeWatchMapping: (folderPath: string) => Promise; - getWatchMappings: () => Promise; + getWatchMappings: () => Promise; updateWatchMappingSyncedFiles: ( folderPath: string, - files: WatchMapping["syncedFiles"], + files: FolderWatch["syncedFiles"], ) => Promise; updateWatchMappingIgnoredFiles: ( folderPath: string, - files: WatchMapping["ignoredFiles"], + files: FolderWatch["ignoredFiles"], ) => Promise; // - FS legacy - saveStreamToDisk: ( - path: string, - fileStream: ReadableStream, - ) => Promise; - saveFileToDisk: (path: string, contents: string) => Promise; - readTextFile: (path: string) => Promise; isFolder: (dirPath: string) => Promise; // - Upload @@ -327,3 +332,30 @@ export interface Electron { setToUploadCollection: (collectionName: string) => Promise; getDirFiles: (dirPath: string) => Promise; } + +/** + * 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; +} diff --git a/web/packages/shared/utils/index.ts b/web/packages/shared/utils/index.ts index 1ed02fabe..c027b6cb6 100644 --- a/web/packages/shared/utils/index.ts +++ b/web/packages/shared/utils/index.ts @@ -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) {