Merge branch 'main' into f-droid
This commit is contained in:
commit
93ba4e011a
173 changed files with 4441 additions and 4186 deletions
56
.github/workflows/mobile-internal-release.yml
vendored
Normal file
56
.github/workflows/mobile-internal-release.yml
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
name: "Internal Release - Photos"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.19.3"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile
|
||||
|
||||
steps:
|
||||
- name: Checkout code and submodules
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 17
|
||||
|
||||
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Setup keys
|
||||
uses: timheuer/base64-to-file@v1
|
||||
with:
|
||||
fileName: "keystore/ente_photos_key.jks"
|
||||
encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }}
|
||||
|
||||
- name: Build PlayStore AAB
|
||||
run: |
|
||||
flutter build appbundle --release --flavor playstore
|
||||
env:
|
||||
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_PHOTOS }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
|
||||
|
||||
- name: Upload AAB to PlayStore
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: io.ente.photos
|
||||
releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
|
||||
track: internal
|
7
.github/workflows/mobile-release.yml
vendored
7
.github/workflows/mobile-release.yml
vendored
|
@ -9,7 +9,7 @@ on:
|
|||
- "photos-v*"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.19.5"
|
||||
FLUTTER_VERSION: "3.19.3"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -25,6 +25,11 @@ jobs:
|
|||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 17
|
||||
|
||||
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
|
|
|
@ -113,12 +113,14 @@
|
|||
"copied": "Kopiert",
|
||||
"pleaseTryAgain": "Bitte versuchen Sie es erneut",
|
||||
"existingUser": "Bestehender Benutzer",
|
||||
"newUser": "Neu bei Ente",
|
||||
"delete": "Löschen",
|
||||
"enterYourPasswordHint": "Geben Sie Ihr Passwort ein",
|
||||
"forgotPassword": "Passwort vergessen",
|
||||
"oops": "Hopla",
|
||||
"suggestFeatures": "Features vorschlagen",
|
||||
"faq": "FAQ",
|
||||
"faq_q_1": "Wie sicher ist Auth?",
|
||||
"faq_q_2": "Kann ich auf meine Codes auf dem Desktop zugreifen?",
|
||||
"faq_a_2": "Sie können auf Ihre Codes im Web via auth.ente.io zugreifen.",
|
||||
"faq_q_3": "Wie kann ich Codes löschen?",
|
||||
|
@ -193,6 +195,7 @@
|
|||
"recoveryKeySaveDescription": "Wir speichern diesen Schlüssel nicht. Sichern sie dieses diesen Schlüssel bestehend aus 24 Wörtern an einem sicheren Platz.",
|
||||
"doThisLater": "Auf später verschieben",
|
||||
"saveKey": "Schlüssel speichern",
|
||||
"save": "Speichern",
|
||||
"back": "Zurück",
|
||||
"createAccount": "Account erstellen",
|
||||
"passwordStrength": "Passwortstärke: {passwordStrengthValue}",
|
||||
|
@ -400,6 +403,7 @@
|
|||
"doNotSignOut": "Nicht abmelden",
|
||||
"hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
|
||||
"hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!",
|
||||
"recoveryKeySaved": "Wiederherstellungsschlüssel im Downloads-Ordner gespeichert!",
|
||||
"waitingForBrowserRequest": "Warten auf Browseranfrage...",
|
||||
"waitingForVerification": "Warte auf Bestätigung...",
|
||||
"passkey": "Passkey",
|
||||
|
|
|
@ -190,6 +190,7 @@
|
|||
"recoveryKeySaveDescription": "Non memorizziamo questa chiave, per favore salva questa chiave di 24 parole in un posto sicuro.",
|
||||
"doThisLater": "Fallo più tardi",
|
||||
"saveKey": "Salva chiave",
|
||||
"save": "Salva",
|
||||
"back": "Indietro",
|
||||
"createAccount": "Crea account",
|
||||
"passwordStrength": "Forza password: {passwordStrengthValue}",
|
||||
|
@ -396,5 +397,6 @@
|
|||
"signOutOtherDevices": "Esci dagli altri dispositivi",
|
||||
"doNotSignOut": "Non uscire",
|
||||
"hearUsWhereTitle": "Dove hai sentito parlare di Ente? (opzionale)",
|
||||
"hearUsExplanation": "Non teniamo traccia delle installazioni dell'app. Sarebbe utile se ci dicessi dove ci hai trovato!"
|
||||
"hearUsExplanation": "Non teniamo traccia delle installazioni dell'app. Sarebbe utile se ci dicessi dove ci hai trovato!",
|
||||
"passkey": "Passkey"
|
||||
}
|
|
@ -78,12 +78,14 @@
|
|||
"data": "Dados",
|
||||
"importCodes": "Importar códigos",
|
||||
"importTypePlainText": "Texto simples",
|
||||
"importTypeEnteEncrypted": "Exportação Ente criptografada",
|
||||
"passwordForDecryptingExport": "Senha para descriptografar a exportação",
|
||||
"passwordEmptyError": "O campo senha não pode estar vazio",
|
||||
"importFromApp": "Importar códigos do {appName}",
|
||||
"importGoogleAuthGuide": "Exporte suas contas do Google Authenticator para um QR code usando a opção \"Transferir contas\". Então, usando outro dispositivo, escaneie o QR code.\n\nDica: Você pode usar a câmera do seu notebook para fotografar o QR code.",
|
||||
"importSelectJsonFile": "Selecione o arquivo JSON",
|
||||
"importSelectAppExport": "Selecione o arquivo de exportação do aplicativo {appName}",
|
||||
"importEnteEncGuide": "Selecione o arquivo JSON criptografado exportado do Ente",
|
||||
"importRaivoGuide": "Use a opção \"Exportar OTPs para arquivo Zip\" nas configurações do Raivo.\n\nExtraia o arquivo zip e importe o arquivo JSON.",
|
||||
"importBitwardenGuide": "Use a opção \"Exportar cofre\" nas configurações do Bitwarden e importe o arquivo JSON não criptografado.",
|
||||
"importAegisGuide": "Use a opção \"Exportar cofre\" nas Configurações do Aegis.\n\nSe o seu cofre estiver criptografado, você precisará inserir a senha do cofre para descriptografá-lo.",
|
||||
|
@ -113,18 +115,22 @@
|
|||
"copied": "Copiado",
|
||||
"pleaseTryAgain": "Por favor, tente novamente",
|
||||
"existingUser": "Usuário Existente",
|
||||
"newUser": "Novo no Ente",
|
||||
"delete": "Excluir",
|
||||
"enterYourPasswordHint": "Insira sua senha",
|
||||
"forgotPassword": "Esqueci a senha",
|
||||
"oops": "Oops",
|
||||
"suggestFeatures": "Sugerir funcionalidades",
|
||||
"faq": "Perguntas frequentes",
|
||||
"faq_q_1": "Quão seguro é o Auth?",
|
||||
"faq_a_1": "Todos os códigos que você faz backup via Auth são armazenados criptografados de ponta a ponta. Isso significa que somente você pode acessar seus códigos. Nossos aplicativos são de código aberto e nossa criptografia foi auditada externamente.",
|
||||
"faq_q_2": "Eu posso acessar meus códigos no computador?",
|
||||
"faq_a_2": "Você pode acessar seus códigos na web em auth.ente.io.",
|
||||
"faq_q_3": "Como faço para excluir códigos?",
|
||||
"faq_a_3": "Você pode excluir um código deslizando para a esquerda sobre esse item.",
|
||||
"faq_q_4": "Como posso apoiar este projeto?",
|
||||
"faq_a_4": "Você pode apoiar o desenvolvimento deste projeto assinando nosso aplicativo de Fotos em ente.io.",
|
||||
"faq_q_5": "Como posso ativar o bloqueio facial no Auth",
|
||||
"faq_a_5": "Você pode ativar o bloqueio facial em Configurações → Segurança → Tela de bloqueio.",
|
||||
"somethingWentWrongMessage": "Algo deu errado. Por favor, tente outra vez",
|
||||
"leaveFamily": "Sair da família",
|
||||
|
@ -344,6 +350,7 @@
|
|||
"deleteCodeAuthMessage": "Autenticar para excluir o código",
|
||||
"showQRAuthMessage": "Autenticar para mostrar o QR code",
|
||||
"confirmAccountDeleteTitle": "Confirmar exclusão de conta",
|
||||
"confirmAccountDeleteMessage": "Esta conta está vinculada a outros aplicativos Ente, se você usa algum.\n\nSeus dados enviados, em todos os aplicativos Ente, serão agendados para exclusão, e sua conta será excluída permanentemente.",
|
||||
"androidBiometricHint": "Verificar identidade",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
"incorrectPasswordTitle": "Felaktigt lösenord",
|
||||
"welcomeBack": "Välkommen tillbaka!",
|
||||
"changePassword": "Ändra lösenord",
|
||||
"importCodes": "Importera koder",
|
||||
"cancel": "Avbryt",
|
||||
"yes": "Ja",
|
||||
"no": "Nej",
|
||||
|
|
|
@ -44,8 +44,8 @@
|
|||
"electron-builder-notarize": "^1.5",
|
||||
"eslint": "^8",
|
||||
"prettier": "^3",
|
||||
"prettier-plugin-organize-imports": "^3.2",
|
||||
"prettier-plugin-packagejson": "^2.4",
|
||||
"prettier-plugin-organize-imports": "^3",
|
||||
"prettier-plugin-packagejson": "^2",
|
||||
"shx": "^0.3",
|
||||
"typescript": "^5"
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
|
||||
*/
|
||||
import { nativeImage } from "electron";
|
||||
import { app, BrowserWindow, Menu, Tray } from "electron/main";
|
||||
import { app, BrowserWindow, Menu, protocol, Tray } from "electron/main";
|
||||
import serveNextAt from "next-electron-server";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
|
@ -24,15 +24,17 @@ import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
|
|||
import log, { initLogging } from "./main/log";
|
||||
import { createApplicationMenu, createTrayContextMenu } from "./main/menu";
|
||||
import { setupAutoUpdater } from "./main/services/app-update";
|
||||
import autoLauncher from "./main/services/autoLauncher";
|
||||
import { initWatcher } from "./main/services/chokidar";
|
||||
import autoLauncher from "./main/services/auto-launcher";
|
||||
import { createWatcher } from "./main/services/watch";
|
||||
import { userPreferences } from "./main/stores/user-preferences";
|
||||
import { migrateLegacyWatchStoreIfNeeded } from "./main/stores/watch";
|
||||
import { registerStreamProtocol } from "./main/stream";
|
||||
import { isDev } from "./main/util";
|
||||
|
||||
/**
|
||||
* The URL where the renderer HTML is being served from.
|
||||
*/
|
||||
export const rendererURL = "next://app";
|
||||
export const rendererURL = "ente://app";
|
||||
|
||||
/**
|
||||
* We want to hide our window instead of closing it when the user presses the
|
||||
|
@ -58,21 +60,6 @@ export const allowWindowClose = (): void => {
|
|||
shouldAllowWindowClose = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* of our code the same.
|
||||
*
|
||||
* It uses protocol handlers to serve files from the "next://app" protocol
|
||||
*
|
||||
* - In development this is proxied to http://localhost:3000
|
||||
* - In production it serves files from the `/out` directory
|
||||
*
|
||||
* For more details, see this comparison:
|
||||
* https://github.com/HaNdTriX/next-electron-server/issues/5
|
||||
*/
|
||||
const setupRendererServer = () => serveNextAt(rendererURL);
|
||||
|
||||
/**
|
||||
* Log a standard startup banner.
|
||||
*
|
||||
|
@ -88,6 +75,75 @@ const logStartupBanner = () => {
|
|||
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
|
||||
* of our code the same.
|
||||
*
|
||||
* It uses protocol handlers to serve files from the "ente://" protocol.
|
||||
*
|
||||
* - In development this is proxied to http://localhost:3000
|
||||
* - In production it serves files from the `/out` directory
|
||||
*
|
||||
* For more details, see this comparison:
|
||||
* https://github.com/HaNdTriX/next-electron-server/issues/5
|
||||
*/
|
||||
const setupRendererServer = () => serveNextAt(rendererURL);
|
||||
|
||||
/**
|
||||
* Register privileged schemes.
|
||||
*
|
||||
* 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 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.
|
||||
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* [Note: Increased disk cache for the desktop app]
|
||||
*
|
||||
|
@ -141,8 +197,6 @@ const createMainWindow = async () => {
|
|||
window.maximize();
|
||||
}
|
||||
|
||||
window.loadURL(rendererURL);
|
||||
|
||||
// Open the DevTools automatically when running in dev mode
|
||||
if (isDev) window.webContents.openDevTools();
|
||||
|
||||
|
@ -241,6 +295,21 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Older versions of our app used to keep a keys.json. It is not needed anymore,
|
||||
* remove it if it exists.
|
||||
*
|
||||
* This code was added March 2024, and can be removed after some time once most
|
||||
* people have upgraded to newer versions.
|
||||
*/
|
||||
const deleteLegacyKeysStoreIfExists = async () => {
|
||||
const keysStore = path.join(app.getPath("userData"), "keys.json");
|
||||
if (existsSync(keysStore)) {
|
||||
log.info(`Removing legacy keys store at ${keysStore}`);
|
||||
await fs.rm(keysStore);
|
||||
}
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
|
@ -251,9 +320,12 @@ const main = () => {
|
|||
let mainWindow: BrowserWindow | undefined;
|
||||
|
||||
initLogging();
|
||||
setupRendererServer();
|
||||
logStartupBanner();
|
||||
// The order of the next two calls is important
|
||||
setupRendererServer();
|
||||
registerPrivilegedSchemes();
|
||||
increaseDiskCache();
|
||||
migrateLegacyWatchStoreIfNeeded();
|
||||
|
||||
app.on("second-instance", () => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
|
@ -268,19 +340,26 @@ const main = () => {
|
|||
//
|
||||
// Note that some Electron APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
// Create window and prepare for renderer
|
||||
mainWindow = await createMainWindow();
|
||||
const watcher = initWatcher(mainWindow);
|
||||
setupTrayItem(mainWindow);
|
||||
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
|
||||
attachIPCHandlers();
|
||||
attachFSWatchIPCHandlers(watcher);
|
||||
if (!isDev) setupAutoUpdater(mainWindow);
|
||||
attachFSWatchIPCHandlers(createWatcher(mainWindow));
|
||||
registerStreamProtocol();
|
||||
handleDownloads(mainWindow);
|
||||
handleExternalLinks(mainWindow);
|
||||
addAllowOriginHeader(mainWindow);
|
||||
|
||||
// Start loading the renderer
|
||||
mainWindow.loadURL(rendererURL);
|
||||
|
||||
// Continue on with the rest of the startup sequence
|
||||
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
|
||||
setupTrayItem(mainWindow);
|
||||
if (!isDev) setupAutoUpdater(mainWindow);
|
||||
|
||||
try {
|
||||
deleteLegacyDiskCacheDirIfExists();
|
||||
deleteLegacyKeysStoreIfExists();
|
||||
} catch (e) {
|
||||
// Log but otherwise ignore errors during non-critical startup
|
||||
// actions.
|
||||
|
|
|
@ -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,81 +16,14 @@ 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));
|
||||
|
||||
/**
|
||||
* 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) =>
|
||||
fs.writeFile(path, contents);
|
||||
|
||||
export const readTextFile = async (filePath: string) =>
|
||||
export const fsReadTextFile = async (filePath: string) =>
|
||||
fs.readFile(filePath, "utf-8");
|
||||
|
||||
export const isFolder = async (dirPath: string) => {
|
||||
export const fsWriteFile = (path: string, contents: string) =>
|
||||
fs.writeFile(path, contents);
|
||||
|
||||
export const fsIsDir = async (dirPath: string) => {
|
||||
if (!existsSync(dirPath)) return false;
|
||||
const stats = await fs.stat(dirPath);
|
||||
return stats.isDirectory();
|
||||
const stat = await fs.stat(dirPath);
|
||||
return stat.isDirectory();
|
||||
};
|
||||
|
|
|
@ -10,7 +10,12 @@
|
|||
|
||||
import type { FSWatcher } from "chokidar";
|
||||
import { ipcMain } from "electron/main";
|
||||
import type { ElectronFile, FILE_PATH_TYPE, WatchMapping } from "../types/ipc";
|
||||
import type {
|
||||
CollectionMapping,
|
||||
ElectronFile,
|
||||
FolderWatch,
|
||||
PendingUploads,
|
||||
} from "../types/ipc";
|
||||
import {
|
||||
selectDirectory,
|
||||
showUploadDirsDialog,
|
||||
|
@ -19,14 +24,13 @@ import {
|
|||
} from "./dialogs";
|
||||
import {
|
||||
fsExists,
|
||||
fsIsDir,
|
||||
fsMkdirIfNeeded,
|
||||
fsReadTextFile,
|
||||
fsRename,
|
||||
fsRm,
|
||||
fsRmdir,
|
||||
isFolder,
|
||||
readTextFile,
|
||||
saveFileToDisk,
|
||||
saveStreamToDisk,
|
||||
fsWriteFile,
|
||||
} from "./fs";
|
||||
import { logToDisk } from "./log";
|
||||
import {
|
||||
|
@ -50,16 +54,17 @@ import {
|
|||
} from "./services/store";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getPendingUploads,
|
||||
setToUploadCollection,
|
||||
setToUploadFiles,
|
||||
pendingUploads,
|
||||
setPendingUploadCollection,
|
||||
setPendingUploadFiles,
|
||||
} from "./services/upload";
|
||||
import {
|
||||
addWatchMapping,
|
||||
getWatchMappings,
|
||||
removeWatchMapping,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
updateWatchMappingSyncedFiles,
|
||||
watchAdd,
|
||||
watchFindFiles,
|
||||
watchGet,
|
||||
watchRemove,
|
||||
watchUpdateIgnoredFiles,
|
||||
watchUpdateSyncedFiles,
|
||||
} from "./services/watch";
|
||||
import { openDirectory, openLogDirectory } from "./util";
|
||||
|
||||
|
@ -113,6 +118,28 @@ 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),
|
||||
);
|
||||
|
||||
ipcMain.handle("fsIsDir", (_, dirPath: string) => fsIsDir(dirPath));
|
||||
|
||||
// - Conversion
|
||||
|
||||
ipcMain.handle("convertToJPEG", (_, fileData, filename) =>
|
||||
|
@ -164,54 +191,26 @@ 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
|
||||
|
||||
ipcMain.handle("getPendingUploads", () => getPendingUploads());
|
||||
ipcMain.handle("pendingUploads", () => pendingUploads());
|
||||
|
||||
ipcMain.handle("setPendingUploadCollection", (_, collectionName: string) =>
|
||||
setPendingUploadCollection(collectionName),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"setToUploadFiles",
|
||||
(_, type: FILE_PATH_TYPE, filePaths: string[]) =>
|
||||
setToUploadFiles(type, filePaths),
|
||||
"setPendingUploadFiles",
|
||||
(_, type: PendingUploads["type"], filePaths: string[]) =>
|
||||
setPendingUploadFiles(type, filePaths),
|
||||
);
|
||||
|
||||
// -
|
||||
|
||||
ipcMain.handle("getElectronFilesFromGoogleZip", (_, filePath: string) =>
|
||||
getElectronFilesFromGoogleZip(filePath),
|
||||
);
|
||||
|
||||
ipcMain.handle("setToUploadCollection", (_, collectionName: string) =>
|
||||
setToUploadCollection(collectionName),
|
||||
);
|
||||
|
||||
ipcMain.handle("getDirFiles", (_, dirPath: string) => getDirFiles(dirPath));
|
||||
};
|
||||
|
||||
|
@ -220,42 +219,36 @@ export const attachIPCHandlers = () => {
|
|||
* watch folder functionality.
|
||||
*
|
||||
* It gets passed a {@link FSWatcher} instance which it can then forward to the
|
||||
* actual handlers.
|
||||
* actual handlers if they need access to it to do their thing.
|
||||
*/
|
||||
export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
|
||||
// - Watch
|
||||
|
||||
ipcMain.handle(
|
||||
"addWatchMapping",
|
||||
(
|
||||
_,
|
||||
collectionName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number,
|
||||
) =>
|
||||
addWatchMapping(
|
||||
watcher,
|
||||
collectionName,
|
||||
folderPath,
|
||||
uploadStrategy,
|
||||
),
|
||||
);
|
||||
|
||||
ipcMain.handle("removeWatchMapping", (_, folderPath: string) =>
|
||||
removeWatchMapping(watcher, folderPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("getWatchMappings", () => getWatchMappings());
|
||||
ipcMain.handle("watchGet", () => watchGet(watcher));
|
||||
|
||||
ipcMain.handle(
|
||||
"updateWatchMappingSyncedFiles",
|
||||
(_, folderPath: string, files: WatchMapping["syncedFiles"]) =>
|
||||
updateWatchMappingSyncedFiles(folderPath, files),
|
||||
"watchAdd",
|
||||
(_, folderPath: string, collectionMapping: CollectionMapping) =>
|
||||
watchAdd(watcher, folderPath, collectionMapping),
|
||||
);
|
||||
|
||||
ipcMain.handle("watchRemove", (_, folderPath: string) =>
|
||||
watchRemove(watcher, folderPath),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"updateWatchMappingIgnoredFiles",
|
||||
(_, folderPath: string, files: WatchMapping["ignoredFiles"]) =>
|
||||
updateWatchMappingIgnoredFiles(folderPath, files),
|
||||
"watchUpdateSyncedFiles",
|
||||
(_, syncedFiles: FolderWatch["syncedFiles"], folderPath: string) =>
|
||||
watchUpdateSyncedFiles(syncedFiles, folderPath),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"watchUpdateIgnoredFiles",
|
||||
(_, ignoredFiles: FolderWatch["ignoredFiles"], folderPath: string) =>
|
||||
watchUpdateIgnoredFiles(ignoredFiles, folderPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("watchFindFiles", (_, folderPath: string) =>
|
||||
watchFindFiles(folderPath),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from "electron";
|
||||
import { allowWindowClose } from "../main";
|
||||
import { forceCheckForAppUpdates } from "./services/app-update";
|
||||
import autoLauncher from "./services/autoLauncher";
|
||||
import autoLauncher from "./services/auto-launcher";
|
||||
import { userPreferences } from "./stores/user-preferences";
|
||||
import { openLogDirectory } from "./util";
|
||||
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
export function isPlatform(platform: "mac" | "windows" | "linux") {
|
||||
return getPlatform() === platform;
|
||||
}
|
||||
|
||||
export function getPlatform(): "mac" | "windows" | "linux" {
|
||||
switch (process.platform) {
|
||||
case "aix":
|
||||
case "freebsd":
|
||||
case "linux":
|
||||
case "openbsd":
|
||||
case "android":
|
||||
return "linux";
|
||||
case "darwin":
|
||||
case "sunos":
|
||||
return "mac";
|
||||
case "win32":
|
||||
return "windows";
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { compareVersions } from "compare-versions";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { default as electronLog } from "electron-log";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { app, BrowserWindow } from "electron/main";
|
||||
import { allowWindowClose } from "../../main";
|
||||
import { AppUpdateInfo } from "../../types/ipc";
|
||||
import { AppUpdate } from "../../types/ipc";
|
||||
import log from "../log";
|
||||
import { userPreferences } from "../stores/user-preferences";
|
||||
|
||||
|
@ -52,8 +52,8 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const showUpdateDialog = (updateInfo: AppUpdateInfo) =>
|
||||
mainWindow.webContents.send("appUpdateAvailable", updateInfo);
|
||||
const showUpdateDialog = (update: AppUpdate) =>
|
||||
mainWindow.webContents.send("appUpdateAvailable", update);
|
||||
|
||||
log.debug(() => "Attempting auto update");
|
||||
autoUpdater.downloadUpdate();
|
||||
|
|
51
desktop/src/main/services/auto-launcher.ts
Normal file
51
desktop/src/main/services/auto-launcher.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import AutoLaunch from "auto-launch";
|
||||
import { app } from "electron/main";
|
||||
|
||||
class AutoLauncher {
|
||||
/**
|
||||
* This property will be set and used on Linux and Windows. On macOS,
|
||||
* there's a separate API
|
||||
*/
|
||||
private autoLaunch?: AutoLaunch;
|
||||
|
||||
constructor() {
|
||||
if (process.platform != "darwin") {
|
||||
this.autoLaunch = new AutoLaunch({
|
||||
name: "ente",
|
||||
isHidden: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async isEnabled() {
|
||||
const autoLaunch = this.autoLaunch;
|
||||
if (autoLaunch) {
|
||||
return await autoLaunch.isEnabled();
|
||||
} else {
|
||||
return app.getLoginItemSettings().openAtLogin;
|
||||
}
|
||||
}
|
||||
|
||||
async toggleAutoLaunch() {
|
||||
const isEnabled = await this.isEnabled();
|
||||
const autoLaunch = this.autoLaunch;
|
||||
if (autoLaunch) {
|
||||
if (isEnabled) await autoLaunch.disable();
|
||||
else await autoLaunch.enable();
|
||||
} else {
|
||||
if (isEnabled) app.setLoginItemSettings({ openAtLogin: false });
|
||||
else app.setLoginItemSettings({ openAtLogin: true });
|
||||
}
|
||||
}
|
||||
|
||||
async wasAutoLaunched() {
|
||||
if (this.autoLaunch) {
|
||||
return app.commandLine.hasSwitch("hidden");
|
||||
} else {
|
||||
// TODO(MR): This apparently doesn't work anymore.
|
||||
return app.getLoginItemSettings().wasOpenedAtLogin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutoLauncher();
|
|
@ -1,41 +0,0 @@
|
|||
import { AutoLauncherClient } from "../../types/main";
|
||||
import { isPlatform } from "../platform";
|
||||
import linuxAndWinAutoLauncher from "./autoLauncherClients/linuxAndWinAutoLauncher";
|
||||
import macAutoLauncher from "./autoLauncherClients/macAutoLauncher";
|
||||
|
||||
class AutoLauncher {
|
||||
private client: AutoLauncherClient;
|
||||
async init() {
|
||||
if (isPlatform("linux") || isPlatform("windows")) {
|
||||
this.client = linuxAndWinAutoLauncher;
|
||||
} else {
|
||||
this.client = macAutoLauncher;
|
||||
}
|
||||
// migrate old auto launch settings for windows from mac auto launcher to linux and windows auto launcher
|
||||
if (isPlatform("windows") && (await macAutoLauncher.isEnabled())) {
|
||||
await macAutoLauncher.toggleAutoLaunch();
|
||||
await linuxAndWinAutoLauncher.toggleAutoLaunch();
|
||||
}
|
||||
}
|
||||
async isEnabled() {
|
||||
if (!this.client) {
|
||||
await this.init();
|
||||
}
|
||||
return await this.client.isEnabled();
|
||||
}
|
||||
async toggleAutoLaunch() {
|
||||
if (!this.client) {
|
||||
await this.init();
|
||||
}
|
||||
await this.client.toggleAutoLaunch();
|
||||
}
|
||||
|
||||
async wasAutoLaunched() {
|
||||
if (!this.client) {
|
||||
await this.init();
|
||||
}
|
||||
return this.client.wasAutoLaunched();
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutoLauncher();
|
|
@ -1,39 +0,0 @@
|
|||
import AutoLaunch from "auto-launch";
|
||||
import { app } from "electron";
|
||||
import { AutoLauncherClient } from "../../../types/main";
|
||||
|
||||
const LAUNCHED_AS_HIDDEN_FLAG = "hidden";
|
||||
|
||||
class LinuxAndWinAutoLauncher implements AutoLauncherClient {
|
||||
private instance: AutoLaunch;
|
||||
constructor() {
|
||||
const autoLauncher = new AutoLaunch({
|
||||
name: "ente",
|
||||
isHidden: true,
|
||||
});
|
||||
this.instance = autoLauncher;
|
||||
}
|
||||
async isEnabled() {
|
||||
return await this.instance.isEnabled();
|
||||
}
|
||||
async toggleAutoLaunch() {
|
||||
if (await this.isEnabled()) {
|
||||
await this.disableAutoLaunch();
|
||||
} else {
|
||||
await this.enableAutoLaunch();
|
||||
}
|
||||
}
|
||||
|
||||
async wasAutoLaunched() {
|
||||
return app.commandLine.hasSwitch(LAUNCHED_AS_HIDDEN_FLAG);
|
||||
}
|
||||
|
||||
private async disableAutoLaunch() {
|
||||
await this.instance.disable();
|
||||
}
|
||||
private async enableAutoLaunch() {
|
||||
await this.instance.enable();
|
||||
}
|
||||
}
|
||||
|
||||
export default new LinuxAndWinAutoLauncher();
|
|
@ -1,28 +0,0 @@
|
|||
import { app } from "electron";
|
||||
import { AutoLauncherClient } from "../../../types/main";
|
||||
|
||||
class MacAutoLauncher implements AutoLauncherClient {
|
||||
async isEnabled() {
|
||||
return app.getLoginItemSettings().openAtLogin;
|
||||
}
|
||||
async toggleAutoLaunch() {
|
||||
if (await this.isEnabled()) {
|
||||
this.disableAutoLaunch();
|
||||
} else {
|
||||
this.enableAutoLaunch();
|
||||
}
|
||||
}
|
||||
|
||||
async wasAutoLaunched() {
|
||||
return app.getLoginItemSettings().wasOpenedAtLogin;
|
||||
}
|
||||
|
||||
private disableAutoLaunch() {
|
||||
app.setLoginItemSettings({ openAtLogin: false });
|
||||
}
|
||||
private enableAutoLaunch() {
|
||||
app.setLoginItemSettings({ openAtLogin: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new MacAutoLauncher();
|
|
@ -1,45 +0,0 @@
|
|||
import chokidar from "chokidar";
|
||||
import { BrowserWindow } from "electron";
|
||||
import path from "path";
|
||||
import log from "../log";
|
||||
import { getElectronFile } from "./fs";
|
||||
import { getWatchMappings } from "./watch";
|
||||
|
||||
/**
|
||||
* Convert a file system {@link filePath} that uses the local system specific
|
||||
* path separators into a path that uses POSIX file separators.
|
||||
*/
|
||||
const normalizeToPOSIX = (filePath: string) =>
|
||||
filePath.split(path.sep).join(path.posix.sep);
|
||||
|
||||
export function initWatcher(mainWindow: BrowserWindow) {
|
||||
const mappings = getWatchMappings();
|
||||
const folderPaths = mappings.map((mapping) => {
|
||||
return mapping.folderPath;
|
||||
});
|
||||
|
||||
const watcher = chokidar.watch(folderPaths, {
|
||||
awaitWriteFinish: true,
|
||||
});
|
||||
watcher
|
||||
.on("add", async (path) => {
|
||||
mainWindow.webContents.send(
|
||||
"watch-add",
|
||||
await getElectronFile(normalizeToPOSIX(path)),
|
||||
);
|
||||
})
|
||||
.on("unlink", (path) => {
|
||||
mainWindow.webContents.send("watch-unlink", normalizeToPOSIX(path));
|
||||
})
|
||||
.on("unlinkDir", (path) => {
|
||||
mainWindow.webContents.send(
|
||||
"watch-unlink-dir",
|
||||
normalizeToPOSIX(path),
|
||||
);
|
||||
})
|
||||
.on("error", (error) => {
|
||||
log.error("Error while watching files", error);
|
||||
});
|
||||
|
||||
return watcher;
|
||||
}
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -91,19 +91,6 @@ export async function getElectronFile(filePath: string): Promise<ElectronFile> {
|
|||
};
|
||||
}
|
||||
|
||||
export const getValidPaths = (paths: string[]) => {
|
||||
if (!paths) {
|
||||
return [] as string[];
|
||||
}
|
||||
return paths.filter(async (path) => {
|
||||
try {
|
||||
await fs.stat(path).then((stat) => stat.isFile());
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getZipFileStream = async (
|
||||
zip: StreamZip.StreamZipAsync,
|
||||
filePath: string,
|
||||
|
|
|
@ -2,9 +2,8 @@ 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";
|
||||
|
@ -67,19 +66,15 @@ const IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE = [
|
|||
OUTPUT_PATH_PLACEHOLDER,
|
||||
];
|
||||
|
||||
function getImageMagickStaticPath() {
|
||||
return isDev
|
||||
? "resources/image-magick"
|
||||
: path.join(process.resourcesPath, "image-magick");
|
||||
}
|
||||
const imageMagickStaticPath = () =>
|
||||
path.join(isDev ? "build" : process.resourcesPath, "image-magick");
|
||||
|
||||
export async function convertToJPEG(
|
||||
fileData: Uint8Array,
|
||||
filename: string,
|
||||
): Promise<Uint8Array> {
|
||||
if (isPlatform("windows")) {
|
||||
if (process.platform == "win32")
|
||||
throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED);
|
||||
}
|
||||
const convertedFileData = await convertToJPEG_(fileData, filename);
|
||||
return convertedFileData;
|
||||
}
|
||||
|
@ -126,7 +121,7 @@ function constructConvertCommand(
|
|||
tempOutputFilePath: string,
|
||||
) {
|
||||
let convertCmd: string[];
|
||||
if (isPlatform("mac")) {
|
||||
if (process.platform == "darwin") {
|
||||
convertCmd = SIPS_HEIC_CONVERT_COMMAND_TEMPLATE.map((cmdPart) => {
|
||||
if (cmdPart === INPUT_PATH_PLACEHOLDER) {
|
||||
return tempInputFilePath;
|
||||
|
@ -136,11 +131,11 @@ function constructConvertCommand(
|
|||
}
|
||||
return cmdPart;
|
||||
});
|
||||
} else if (isPlatform("linux")) {
|
||||
} else if (process.platform == "linux") {
|
||||
convertCmd = IMAGEMAGICK_HEIC_CONVERT_COMMAND_TEMPLATE.map(
|
||||
(cmdPart) => {
|
||||
if (cmdPart === IMAGE_MAGICK_PLACEHOLDER) {
|
||||
return getImageMagickStaticPath();
|
||||
return imageMagickStaticPath();
|
||||
}
|
||||
if (cmdPart === INPUT_PATH_PLACEHOLDER) {
|
||||
return tempInputFilePath;
|
||||
|
@ -165,11 +160,10 @@ export async function generateImageThumbnail(
|
|||
let inputFilePath = null;
|
||||
let createdTempInputFile = null;
|
||||
try {
|
||||
if (isPlatform("windows")) {
|
||||
if (process.platform == "win32")
|
||||
throw Error(
|
||||
CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED,
|
||||
);
|
||||
}
|
||||
if (!existsSync(inputFile.path)) {
|
||||
const tempFilePath = await generateTempFilePath(inputFile.name);
|
||||
await writeStream(tempFilePath, await inputFile.stream());
|
||||
|
@ -240,7 +234,7 @@ function constructThumbnailGenerationCommand(
|
|||
quality: number,
|
||||
) {
|
||||
let thumbnailGenerationCmd: string[];
|
||||
if (isPlatform("mac")) {
|
||||
if (process.platform == "darwin") {
|
||||
thumbnailGenerationCmd = SIPS_THUMBNAIL_GENERATE_COMMAND_TEMPLATE.map(
|
||||
(cmdPart) => {
|
||||
if (cmdPart === INPUT_PATH_PLACEHOLDER) {
|
||||
|
@ -258,11 +252,11 @@ function constructThumbnailGenerationCommand(
|
|||
return cmdPart;
|
||||
},
|
||||
);
|
||||
} else if (isPlatform("linux")) {
|
||||
} else if (process.platform == "linux") {
|
||||
thumbnailGenerationCmd =
|
||||
IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE.map((cmdPart) => {
|
||||
if (cmdPart === IMAGE_MAGICK_PLACEHOLDER) {
|
||||
return getImageMagickStaticPath();
|
||||
return imageMagickStaticPath();
|
||||
}
|
||||
if (cmdPart === INPUT_PATH_PLACEHOLDER) {
|
||||
return inputFilePath;
|
||||
|
|
|
@ -11,8 +11,8 @@ import fs from "node:fs/promises";
|
|||
import * as ort from "onnxruntime-node";
|
||||
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
|
||||
import { CustomErrors } from "../../types/ipc";
|
||||
import { writeStream } from "../fs";
|
||||
import log from "../log";
|
||||
import { writeStream } from "../stream";
|
||||
import { generateTempFilePath } from "../temp";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
import {
|
||||
|
|
|
@ -15,8 +15,8 @@ import { existsSync } from "fs";
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as ort from "onnxruntime-node";
|
||||
import { writeStream } from "../fs";
|
||||
import log from "../log";
|
||||
import { writeStream } from "../stream";
|
||||
|
||||
/**
|
||||
* Download the model named {@link modelName} if we don't already have it.
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { safeStorage } from "electron/main";
|
||||
import { keysStore } from "../stores/keys.store";
|
||||
import { safeStorageStore } from "../stores/safeStorage.store";
|
||||
import { uploadStatusStore } from "../stores/upload.store";
|
||||
import { watchStore } from "../stores/watch.store";
|
||||
import { safeStorageStore } from "../stores/safe-storage";
|
||||
import { uploadStatusStore } from "../stores/upload-status";
|
||||
import { watchStore } from "../stores/watch";
|
||||
|
||||
/**
|
||||
* Clear all stores except user preferences.
|
||||
*
|
||||
* This is useful to reset state when the user logs out.
|
||||
*/
|
||||
export const clearStores = () => {
|
||||
uploadStatusStore.clear();
|
||||
keysStore.clear();
|
||||
safeStorageStore.clear();
|
||||
watchStore.clear();
|
||||
};
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import StreamZip from "node-stream-zip";
|
||||
import { existsSync } from "original-fs";
|
||||
import path from "path";
|
||||
import { ElectronFile, FILE_PATH_TYPE } from "../../types/ipc";
|
||||
import { FILE_PATH_KEYS } from "../../types/main";
|
||||
import { uploadStatusStore } from "../stores/upload.store";
|
||||
import { getElectronFile, getValidPaths, getZipFileStream } from "./fs";
|
||||
import { ElectronFile, type PendingUploads } from "../../types/ipc";
|
||||
import {
|
||||
uploadStatusStore,
|
||||
type UploadStatusStore,
|
||||
} from "../stores/upload-status";
|
||||
import { getElectronFile, getZipFileStream } from "./fs";
|
||||
|
||||
export const getPendingUploads = async () => {
|
||||
const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES);
|
||||
const zipPaths = getSavedFilePaths(FILE_PATH_TYPE.ZIPS);
|
||||
export const pendingUploads = async () => {
|
||||
const collectionName = uploadStatusStore.get("collectionName");
|
||||
const filePaths = validSavedPaths("files");
|
||||
const zipPaths = validSavedPaths("zips");
|
||||
|
||||
let files: ElectronFile[] = [];
|
||||
let type: FILE_PATH_TYPE;
|
||||
let type: PendingUploads["type"];
|
||||
|
||||
if (zipPaths.length) {
|
||||
type = FILE_PATH_TYPE.ZIPS;
|
||||
type = "zips";
|
||||
for (const zipPath of zipPaths) {
|
||||
files = [
|
||||
...files,
|
||||
|
@ -23,9 +27,10 @@ export const getPendingUploads = async () => {
|
|||
const pendingFilePaths = new Set(filePaths);
|
||||
files = files.filter((file) => pendingFilePaths.has(file.path));
|
||||
} else if (filePaths.length) {
|
||||
type = FILE_PATH_TYPE.FILES;
|
||||
type = "files";
|
||||
files = await Promise.all(filePaths.map(getElectronFile));
|
||||
}
|
||||
|
||||
return {
|
||||
files,
|
||||
collectionName,
|
||||
|
@ -33,16 +38,56 @@ export const getPendingUploads = async () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const getSavedFilePaths = (type: FILE_PATH_TYPE) => {
|
||||
const paths =
|
||||
getValidPaths(
|
||||
uploadStatusStore.get(FILE_PATH_KEYS[type]) as string[],
|
||||
) ?? [];
|
||||
|
||||
setToUploadFiles(type, paths);
|
||||
export const validSavedPaths = (type: PendingUploads["type"]) => {
|
||||
const key = storeKey(type);
|
||||
const savedPaths = (uploadStatusStore.get(key) as string[]) ?? [];
|
||||
const paths = savedPaths.filter((p) => existsSync(p));
|
||||
uploadStatusStore.set(key, paths);
|
||||
return paths;
|
||||
};
|
||||
|
||||
export const setPendingUploadCollection = (collectionName: string) => {
|
||||
if (collectionName) uploadStatusStore.set("collectionName", collectionName);
|
||||
else uploadStatusStore.delete("collectionName");
|
||||
};
|
||||
|
||||
export const setPendingUploadFiles = (
|
||||
type: PendingUploads["type"],
|
||||
filePaths: string[],
|
||||
) => {
|
||||
const key = storeKey(type);
|
||||
if (filePaths) uploadStatusStore.set(key, filePaths);
|
||||
else uploadStatusStore.delete(key);
|
||||
};
|
||||
|
||||
const storeKey = (type: PendingUploads["type"]): keyof UploadStatusStore => {
|
||||
switch (type) {
|
||||
case "zips":
|
||||
return "zipPaths";
|
||||
case "files":
|
||||
return "filePaths";
|
||||
}
|
||||
};
|
||||
|
||||
export const getElectronFilesFromGoogleZip = async (filePath: string) => {
|
||||
const zip = new StreamZip.async({
|
||||
file: filePath,
|
||||
});
|
||||
const zipName = path.basename(filePath, ".zip");
|
||||
|
||||
const entries = await zip.entries();
|
||||
const files: ElectronFile[] = [];
|
||||
|
||||
for (const entry of Object.values(entries)) {
|
||||
const basename = path.basename(entry.name);
|
||||
if (entry.isFile && basename.length > 0 && basename[0] !== ".") {
|
||||
files.push(await getZipEntryAsElectronFile(zipName, zip, entry));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
export async function getZipEntryAsElectronFile(
|
||||
zipName: string,
|
||||
zip: StreamZip.StreamZipAsync,
|
||||
|
@ -69,39 +114,3 @@ export async function getZipEntryAsElectronFile(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const setToUploadFiles = (type: FILE_PATH_TYPE, filePaths: string[]) => {
|
||||
const key = FILE_PATH_KEYS[type];
|
||||
if (filePaths) {
|
||||
uploadStatusStore.set(key, filePaths);
|
||||
} else {
|
||||
uploadStatusStore.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
export const setToUploadCollection = (collectionName: string) => {
|
||||
if (collectionName) {
|
||||
uploadStatusStore.set("collectionName", collectionName);
|
||||
} else {
|
||||
uploadStatusStore.delete("collectionName");
|
||||
}
|
||||
};
|
||||
|
||||
export const getElectronFilesFromGoogleZip = async (filePath: string) => {
|
||||
const zip = new StreamZip.async({
|
||||
file: filePath,
|
||||
});
|
||||
const zipName = path.basename(filePath, ".zip");
|
||||
|
||||
const entries = await zip.entries();
|
||||
const files: ElectronFile[] = [];
|
||||
|
||||
for (const entry of Object.values(entries)) {
|
||||
const basename = path.basename(entry.name);
|
||||
if (entry.isFile && basename.length > 0 && basename[0] !== ".") {
|
||||
files.push(await getZipEntryAsElectronFile(zipName, zip, entry));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
|
|
@ -1,101 +1,159 @@
|
|||
import type { FSWatcher } from "chokidar";
|
||||
import ElectronLog from "electron-log";
|
||||
import { WatchMapping, WatchStoreType } from "../../types/ipc";
|
||||
import { watchStore } from "../stores/watch.store";
|
||||
import chokidar, { type FSWatcher } from "chokidar";
|
||||
import { BrowserWindow } from "electron/main";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { FolderWatch, type CollectionMapping } from "../../types/ipc";
|
||||
import { fsIsDir } from "../fs";
|
||||
import log from "../log";
|
||||
import { watchStore } from "../stores/watch";
|
||||
|
||||
export const addWatchMapping = async (
|
||||
watcher: FSWatcher,
|
||||
rootFolderName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number,
|
||||
) => {
|
||||
ElectronLog.log(`Adding watch mapping: ${folderPath}`);
|
||||
const watchMappings = getWatchMappings();
|
||||
if (isMappingPresent(watchMappings, folderPath)) {
|
||||
throw new Error(`Watch mapping already exists`);
|
||||
/**
|
||||
* Create and return a new file system watcher.
|
||||
*
|
||||
* Internally this uses the watcher from the chokidar package.
|
||||
*
|
||||
* @param mainWindow The window handle is used to notify the renderer process of
|
||||
* pertinent file system events.
|
||||
*/
|
||||
export const createWatcher = (mainWindow: BrowserWindow) => {
|
||||
const send = (eventName: string) => (path: string) =>
|
||||
mainWindow.webContents.send(eventName, ...eventData(path));
|
||||
|
||||
const folderPaths = folderWatches().map((watch) => watch.folderPath);
|
||||
|
||||
const watcher = chokidar.watch(folderPaths, {
|
||||
awaitWriteFinish: true,
|
||||
});
|
||||
|
||||
watcher
|
||||
.on("add", send("watchAddFile"))
|
||||
.on("unlink", send("watchRemoveFile"))
|
||||
.on("unlinkDir", send("watchRemoveDir"))
|
||||
.on("error", (error) => log.error("Error while watching files", error));
|
||||
|
||||
return watcher;
|
||||
};
|
||||
|
||||
const eventData = (path: string): [string, FolderWatch] => {
|
||||
path = posixPath(path);
|
||||
|
||||
const watch = folderWatches().find((watch) =>
|
||||
path.startsWith(watch.folderPath + "/"),
|
||||
);
|
||||
|
||||
if (!watch) throw new Error(`No folder watch was found for path ${path}`);
|
||||
|
||||
return [path, watch];
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a file system {@link filePath} that uses the local system specific
|
||||
* path separators into a path that uses POSIX file separators.
|
||||
*/
|
||||
const posixPath = (filePath: string) =>
|
||||
filePath.split(path.sep).join(path.posix.sep);
|
||||
|
||||
export const watchGet = (watcher: FSWatcher) => {
|
||||
const [valid, deleted] = folderWatches().reduce(
|
||||
([valid, deleted], watch) => {
|
||||
(fsIsDir(watch.folderPath) ? valid : deleted).push(watch);
|
||||
return [valid, deleted];
|
||||
},
|
||||
[[], []],
|
||||
);
|
||||
if (deleted.length) {
|
||||
for (const watch of deleted) watchRemove(watcher, watch.folderPath);
|
||||
setFolderWatches(valid);
|
||||
}
|
||||
return valid;
|
||||
};
|
||||
|
||||
watcher.add(folderPath);
|
||||
const folderWatches = (): FolderWatch[] => watchStore.get("mappings") ?? [];
|
||||
|
||||
watchMappings.push({
|
||||
rootFolderName,
|
||||
uploadStrategy,
|
||||
const setFolderWatches = (watches: FolderWatch[]) =>
|
||||
watchStore.set("mappings", watches);
|
||||
|
||||
export const watchAdd = async (
|
||||
watcher: FSWatcher,
|
||||
folderPath: string,
|
||||
collectionMapping: CollectionMapping,
|
||||
) => {
|
||||
const watches = folderWatches();
|
||||
|
||||
if (!fsIsDir(folderPath))
|
||||
throw new Error(
|
||||
`Attempting to add a folder watch for a folder path ${folderPath} that is not an existing directory`,
|
||||
);
|
||||
|
||||
if (watches.find((watch) => watch.folderPath == folderPath))
|
||||
throw new Error(
|
||||
`A folder watch with the given folder path ${folderPath} already exists`,
|
||||
);
|
||||
|
||||
watches.push({
|
||||
folderPath,
|
||||
collectionMapping,
|
||||
syncedFiles: [],
|
||||
ignoredFiles: [],
|
||||
});
|
||||
|
||||
setWatchMappings(watchMappings);
|
||||
setFolderWatches(watches);
|
||||
|
||||
watcher.add(folderPath);
|
||||
|
||||
return watches;
|
||||
};
|
||||
|
||||
function isMappingPresent(watchMappings: WatchMapping[], folderPath: string) {
|
||||
const watchMapping = watchMappings?.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
return !!watchMapping;
|
||||
}
|
||||
export const watchRemove = async (watcher: FSWatcher, folderPath: string) => {
|
||||
const watches = folderWatches();
|
||||
const filtered = watches.filter((watch) => watch.folderPath != folderPath);
|
||||
if (watches.length == filtered.length)
|
||||
throw new Error(
|
||||
`Attempting to remove a non-existing folder watch for folder path ${folderPath}`,
|
||||
);
|
||||
setFolderWatches(filtered);
|
||||
watcher.unwatch(folderPath);
|
||||
return filtered;
|
||||
};
|
||||
|
||||
export const removeWatchMapping = async (
|
||||
watcher: FSWatcher,
|
||||
export const watchUpdateSyncedFiles = (
|
||||
syncedFiles: FolderWatch["syncedFiles"],
|
||||
folderPath: string,
|
||||
) => {
|
||||
let watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
setFolderWatches(
|
||||
folderWatches().map((watch) => {
|
||||
if (watch.folderPath == folderPath) {
|
||||
watch.syncedFiles = syncedFiles;
|
||||
}
|
||||
return watch;
|
||||
}),
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
throw new Error(`Watch mapping does not exist`);
|
||||
}
|
||||
|
||||
watcher.unwatch(watchMapping.folderPath);
|
||||
|
||||
watchMappings = watchMappings.filter(
|
||||
(mapping) => mapping.folderPath !== watchMapping.folderPath,
|
||||
);
|
||||
|
||||
setWatchMappings(watchMappings);
|
||||
};
|
||||
|
||||
export function updateWatchMappingSyncedFiles(
|
||||
export const watchUpdateIgnoredFiles = (
|
||||
ignoredFiles: FolderWatch["ignoredFiles"],
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
) => {
|
||||
setFolderWatches(
|
||||
folderWatches().map((watch) => {
|
||||
if (watch.folderPath == folderPath) {
|
||||
watch.ignoredFiles = ignoredFiles;
|
||||
}
|
||||
return watch;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (!watchMapping) {
|
||||
throw Error(`Watch mapping not found`);
|
||||
export const watchFindFiles = async (dirPath: string) => {
|
||||
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
let paths: string[] = [];
|
||||
for (const item of items) {
|
||||
const itemPath = path.posix.join(dirPath, item.name);
|
||||
if (item.isFile()) {
|
||||
paths.push(itemPath);
|
||||
} else if (item.isDirectory()) {
|
||||
paths = [...paths, ...(await watchFindFiles(itemPath))];
|
||||
}
|
||||
}
|
||||
|
||||
watchMapping.syncedFiles = files;
|
||||
setWatchMappings(watchMappings);
|
||||
}
|
||||
|
||||
export function updateWatchMappingIgnoredFiles(
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
throw Error(`Watch mapping not found`);
|
||||
}
|
||||
|
||||
watchMapping.ignoredFiles = files;
|
||||
setWatchMappings(watchMappings);
|
||||
}
|
||||
|
||||
export function getWatchMappings() {
|
||||
const mappings = watchStore.get("mappings") ?? [];
|
||||
return mappings;
|
||||
}
|
||||
|
||||
function setWatchMappings(watchMappings: WatchStoreType["mappings"]) {
|
||||
watchStore.set("mappings", watchMappings);
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import Store, { Schema } from "electron-store";
|
||||
import type { KeysStoreType } from "../../types/main";
|
||||
|
||||
const keysStoreSchema: Schema<KeysStoreType> = {
|
||||
AnonymizeUserID: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const keysStore = new Store({
|
||||
name: "keys",
|
||||
schema: keysStoreSchema,
|
||||
});
|
|
@ -1,7 +1,10 @@
|
|||
import Store, { Schema } from "electron-store";
|
||||
import type { SafeStorageStoreType } from "../../types/main";
|
||||
|
||||
const safeStorageSchema: Schema<SafeStorageStoreType> = {
|
||||
interface SafeStorageStore {
|
||||
encryptionKey: string;
|
||||
}
|
||||
|
||||
const safeStorageSchema: Schema<SafeStorageStore> = {
|
||||
encryptionKey: {
|
||||
type: "string",
|
||||
},
|
|
@ -1,7 +1,12 @@
|
|||
import Store, { Schema } from "electron-store";
|
||||
import type { UploadStoreType } from "../../types/main";
|
||||
|
||||
const uploadStoreSchema: Schema<UploadStoreType> = {
|
||||
export interface UploadStatusStore {
|
||||
filePaths: string[];
|
||||
zipPaths: string[];
|
||||
collectionName: string;
|
||||
}
|
||||
|
||||
const uploadStatusSchema: Schema<UploadStatusStore> = {
|
||||
filePaths: {
|
||||
type: "array",
|
||||
items: {
|
||||
|
@ -21,5 +26,5 @@ const uploadStoreSchema: Schema<UploadStoreType> = {
|
|||
|
||||
export const uploadStatusStore = new Store({
|
||||
name: "upload-status",
|
||||
schema: uploadStoreSchema,
|
||||
schema: uploadStatusSchema,
|
||||
});
|
|
@ -1,12 +1,12 @@
|
|||
import Store, { Schema } from "electron-store";
|
||||
|
||||
interface UserPreferencesSchema {
|
||||
interface UserPreferences {
|
||||
hideDockIcon: boolean;
|
||||
skipAppVersion?: string;
|
||||
muteUpdateNotificationVersion?: string;
|
||||
}
|
||||
|
||||
const userPreferencesSchema: Schema<UserPreferencesSchema> = {
|
||||
const userPreferencesSchema: Schema<UserPreferences> = {
|
||||
hideDockIcon: {
|
||||
type: "boolean",
|
||||
},
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import Store, { Schema } from "electron-store";
|
||||
import { WatchStoreType } from "../../types/ipc";
|
||||
|
||||
const watchStoreSchema: Schema<WatchStoreType> = {
|
||||
mappings: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
rootFolderName: {
|
||||
type: "string",
|
||||
},
|
||||
uploadStrategy: {
|
||||
type: "number",
|
||||
},
|
||||
folderPath: {
|
||||
type: "string",
|
||||
},
|
||||
syncedFiles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
},
|
||||
id: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ignoredFiles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const watchStore = new Store({
|
||||
name: "watch-status",
|
||||
schema: watchStoreSchema,
|
||||
});
|
73
desktop/src/main/stores/watch.ts
Normal file
73
desktop/src/main/stores/watch.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import Store, { Schema } from "electron-store";
|
||||
import { type FolderWatch } from "../../types/ipc";
|
||||
import log from "../log";
|
||||
|
||||
interface WatchStore {
|
||||
mappings: FolderWatchWithLegacyFields[];
|
||||
}
|
||||
|
||||
type FolderWatchWithLegacyFields = FolderWatch & {
|
||||
/** @deprecated Only retained for migration, do not use in other code */
|
||||
rootFolderName?: string;
|
||||
/** @deprecated Only retained for migration, do not use in other code */
|
||||
uploadStrategy?: number;
|
||||
};
|
||||
|
||||
const watchStoreSchema: Schema<WatchStore> = {
|
||||
mappings: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
rootFolderName: { type: "string" },
|
||||
collectionMapping: { type: "string" },
|
||||
uploadStrategy: { type: "number" },
|
||||
folderPath: { type: "string" },
|
||||
syncedFiles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
uploadedFileID: { type: "number" },
|
||||
collectionID: { type: "number" },
|
||||
},
|
||||
},
|
||||
},
|
||||
ignoredFiles: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const watchStore = new Store({
|
||||
name: "watch-status",
|
||||
schema: watchStoreSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Previous versions of the store used to store an integer to indicate the
|
||||
* collection mapping, migrate these to the new schema if we encounter them.
|
||||
*/
|
||||
export const migrateLegacyWatchStoreIfNeeded = () => {
|
||||
let needsUpdate = false;
|
||||
const watches = watchStore.get("mappings")?.map((watch) => {
|
||||
let collectionMapping = watch.collectionMapping;
|
||||
if (!collectionMapping) {
|
||||
collectionMapping = watch.uploadStrategy == 1 ? "parent" : "root";
|
||||
needsUpdate = true;
|
||||
}
|
||||
if (watch.rootFolderName) {
|
||||
delete watch.rootFolderName;
|
||||
needsUpdate = true;
|
||||
}
|
||||
return { ...watch, collectionMapping };
|
||||
});
|
||||
if (needsUpdate) {
|
||||
watchStore.set("mappings", watches);
|
||||
log.info("Migrated legacy watch store data to new schema");
|
||||
}
|
||||
};
|
116
desktop/src/main/stream.ts
Normal file
116
desktop/src/main/stream.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* @file stream data to-from renderer using a custom protocol handler.
|
||||
*/
|
||||
import { protocol } from "electron/main";
|
||||
import { createWriteStream, existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Readable } from "node:stream";
|
||||
import log from "./log";
|
||||
|
||||
/**
|
||||
* Register a protocol handler that we use for streaming large files between the
|
||||
* main process (node) and the renderer process (browser) layer.
|
||||
*
|
||||
* [Note: IPC streams]
|
||||
*
|
||||
* When running without node integration, there is no direct way to pass streams
|
||||
* across IPC. And passing the entire contents of the file is not feasible for
|
||||
* large video files because of the memory pressure the copying would entail.
|
||||
*
|
||||
* As an alternative, we register a custom protocol handler that can provided a
|
||||
* bi-directional stream. The renderer can stream data to the node side by
|
||||
* streaming the request. The node side can stream to the renderer side by
|
||||
* streaming the response.
|
||||
*
|
||||
* See also: [Note: Transferring large amount of data over IPC]
|
||||
*
|
||||
* Depends on {@link registerPrivilegedSchemes}.
|
||||
*/
|
||||
export const registerStreamProtocol = () => {
|
||||
protocol.handle("stream", async (request: Request) => {
|
||||
const url = request.url;
|
||||
const { host, pathname } = new URL(url);
|
||||
// Convert e.g. "%20" to spaces.
|
||||
const path = decodeURIComponent(pathname);
|
||||
switch (host) {
|
||||
/* stream://write/path/to/file */
|
||||
/* host-pathname----- */
|
||||
case "write":
|
||||
try {
|
||||
await writeStream(path, request.body);
|
||||
return new Response("", { status: 200 });
|
||||
} catch (e) {
|
||||
log.error(`Failed to write stream for ${url}`, e);
|
||||
return new Response(
|
||||
`Failed to write stream: ${e.message}`,
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
default:
|
||||
return new Response("", { status: 404 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Write a (web) ReadableStream to a file at the given {@link filePath}.
|
||||
*
|
||||
* The returned promise resolves when the write completes.
|
||||
*
|
||||
* @param filePath The local filesystem path where the file should be written.
|
||||
* @param readableStream A [web
|
||||
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
*/
|
||||
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
|
||||
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
|
||||
|
||||
/**
|
||||
* Convert a Web ReadableStream into a Node.js ReadableStream
|
||||
*
|
||||
* This can be used to, for example, write a ReadableStream obtained via
|
||||
* `net.fetch` into a file using the Node.js `fs` APIs
|
||||
*/
|
||||
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
|
||||
const reader = readableStream.getReader();
|
||||
const rs = new Readable();
|
||||
|
||||
rs._read = async () => {
|
||||
try {
|
||||
const result = await reader.read();
|
||||
|
||||
if (!result.done) {
|
||||
rs.push(Buffer.from(result.value));
|
||||
} else {
|
||||
rs.push(null);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
rs.emit("error", e);
|
||||
}
|
||||
};
|
||||
|
||||
return rs;
|
||||
};
|
||||
|
||||
const writeNodeStream = async (
|
||||
filePath: string,
|
||||
fileStream: NodeJS.ReadableStream,
|
||||
) => {
|
||||
const writeable = createWriteStream(filePath);
|
||||
|
||||
fileStream.on("error", (error) => {
|
||||
writeable.destroy(error); // Close the writable stream with an error
|
||||
});
|
||||
|
||||
fileStream.pipe(writeable);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeable.on("finish", resolve);
|
||||
writeable.on("error", async (e: unknown) => {
|
||||
if (existsSync(filePath)) {
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -56,6 +56,13 @@ export const openDirectory = async (dirPath: string) => {
|
|||
if (res) throw new Error(`Failed to open directory ${dirPath}: res`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the app's log directory in the system's folder viewer.
|
||||
*
|
||||
* @see {@link openDirectory}
|
||||
*/
|
||||
export const openLogDirectory = () => openDirectory(logDirectoryPath());
|
||||
|
||||
/**
|
||||
* Return the path where the logs for the app are saved.
|
||||
*
|
||||
|
@ -72,10 +79,3 @@ export const openDirectory = async (dirPath: string) => {
|
|||
*
|
||||
*/
|
||||
const logDirectoryPath = () => app.getPath("logs");
|
||||
|
||||
/**
|
||||
* Open the app's log directory in the system's folder viewer.
|
||||
*
|
||||
* @see {@link openDirectory}
|
||||
*/
|
||||
export const openLogDirectory = () => openDirectory(logDirectoryPath());
|
||||
|
|
|
@ -40,12 +40,13 @@
|
|||
import { contextBridge, ipcRenderer } from "electron/renderer";
|
||||
|
||||
// While we can't import other code, we can import types since they're just
|
||||
// needed when compiling and will not be needed / looked around for at runtime.
|
||||
// needed when compiling and will not be needed or looked around for at runtime.
|
||||
import type {
|
||||
AppUpdateInfo,
|
||||
AppUpdate,
|
||||
CollectionMapping,
|
||||
ElectronFile,
|
||||
FILE_PATH_TYPE,
|
||||
WatchMapping,
|
||||
FolderWatch,
|
||||
PendingUploads,
|
||||
} from "./types/ipc";
|
||||
|
||||
// - General
|
||||
|
@ -77,12 +78,12 @@ const onMainWindowFocus = (cb?: () => void) => {
|
|||
// - App update
|
||||
|
||||
const onAppUpdateAvailable = (
|
||||
cb?: ((updateInfo: AppUpdateInfo) => void) | undefined,
|
||||
cb?: ((update: AppUpdate) => void) | undefined,
|
||||
) => {
|
||||
ipcRenderer.removeAllListeners("appUpdateAvailable");
|
||||
if (cb) {
|
||||
ipcRenderer.on("appUpdateAvailable", (_, updateInfo: AppUpdateInfo) =>
|
||||
cb(updateInfo),
|
||||
ipcRenderer.on("appUpdateAvailable", (_, update: AppUpdate) =>
|
||||
cb(update),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -96,6 +97,8 @@ const skipAppUpdate = (version: string) => {
|
|||
ipcRenderer.send("skipAppUpdate", version);
|
||||
};
|
||||
|
||||
// - FS
|
||||
|
||||
const fsExists = (path: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("fsExists", path);
|
||||
|
||||
|
@ -110,6 +113,15 @@ const fsRmdir = (path: string): Promise<void> =>
|
|||
|
||||
const fsRm = (path: string): Promise<void> => ipcRenderer.invoke("fsRm", path);
|
||||
|
||||
const fsReadTextFile = (path: string): Promise<string> =>
|
||||
ipcRenderer.invoke("fsReadTextFile", path);
|
||||
|
||||
const fsWriteFile = (path: string, contents: string): Promise<void> =>
|
||||
ipcRenderer.invoke("fsWriteFile", path, contents);
|
||||
|
||||
const fsIsDir = (dirPath: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("fsIsDir", dirPath);
|
||||
|
||||
// - AUDIT below this
|
||||
|
||||
// - Conversion
|
||||
|
@ -180,93 +192,78 @@ const showUploadZipDialog = (): Promise<{
|
|||
|
||||
// - Watch
|
||||
|
||||
const registerWatcherFunctions = (
|
||||
addFile: (file: ElectronFile) => Promise<void>,
|
||||
removeFile: (path: string) => Promise<void>,
|
||||
removeFolder: (folderPath: string) => Promise<void>,
|
||||
) => {
|
||||
ipcRenderer.removeAllListeners("watch-add");
|
||||
ipcRenderer.removeAllListeners("watch-unlink");
|
||||
ipcRenderer.removeAllListeners("watch-unlink-dir");
|
||||
ipcRenderer.on("watch-add", (_, file: ElectronFile) => addFile(file));
|
||||
ipcRenderer.on("watch-unlink", (_, filePath: string) =>
|
||||
removeFile(filePath),
|
||||
);
|
||||
ipcRenderer.on("watch-unlink-dir", (_, folderPath: string) =>
|
||||
removeFolder(folderPath),
|
||||
const watchGet = (): Promise<FolderWatch[]> => ipcRenderer.invoke("watchGet");
|
||||
|
||||
const watchAdd = (
|
||||
folderPath: string,
|
||||
collectionMapping: CollectionMapping,
|
||||
): Promise<FolderWatch[]> =>
|
||||
ipcRenderer.invoke("watchAdd", folderPath, collectionMapping);
|
||||
|
||||
const watchRemove = (folderPath: string): Promise<FolderWatch[]> =>
|
||||
ipcRenderer.invoke("watchRemove", folderPath);
|
||||
|
||||
const watchUpdateSyncedFiles = (
|
||||
syncedFiles: FolderWatch["syncedFiles"],
|
||||
folderPath: string,
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("watchUpdateSyncedFiles", syncedFiles, folderPath);
|
||||
|
||||
const watchUpdateIgnoredFiles = (
|
||||
ignoredFiles: FolderWatch["ignoredFiles"],
|
||||
folderPath: string,
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("watchUpdateIgnoredFiles", ignoredFiles, folderPath);
|
||||
|
||||
const watchOnAddFile = (f: (path: string, watch: FolderWatch) => void) => {
|
||||
ipcRenderer.removeAllListeners("watchAddFile");
|
||||
ipcRenderer.on("watchAddFile", (_, path: string, watch: FolderWatch) =>
|
||||
f(path, watch),
|
||||
);
|
||||
};
|
||||
|
||||
const addWatchMapping = (
|
||||
collectionName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number,
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke(
|
||||
"addWatchMapping",
|
||||
collectionName,
|
||||
folderPath,
|
||||
uploadStrategy,
|
||||
const watchOnRemoveFile = (f: (path: string, watch: FolderWatch) => void) => {
|
||||
ipcRenderer.removeAllListeners("watchRemoveFile");
|
||||
ipcRenderer.on("watchRemoveFile", (_, path: string, watch: FolderWatch) =>
|
||||
f(path, watch),
|
||||
);
|
||||
};
|
||||
|
||||
const removeWatchMapping = (folderPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke("removeWatchMapping", folderPath);
|
||||
const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => {
|
||||
ipcRenderer.removeAllListeners("watchRemoveDir");
|
||||
ipcRenderer.on("watchRemoveDir", (_, path: string, watch: FolderWatch) =>
|
||||
f(path, watch),
|
||||
);
|
||||
};
|
||||
|
||||
const getWatchMappings = (): Promise<WatchMapping[]> =>
|
||||
ipcRenderer.invoke("getWatchMappings");
|
||||
|
||||
const updateWatchMappingSyncedFiles = (
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files);
|
||||
|
||||
const updateWatchMappingIgnoredFiles = (
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files);
|
||||
|
||||
// - FS Legacy
|
||||
|
||||
const saveStreamToDisk = (
|
||||
path: string,
|
||||
fileStream: ReadableStream,
|
||||
): Promise<void> => ipcRenderer.invoke("saveStreamToDisk", path, fileStream);
|
||||
|
||||
const saveFileToDisk = (path: string, contents: string): Promise<void> =>
|
||||
ipcRenderer.invoke("saveFileToDisk", path, contents);
|
||||
|
||||
const readTextFile = (path: string): Promise<string> =>
|
||||
ipcRenderer.invoke("readTextFile", path);
|
||||
|
||||
const isFolder = (dirPath: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("isFolder", dirPath);
|
||||
const watchFindFiles = (folderPath: string): Promise<string[]> =>
|
||||
ipcRenderer.invoke("watchFindFiles", folderPath);
|
||||
|
||||
// - Upload
|
||||
|
||||
const getPendingUploads = (): Promise<{
|
||||
files: ElectronFile[];
|
||||
collectionName: string;
|
||||
type: string;
|
||||
}> => ipcRenderer.invoke("getPendingUploads");
|
||||
const pendingUploads = (): Promise<PendingUploads | undefined> =>
|
||||
ipcRenderer.invoke("pendingUploads");
|
||||
|
||||
const setToUploadFiles = (
|
||||
type: FILE_PATH_TYPE,
|
||||
const setPendingUploadCollection = (collectionName: string): Promise<void> =>
|
||||
ipcRenderer.invoke("setPendingUploadCollection", collectionName);
|
||||
|
||||
const setPendingUploadFiles = (
|
||||
type: PendingUploads["type"],
|
||||
filePaths: string[],
|
||||
): Promise<void> => ipcRenderer.invoke("setToUploadFiles", type, filePaths);
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("setPendingUploadFiles", type, filePaths);
|
||||
|
||||
// -
|
||||
|
||||
const getElectronFilesFromGoogleZip = (
|
||||
filePath: string,
|
||||
): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("getElectronFilesFromGoogleZip", filePath);
|
||||
|
||||
const setToUploadCollection = (collectionName: string): Promise<void> =>
|
||||
ipcRenderer.invoke("setToUploadCollection", collectionName);
|
||||
|
||||
const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("getDirFiles", dirPath);
|
||||
|
||||
//
|
||||
// These objects exposed here will become available to the JS code in our
|
||||
// renderer (the web/ code) as `window.ElectronAPIs.*`
|
||||
//
|
||||
|
@ -299,8 +296,12 @@ const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
|
|||
//
|
||||
// The copy itself is relatively fast, but the problem with transfering large
|
||||
// amounts of data is potentially running out of memory during the copy.
|
||||
//
|
||||
// For an alternative, see [Note: IPC streams].
|
||||
//
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
// - General
|
||||
|
||||
appVersion,
|
||||
logToDisk,
|
||||
openDirectory,
|
||||
|
@ -311,57 +312,67 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
onMainWindowFocus,
|
||||
|
||||
// - App update
|
||||
|
||||
onAppUpdateAvailable,
|
||||
updateAndRestart,
|
||||
updateOnNextRestart,
|
||||
skipAppUpdate,
|
||||
|
||||
// - Conversion
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
runFFmpegCmd,
|
||||
|
||||
// - ML
|
||||
clipImageEmbedding,
|
||||
clipTextEmbedding,
|
||||
detectFaces,
|
||||
faceEmbedding,
|
||||
|
||||
// - File selection
|
||||
selectDirectory,
|
||||
showUploadFilesDialog,
|
||||
showUploadDirsDialog,
|
||||
showUploadZipDialog,
|
||||
|
||||
// - Watch
|
||||
registerWatcherFunctions,
|
||||
addWatchMapping,
|
||||
removeWatchMapping,
|
||||
getWatchMappings,
|
||||
updateWatchMappingSyncedFiles,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
|
||||
// - FS
|
||||
|
||||
fs: {
|
||||
exists: fsExists,
|
||||
rename: fsRename,
|
||||
mkdirIfNeeded: fsMkdirIfNeeded,
|
||||
rmdir: fsRmdir,
|
||||
rm: fsRm,
|
||||
readTextFile: fsReadTextFile,
|
||||
writeFile: fsWriteFile,
|
||||
isDir: fsIsDir,
|
||||
},
|
||||
|
||||
// - FS legacy
|
||||
// TODO: Move these into fs + document + rename if needed
|
||||
saveStreamToDisk,
|
||||
saveFileToDisk,
|
||||
readTextFile,
|
||||
isFolder,
|
||||
// - Conversion
|
||||
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
runFFmpegCmd,
|
||||
|
||||
// - ML
|
||||
|
||||
clipImageEmbedding,
|
||||
clipTextEmbedding,
|
||||
detectFaces,
|
||||
faceEmbedding,
|
||||
|
||||
// - File selection
|
||||
|
||||
selectDirectory,
|
||||
showUploadFilesDialog,
|
||||
showUploadDirsDialog,
|
||||
showUploadZipDialog,
|
||||
|
||||
// - Watch
|
||||
|
||||
watch: {
|
||||
get: watchGet,
|
||||
add: watchAdd,
|
||||
remove: watchRemove,
|
||||
onAddFile: watchOnAddFile,
|
||||
onRemoveFile: watchOnRemoveFile,
|
||||
onRemoveDir: watchOnRemoveDir,
|
||||
findFiles: watchFindFiles,
|
||||
updateSyncedFiles: watchUpdateSyncedFiles,
|
||||
updateIgnoredFiles: watchUpdateIgnoredFiles,
|
||||
},
|
||||
|
||||
// - Upload
|
||||
|
||||
getPendingUploads,
|
||||
setToUploadFiles,
|
||||
pendingUploads,
|
||||
setPendingUploadCollection,
|
||||
setPendingUploadFiles,
|
||||
|
||||
// -
|
||||
|
||||
getElectronFilesFromGoogleZip,
|
||||
setToUploadCollection,
|
||||
getDirFiles,
|
||||
});
|
||||
|
|
|
@ -5,6 +5,32 @@
|
|||
* See [Note: types.ts <-> preload.ts <-> ipc.ts]
|
||||
*/
|
||||
|
||||
export interface AppUpdate {
|
||||
autoUpdatable: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface FolderWatch {
|
||||
collectionMapping: CollectionMapping;
|
||||
folderPath: string;
|
||||
syncedFiles: FolderWatchSyncedFile[];
|
||||
ignoredFiles: string[];
|
||||
}
|
||||
|
||||
export type CollectionMapping = "root" | "parent";
|
||||
|
||||
export interface FolderWatchSyncedFile {
|
||||
path: string;
|
||||
uploadedFileID: number;
|
||||
collectionID: number;
|
||||
}
|
||||
|
||||
export interface PendingUploads {
|
||||
collectionName: string;
|
||||
type: "files" | "zips";
|
||||
files: ElectronFile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors that have special semantics on the web side.
|
||||
*
|
||||
|
@ -51,32 +77,3 @@ export interface ElectronFile {
|
|||
blob: () => Promise<Blob>;
|
||||
arrayBuffer: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
interface WatchMappingSyncedFile {
|
||||
path: string;
|
||||
uploadedFileID: number;
|
||||
collectionID: number;
|
||||
}
|
||||
|
||||
export interface WatchMapping {
|
||||
rootFolderName: string;
|
||||
uploadStrategy: number;
|
||||
folderPath: string;
|
||||
syncedFiles: WatchMappingSyncedFile[];
|
||||
ignoredFiles: string[];
|
||||
}
|
||||
|
||||
export interface WatchStoreType {
|
||||
mappings: WatchMapping[];
|
||||
}
|
||||
|
||||
export enum FILE_PATH_TYPE {
|
||||
/* eslint-disable no-unused-vars */
|
||||
FILES = "files",
|
||||
ZIPS = "zips",
|
||||
}
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
autoUpdatable: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { FILE_PATH_TYPE } from "./ipc";
|
||||
|
||||
export interface AutoLauncherClient {
|
||||
isEnabled: () => Promise<boolean>;
|
||||
toggleAutoLaunch: () => Promise<void>;
|
||||
wasAutoLaunched: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface UploadStoreType {
|
||||
filePaths: string[];
|
||||
zipPaths: string[];
|
||||
collectionName: string;
|
||||
}
|
||||
|
||||
export interface KeysStoreType {
|
||||
AnonymizeUserID: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export const FILE_PATH_KEYS: {
|
||||
[k in FILE_PATH_TYPE]: keyof UploadStoreType;
|
||||
} = {
|
||||
[FILE_PATH_TYPE.ZIPS]: "zipPaths",
|
||||
[FILE_PATH_TYPE.FILES]: "filePaths",
|
||||
};
|
||||
|
||||
export interface SafeStorageStoreType {
|
||||
encryptionKey: string;
|
||||
}
|
|
@ -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==
|
||||
|
|
|
@ -110,10 +110,10 @@ or "dog playing at the beach".
|
|||
Check the sections within the upload progress bar for "Failed Uploads," "Ignored
|
||||
Uploads," and "Unsuccessful Uploads."
|
||||
|
||||
## How do i keep NAS and Ente photos synced?
|
||||
## How do I keep NAS and Ente photos synced?
|
||||
|
||||
Please try using our CLI to pull data into your NAS
|
||||
https://github.com/ente-io/ente/tree/main/cli#readme .
|
||||
https://github.com/ente-io/ente/tree/main/cli#readme.
|
||||
|
||||
## Is there a way to view all albums on the map view?
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ You can alternatively install the build from PlayStore or F-Droid.
|
|||
|
||||
## 🧑💻 Building from source
|
||||
|
||||
1. [Install Flutter v3.19.5](https://flutter.dev/docs/get-started/install).
|
||||
1. [Install Flutter v3.19.3](https://flutter.dev/docs/get-started/install).
|
||||
|
||||
2. Pull in all submodules with `git submodule update --init --recursive`
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ linter:
|
|||
- use_key_in_widget_constructors
|
||||
- cancel_subscriptions
|
||||
|
||||
|
||||
- avoid_empty_else
|
||||
- exhaustive_cases
|
||||
|
||||
|
@ -59,6 +60,7 @@ analyzer:
|
|||
prefer_final_locals: warning
|
||||
unnecessary_const: error
|
||||
cancel_subscriptions: error
|
||||
unrelated_type_equality_checks: error
|
||||
|
||||
|
||||
unawaited_futures: warning # convert to warning after fixing existing issues
|
||||
|
|
|
@ -3,12 +3,9 @@ PODS:
|
|||
- Flutter
|
||||
- battery_info (0.0.1):
|
||||
- Flutter
|
||||
- bonsoir_darwin (3.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
|
@ -171,7 +168,6 @@ PODS:
|
|||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- ReachabilitySwift (5.2.1)
|
||||
- receive_sharing_intent (1.6.8):
|
||||
- Flutter
|
||||
- screen_brightness_ios (0.1.0):
|
||||
|
@ -231,8 +227,7 @@ PODS:
|
|||
DEPENDENCIES:
|
||||
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
|
||||
- battery_info (from `.symlinks/plugins/battery_info/ios`)
|
||||
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
|
@ -296,7 +291,6 @@ SPEC REPOS:
|
|||
- onnxruntime-objc
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- ReachabilitySwift
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- Sentry
|
||||
|
@ -309,10 +303,8 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/background_fetch/ios"
|
||||
battery_info:
|
||||
:path: ".symlinks/plugins/battery_info/ios"
|
||||
bonsoir_darwin:
|
||||
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_saver:
|
||||
|
@ -409,8 +401,7 @@ EXTERNAL SOURCES:
|
|||
SPEC CHECKSUMS:
|
||||
background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a
|
||||
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
|
||||
bonsoir_darwin: 127bdc632fdc154ae2f277a4d5c86a6212bc75be
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: 797fd7297b7e1be954432743a0b3f90038e45a71
|
||||
|
@ -458,7 +449,6 @@ SPEC CHECKSUMS:
|
|||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66
|
||||
receive_sharing_intent: 6837b01768e567fe8562182397bf43d63d8c6437
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb
|
||||
|
|
|
@ -285,7 +285,6 @@
|
|||
"${BUILT_PRODUCTS_DIR}/Mantle/Mantle.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
|
||||
|
@ -293,7 +292,6 @@
|
|||
"${BUILT_PRODUCTS_DIR}/Toast/Toast.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/bonsoir_darwin/bonsoir_darwin.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
|
||||
|
@ -369,7 +367,6 @@
|
|||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mantle.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
|
||||
|
@ -377,7 +374,6 @@
|
|||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bonsoir_darwin.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
|
||||
|
|
|
@ -13,18 +13,13 @@ import 'package:media_extension/media_extension_action_types.dart';
|
|||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/collection/collection_items.dart";
|
||||
import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/favorites_service.dart";
|
||||
import "package:photos/services/home_widget_service.dart";
|
||||
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/ui/tabs/home_widget.dart';
|
||||
import "package:photos/ui/viewer/actions/file_viewer.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import "package:photos/utils/intent_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
|
||||
class EnteApp extends StatefulWidget {
|
||||
final Future<void> Function(String) runBackgroundTask;
|
||||
|
@ -66,39 +61,14 @@ class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
|
|||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_checkForWidgetLaunch();
|
||||
hw.HomeWidget.widgetClicked.listen(_launchedFromWidget);
|
||||
}
|
||||
|
||||
void _checkForWidgetLaunch() {
|
||||
hw.HomeWidget.initiallyLaunchedFromHomeWidget().then(_launchedFromWidget);
|
||||
}
|
||||
|
||||
Future<void> _launchedFromWidget(Uri? uri) async {
|
||||
if (uri == null) return;
|
||||
final collectionID =
|
||||
await FavoritesService.instance.getFavoriteCollectionID();
|
||||
if (collectionID == null) {
|
||||
return;
|
||||
}
|
||||
final collection = CollectionsService.instance.getCollectionByID(
|
||||
collectionID,
|
||||
hw.HomeWidget.initiallyLaunchedFromHomeWidget().then(
|
||||
(uri) => HomeWidgetService.instance.onLaunchFromWidget(uri, context),
|
||||
);
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
unawaited(HomeWidgetService.instance.initHomeWidget());
|
||||
|
||||
final thumbnail = await CollectionsService.instance.getCover(collection);
|
||||
unawaited(
|
||||
routeToPage(
|
||||
context,
|
||||
CollectionPage(
|
||||
CollectionWithThumbnail(
|
||||
collection,
|
||||
thumbnail,
|
||||
),
|
||||
),
|
||||
),
|
||||
hw.HomeWidget.widgetClicked.listen(
|
||||
(uri) => HomeWidgetService.instance.onLaunchFromWidget(uri, context),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,9 @@ class FFDefault {
|
|||
static const bool enablePasskey = false;
|
||||
}
|
||||
|
||||
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
|
||||
const multipartPartSize = 20 * 1024 * 1024;
|
||||
|
||||
const kDefaultProductionEndpoint = 'https://api.ente.io';
|
||||
|
||||
const int intMaxValue = 9223372036854775807;
|
||||
|
@ -71,11 +74,11 @@ const kSearchSectionLimit = 9;
|
|||
|
||||
const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
|
||||
|
||||
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' +
|
||||
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ' +
|
||||
'EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC' +
|
||||
'ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF' +
|
||||
'BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk' +
|
||||
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'
|
||||
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ'
|
||||
'EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC'
|
||||
'ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF'
|
||||
'BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk' +
|
||||
'6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL' +
|
||||
'W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA' +
|
||||
'AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY' +
|
||||
|
|
|
@ -340,7 +340,7 @@ extension DeviceFiles on FilesDB {
|
|||
int ownerID,
|
||||
) async {
|
||||
final db = await database;
|
||||
const String rawQuery = '''
|
||||
const String rawQuery = '''
|
||||
SELECT ${FilesDB.columnLocalID}, ${FilesDB.columnUploadedFileID},
|
||||
${FilesDB.columnFileSize}
|
||||
FROM ${FilesDB.filesTable}
|
||||
|
|
|
@ -16,7 +16,6 @@ import "package:photos/services/filter/db_filters.dart";
|
|||
import 'package:photos/utils/file_uploader_util.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:sqflite_migration/sqflite_migration.dart';
|
||||
import 'package:sqlite3/sqlite3.dart' as sqlite3;
|
||||
import 'package:sqlite_async/sqlite_async.dart' as sqlite_async;
|
||||
|
||||
class FilesDB {
|
||||
|
@ -103,20 +102,15 @@ class FilesDB {
|
|||
|
||||
// only have a single app-wide reference to the database
|
||||
static Future<Database>? _dbFuture;
|
||||
static Future<sqlite3.Database>? _ffiDBFuture;
|
||||
static Future<sqlite_async.SqliteDatabase>? _sqliteAsyncDBFuture;
|
||||
|
||||
@Deprecated("Use sqliteAsyncDB instead (sqlite_async)")
|
||||
Future<Database> get database async {
|
||||
// lazily instantiate the db the first time it is accessed
|
||||
_dbFuture ??= _initDatabase();
|
||||
return _dbFuture!;
|
||||
}
|
||||
|
||||
Future<sqlite3.Database> get ffiDB async {
|
||||
_ffiDBFuture ??= _initFFIDatabase();
|
||||
return _ffiDBFuture!;
|
||||
}
|
||||
|
||||
Future<sqlite_async.SqliteDatabase> get sqliteAsyncDB async {
|
||||
_sqliteAsyncDBFuture ??= _initSqliteAsyncDatabase();
|
||||
return _sqliteAsyncDBFuture!;
|
||||
|
@ -131,14 +125,6 @@ class FilesDB {
|
|||
return await openDatabaseWithMigration(path, dbConfig);
|
||||
}
|
||||
|
||||
Future<sqlite3.Database> _initFFIDatabase() async {
|
||||
final Directory documentsDirectory =
|
||||
await getApplicationDocumentsDirectory();
|
||||
final String path = join(documentsDirectory.path, _databaseName);
|
||||
_logger.info("DB path " + path);
|
||||
return sqlite3.sqlite3.open(path);
|
||||
}
|
||||
|
||||
Future<sqlite_async.SqliteDatabase> _initSqliteAsyncDatabase() async {
|
||||
final Directory documentsDirectory =
|
||||
await getApplicationDocumentsDirectory();
|
||||
|
@ -478,11 +464,10 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<EnteFile?> getFile(int generatedID) async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where: '$columnGeneratedID = ?',
|
||||
whereArgs: [generatedID],
|
||||
final db = await instance.sqliteAsyncDB;
|
||||
final results = await db.getAll(
|
||||
'SELECT * FROM $filesTable WHERE $columnGeneratedID = ?',
|
||||
[generatedID],
|
||||
);
|
||||
if (results.isEmpty) {
|
||||
return null;
|
||||
|
@ -491,11 +476,10 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<EnteFile?> getUploadedFile(int uploadedID, int collectionID) async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where: '$columnUploadedFileID = ? AND $columnCollectionID = ?',
|
||||
whereArgs: [
|
||||
final db = await instance.sqliteAsyncDB;
|
||||
final results = await db.getAll(
|
||||
'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ? AND $columnCollectionID = ?',
|
||||
[
|
||||
uploadedID,
|
||||
collectionID,
|
||||
],
|
||||
|
@ -506,29 +490,12 @@ class FilesDB {
|
|||
return convertToFiles(results)[0];
|
||||
}
|
||||
|
||||
Future<EnteFile?> getAnyUploadedFile(int uploadedID) async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where: '$columnUploadedFileID = ?',
|
||||
whereArgs: [
|
||||
uploadedID,
|
||||
],
|
||||
);
|
||||
if (results.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return convertToFiles(results)[0];
|
||||
}
|
||||
|
||||
Future<Set<int>> getUploadedFileIDs(int collectionID) async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
columns: [columnUploadedFileID],
|
||||
where:
|
||||
'$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
|
||||
whereArgs: [
|
||||
final db = await instance.sqliteAsyncDB;
|
||||
final results = await db.getAll(
|
||||
'SELECT $columnUploadedFileID FROM $filesTable'
|
||||
' WHERE $columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
|
||||
[
|
||||
collectionID,
|
||||
],
|
||||
);
|
||||
|
@ -540,12 +507,10 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<BackedUpFileIDs> getBackedUpIDs() async {
|
||||
final db = await instance.database;
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
columns: [columnLocalID, columnUploadedFileID, columnFileSize],
|
||||
where:
|
||||
'$columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
|
||||
final db = await instance.sqliteAsyncDB;
|
||||
final results = await db.getAll(
|
||||
'SELECT $columnLocalID, $columnUploadedFileID, $columnFileSize FROM $filesTable'
|
||||
' WHERE $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)',
|
||||
);
|
||||
final Set<String> localIDs = <String>{};
|
||||
final Set<int> uploadedIDs = <int>{};
|
||||
|
@ -681,13 +646,12 @@ class FilesDB {
|
|||
}
|
||||
|
||||
Future<List<EnteFile>> getAllFilesCollection(int collectionID) async {
|
||||
final db = await instance.database;
|
||||
final db = await instance.sqliteAsyncDB;
|
||||
const String whereClause = '$columnCollectionID = ?';
|
||||
final List<Object> whereArgs = [collectionID];
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where: whereClause,
|
||||
whereArgs: whereArgs,
|
||||
final results = await db.getAll(
|
||||
'SELECT * FROM $filesTable WHERE $whereClause',
|
||||
whereArgs,
|
||||
);
|
||||
final files = convertToFiles(results);
|
||||
return files;
|
||||
|
@ -697,14 +661,13 @@ class FilesDB {
|
|||
int collectionID,
|
||||
int addedTime,
|
||||
) async {
|
||||
final db = await instance.database;
|
||||
final db = await instance.sqliteAsyncDB;
|
||||
const String whereClause =
|
||||
'$columnCollectionID = ? AND $columnAddedTime > ?';
|
||||
final List<Object> whereArgs = [collectionID, addedTime];
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where: whereClause,
|
||||
whereArgs: whereArgs,
|
||||
final results = await db.getAll(
|
||||
'SELECT * FROM $filesTable WHERE $whereClause',
|
||||
whereArgs,
|
||||
);
|
||||
final files = convertToFiles(results);
|
||||
return files;
|
||||
|
@ -726,20 +689,22 @@ class FilesDB {
|
|||
inParam += "'" + id.toString() + "',";
|
||||
}
|
||||
inParam = inParam.substring(0, inParam.length - 1);
|
||||
final db = await instance.database;
|
||||
final db = await instance.sqliteAsyncDB;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final String whereClause =
|
||||
'$columnCollectionID IN ($inParam) AND $columnCreationTime >= ? AND '
|
||||
'$columnCreationTime <= ? AND $columnOwnerID = ?';
|
||||
final List<Object> whereArgs = [startTime, endTime, userID];
|
||||
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where: whereClause,
|
||||
whereArgs: whereArgs,
|
||||
orderBy:
|
||||
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
|
||||
limit: limit,
|
||||
String query = 'SELECT * FROM $filesTable WHERE $whereClause ORDER BY '
|
||||
'$columnCreationTime $order, $columnModificationTime $order';
|
||||
if (limit != null) {
|
||||
query += ' LIMIT ?';
|
||||
whereArgs.add(limit);
|
||||
}
|
||||
final results = await db.getAll(
|
||||
query,
|
||||
whereArgs,
|
||||
);
|
||||
final files = convertToFiles(results);
|
||||
final dedupeResult =
|
||||
|
@ -757,7 +722,7 @@ class FilesDB {
|
|||
if (durations.isEmpty) {
|
||||
return <EnteFile>[];
|
||||
}
|
||||
final db = await instance.database;
|
||||
final db = await instance.sqliteAsyncDB;
|
||||
String whereClause = "( ";
|
||||
for (int index = 0; index < durations.length; index++) {
|
||||
whereClause += "($columnCreationTime >= " +
|
||||
|
@ -772,44 +737,10 @@ class FilesDB {
|
|||
}
|
||||
}
|
||||
whereClause += ")";
|
||||
final results = await db.query(
|
||||
filesTable,
|
||||
where: whereClause,
|
||||
orderBy: '$columnCreationTime ' + order,
|
||||
);
|
||||
final files = convertToFiles(results);
|
||||
return applyDBFilters(
|
||||
files,
|
||||
DBFilterOptions(ignoredCollectionIDs: ignoredCollectionIDs),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<EnteFile>> getFilesCreatedWithinDurationsSync(
|
||||
List<List<int>> durations,
|
||||
Set<int> ignoredCollectionIDs, {
|
||||
int? visibility,
|
||||
String order = 'ASC',
|
||||
}) async {
|
||||
if (durations.isEmpty) {
|
||||
return <EnteFile>[];
|
||||
}
|
||||
final db = await instance.ffiDB;
|
||||
String whereClause = "( ";
|
||||
for (int index = 0; index < durations.length; index++) {
|
||||
whereClause += "($columnCreationTime >= " +
|
||||
durations[index][0].toString() +
|
||||
" AND $columnCreationTime < " +
|
||||
durations[index][1].toString() +
|
||||
")";
|
||||
if (index != durations.length - 1) {
|
||||
whereClause += " OR ";
|
||||
} else if (visibility != null) {
|
||||
whereClause += ' AND $columnMMdVisibility = $visibility';
|
||||
}
|
||||
}
|
||||
whereClause += ")";
|
||||
final results = db.select(
|
||||
'select * from $filesTable where $whereClause order by $columnCreationTime $order',
|
||||
final query =
|
||||
'SELECT * FROM $filesTable WHERE $whereClause ORDER BY $columnCreationTime $order';
|
||||
final results = await db.getAll(
|
||||
query,
|
||||
);
|
||||
final files = convertToFiles(results);
|
||||
return applyDBFilters(
|
||||
|
|
1
mobile/lib/generated/intl/messages_cs.dart
generated
1
mobile/lib/generated/intl/messages_cs.dart
generated
|
@ -55,6 +55,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"Modify your query, or try searching for"),
|
||||
"moveToHiddenAlbum":
|
||||
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"selectALocation":
|
||||
MessageLookupByLibrary.simpleMessage("Select a location"),
|
||||
"selectALocationFirst":
|
||||
|
|
1
mobile/lib/generated/intl/messages_de.dart
generated
1
mobile/lib/generated/intl/messages_de.dart
generated
|
@ -1213,6 +1213,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scanne diesen Code mit \ndeiner Authentifizierungs-App"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Alben"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_en.dart
generated
1
mobile/lib/generated/intl/messages_en.dart
generated
|
@ -1175,6 +1175,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scan this barcode with\nyour authenticator app"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Albums"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_es.dart
generated
1
mobile/lib/generated/intl/messages_es.dart
generated
|
@ -1044,6 +1044,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Escanea este código QR con tu aplicación de autenticación"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchByAlbumNameHint":
|
||||
MessageLookupByLibrary.simpleMessage("Nombre del álbum"),
|
||||
"searchByExamples": MessageLookupByLibrary.simpleMessage(
|
||||
|
|
1
mobile/lib/generated/intl/messages_fr.dart
generated
1
mobile/lib/generated/intl/messages_fr.dart
generated
|
@ -1182,6 +1182,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scannez ce code-barres avec\nvotre application d\'authentification"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Albums"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_it.dart
generated
1
mobile/lib/generated/intl/messages_it.dart
generated
|
@ -1137,6 +1137,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scansione questo codice QR\ncon la tua app di autenticazione"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchByAlbumNameHint":
|
||||
MessageLookupByLibrary.simpleMessage("Nome album"),
|
||||
"searchByExamples": MessageLookupByLibrary.simpleMessage(
|
||||
|
|
1
mobile/lib/generated/intl/messages_ko.dart
generated
1
mobile/lib/generated/intl/messages_ko.dart
generated
|
@ -55,6 +55,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"Modify your query, or try searching for"),
|
||||
"moveToHiddenAlbum":
|
||||
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"selectALocation":
|
||||
MessageLookupByLibrary.simpleMessage("Select a location"),
|
||||
"selectALocationFirst":
|
||||
|
|
1
mobile/lib/generated/intl/messages_nl.dart
generated
1
mobile/lib/generated/intl/messages_nl.dart
generated
|
@ -1206,6 +1206,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Scan deze barcode met\nje authenticator app"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Albums"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_no.dart
generated
1
mobile/lib/generated/intl/messages_no.dart
generated
|
@ -77,6 +77,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"Modify your query, or try searching for"),
|
||||
"moveToHiddenAlbum":
|
||||
MessageLookupByLibrary.simpleMessage("Move to hidden album"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"selectALocation":
|
||||
MessageLookupByLibrary.simpleMessage("Select a location"),
|
||||
"selectALocationFirst":
|
||||
|
|
1
mobile/lib/generated/intl/messages_pl.dart
generated
1
mobile/lib/generated/intl/messages_pl.dart
generated
|
@ -171,6 +171,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"resetPasswordTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Zresetuj hasło"),
|
||||
"saveKey": MessageLookupByLibrary.simpleMessage("Zapisz klucz"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"selectALocation":
|
||||
MessageLookupByLibrary.simpleMessage("Select a location"),
|
||||
"selectALocationFirst":
|
||||
|
|
1
mobile/lib/generated/intl/messages_pt.dart
generated
1
mobile/lib/generated/intl/messages_pt.dart
generated
|
@ -1217,6 +1217,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Escaneie este código de barras com\nseu aplicativo autenticador"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Álbuns"),
|
||||
"searchByAlbumNameHint":
|
||||
|
|
1
mobile/lib/generated/intl/messages_zh.dart
generated
1
mobile/lib/generated/intl/messages_zh.dart
generated
|
@ -988,6 +988,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"scanCode": MessageLookupByLibrary.simpleMessage("扫描二维码/条码"),
|
||||
"scanThisBarcodeWithnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage("用您的身份验证器应用\n扫描此条码"),
|
||||
"search": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"searchAlbumsEmptySection": MessageLookupByLibrary.simpleMessage("相册"),
|
||||
"searchByAlbumNameHint": MessageLookupByLibrary.simpleMessage("相册名称"),
|
||||
"searchByExamples": MessageLookupByLibrary.simpleMessage(
|
||||
|
|
10
mobile/lib/generated/l10n.dart
generated
10
mobile/lib/generated/l10n.dart
generated
|
@ -8553,6 +8553,16 @@ class S {
|
|||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Search`
|
||||
String get search {
|
||||
return Intl.message(
|
||||
'Search',
|
||||
name: 'search',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1203,5 +1203,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1211,5 +1211,6 @@
|
|||
"invalidEndpointMessage": "Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.",
|
||||
"endpointUpdatedMessage": "Endpoint updated successfully",
|
||||
"customEndpoint": "Connected to {endpoint}",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -979,5 +979,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1160,5 +1160,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1122,5 +1122,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -17,5 +17,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -23,7 +23,7 @@
|
|||
"sendEmail": "E-mail versturen",
|
||||
"deleteRequestSLAText": "Je verzoek wordt binnen 72 uur verwerkt.",
|
||||
"deleteEmailRequest": "Stuur een e-mail naar <warning>account-deletion@ente.io</warning> vanaf het door jou geregistreerde e-mailadres.",
|
||||
"entePhotosPerm": "ente <i>heeft toestemming nodig om</i> je foto's te bewaren",
|
||||
"entePhotosPerm": "Ente <i>heeft toestemming nodig om</i> je foto's te bewaren",
|
||||
"ok": "Oké",
|
||||
"createAccount": "Account aanmaken",
|
||||
"createNewAccount": "Nieuw account aanmaken",
|
||||
|
@ -225,17 +225,17 @@
|
|||
},
|
||||
"description": "Number of participants in an album, including the album owner."
|
||||
},
|
||||
"collabLinkSectionDescription": "Maak een link waarmee mensen foto's in jouw gedeelde album kunnen toevoegen en bekijken zonder dat ze daarvoor een ente app of account nodig hebben. Handig voor het verzamelen van foto's van evenementen.",
|
||||
"collabLinkSectionDescription": "Maak een link waarmee mensen foto's in jouw gedeelde album kunnen toevoegen en bekijken zonder dat ze daarvoor een Ente app of account nodig hebben. Handig voor het verzamelen van foto's van evenementen.",
|
||||
"collectPhotos": "Foto's verzamelen",
|
||||
"collaborativeLink": "Gezamenlijke link",
|
||||
"shareWithNonenteUsers": "Delen met niet-ente gebruikers",
|
||||
"shareWithNonenteUsers": "Delen met niet-Ente gebruikers",
|
||||
"createPublicLink": "Maak publieke link",
|
||||
"sendLink": "Stuur link",
|
||||
"copyLink": "Kopieer link",
|
||||
"linkHasExpired": "Link is vervallen",
|
||||
"publicLinkEnabled": "Publieke link ingeschakeld",
|
||||
"shareALink": "Deel een link",
|
||||
"sharedAlbumSectionDescription": "Maak gedeelde en collaboratieve albums met andere ente gebruikers, inclusief gebruikers met gratis abonnementen.",
|
||||
"sharedAlbumSectionDescription": "Maak gedeelde en collaboratieve albums met andere Ente gebruikers, inclusief gebruikers met gratis abonnementen.",
|
||||
"shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Deel met specifieke mensen} =1 {Gedeeld met 1 persoon} other {Gedeeld met {numberOfPeople} mensen}}",
|
||||
"@shareWithPeopleSectionTitle": {
|
||||
"placeholders": {
|
||||
|
@ -259,12 +259,12 @@
|
|||
},
|
||||
"verificationId": "Verificatie ID",
|
||||
"verifyEmailID": "Verifieer {email}",
|
||||
"emailNoEnteAccount": "{email} heeft geen ente account.\n\nStuur ze een uitnodiging om foto's te delen.",
|
||||
"emailNoEnteAccount": "{email} heeft geen Ente account.\n\nStuur ze een uitnodiging om foto's te delen.",
|
||||
"shareMyVerificationID": "Hier is mijn verificatie-ID: {verificationID} voor ente.io.",
|
||||
"shareTextConfirmOthersVerificationID": "Hey, kunt u bevestigen dat dit uw ente.io verificatie-ID is: {verificationID}",
|
||||
"somethingWentWrong": "Er ging iets mis",
|
||||
"sendInvite": "Stuur een uitnodiging",
|
||||
"shareTextRecommendUsingEnte": "Download ente zodat we gemakkelijk foto's en video's van originele kwaliteit kunnen delen\n\nhttps://ente.io",
|
||||
"shareTextRecommendUsingEnte": "Download Ente zodat we gemakkelijk foto's en video's in originele kwaliteit kunnen delen\n\nhttps://ente.io",
|
||||
"done": "Voltooid",
|
||||
"applyCodeTitle": "Code toepassen",
|
||||
"enterCodeDescription": "Voer de code van de vriend in om gratis opslag voor jullie beiden te claimen",
|
||||
|
@ -281,7 +281,7 @@
|
|||
"claimMore": "Claim meer!",
|
||||
"theyAlsoGetXGb": "Zij krijgen ook {storageAmountInGB} GB",
|
||||
"freeStorageOnReferralSuccess": "{storageAmountInGB} GB telkens als iemand zich aanmeldt voor een betaald abonnement en je code toepast",
|
||||
"shareTextReferralCode": "ente verwijzingscode: {referralCode} \n\nPas het toe bij Instellingen → Algemeen → Verwijzingen om {referralStorageInGB} GB gratis te krijgen nadat je je hebt aangemeld voor een betaald abonnement\n\nhttps://ente.io",
|
||||
"shareTextReferralCode": "Ente verwijzingscode: {referralCode} \n\nPas het toe bij Instellingen → Algemeen → Verwijzingen om {referralStorageInGB} GB gratis te krijgen nadat je je hebt aangemeld voor een betaald abonnement\n\nhttps://ente.io",
|
||||
"claimFreeStorage": "Claim gratis opslag",
|
||||
"inviteYourFriends": "Vrienden uitnodigen",
|
||||
"failedToFetchReferralDetails": "Kan geen verwijzingsgegevens ophalen. Probeer het later nog eens.",
|
||||
|
@ -304,6 +304,7 @@
|
|||
}
|
||||
},
|
||||
"faq": "Veelgestelde vragen",
|
||||
"help": "Hulp",
|
||||
"oopsSomethingWentWrong": "Oeps, er is iets misgegaan",
|
||||
"peopleUsingYourCode": "Mensen die jouw code gebruiken",
|
||||
"eligible": "gerechtigd",
|
||||
|
@ -333,7 +334,7 @@
|
|||
"removeParticipantBody": "{userEmail} zal worden verwijderd uit dit gedeelde album\n\nAlle door hen toegevoegde foto's worden ook uit het album verwijderd",
|
||||
"keepPhotos": "Foto's behouden",
|
||||
"deletePhotos": "Foto's verwijderen",
|
||||
"inviteToEnte": "Uitnodigen voor ente",
|
||||
"inviteToEnte": "Uitnodigen voor Ente",
|
||||
"removePublicLink": "Verwijder publieke link",
|
||||
"disableLinkMessage": "Dit verwijdert de openbare link voor toegang tot \"{albumName}\".",
|
||||
"sharing": "Delen...",
|
||||
|
@ -349,10 +350,10 @@
|
|||
"videoSmallCase": "video",
|
||||
"photoSmallCase": "foto",
|
||||
"singleFileDeleteHighlight": "Het wordt uit alle albums verwijderd.",
|
||||
"singleFileInBothLocalAndRemote": "Deze {fileType} staat zowel in ente als op jouw apparaat.",
|
||||
"singleFileInRemoteOnly": "Deze {fileType} zal worden verwijderd uit ente.",
|
||||
"singleFileInBothLocalAndRemote": "Deze {fileType} staat zowel in Ente als op jouw apparaat.",
|
||||
"singleFileInRemoteOnly": "Deze {fileType} zal worden verwijderd uit Ente.",
|
||||
"singleFileDeleteFromDevice": "Deze {fileType} zal worden verwijderd van jouw apparaat.",
|
||||
"deleteFromEnte": "Verwijder van ente",
|
||||
"deleteFromEnte": "Verwijder van Ente",
|
||||
"yesDelete": "Ja, verwijderen",
|
||||
"movedToTrash": "Naar prullenbak verplaatst",
|
||||
"deleteFromDevice": "Verwijder van apparaat",
|
||||
|
@ -444,7 +445,7 @@
|
|||
"backupOverMobileData": "Back-up maken via mobiele data",
|
||||
"backupVideos": "Back-up video's",
|
||||
"disableAutoLock": "Automatisch vergrendelen uitschakelen",
|
||||
"deviceLockExplanation": "Schakel de schermvergrendeling van het apparaat uit wanneer ente op de voorgrond is en er een back-up aan de gang is. Dit is normaal gesproken niet nodig, maar kan grote uploads en initiële imports van grote mappen sneller laten verlopen.",
|
||||
"deviceLockExplanation": "Schakel de schermvergrendeling van het apparaat uit wanneer Ente op de voorgrond is en er een back-up aan de gang is. Dit is normaal gesproken niet nodig, maar kan grote uploads en initiële imports van grote mappen sneller laten verlopen.",
|
||||
"about": "Over",
|
||||
"weAreOpenSource": "We zijn open source!",
|
||||
"privacy": "Privacy",
|
||||
|
@ -464,7 +465,7 @@
|
|||
"authToInitiateAccountDeletion": "Gelieve te verifiëren om het verwijderen van je account te starten",
|
||||
"areYouSureYouWantToLogout": "Weet je zeker dat je wilt uitloggen?",
|
||||
"yesLogout": "Ja, log uit",
|
||||
"aNewVersionOfEnteIsAvailable": "Er is een nieuwe versie van ente beschikbaar.",
|
||||
"aNewVersionOfEnteIsAvailable": "Er is een nieuwe versie van Ente beschikbaar.",
|
||||
"update": "Update",
|
||||
"installManually": "Installeer handmatig",
|
||||
"criticalUpdateAvailable": "Belangrijke update beschikbaar",
|
||||
|
@ -553,11 +554,11 @@
|
|||
"systemTheme": "Systeem",
|
||||
"freeTrial": "Gratis proefversie",
|
||||
"selectYourPlan": "Kies uw abonnement",
|
||||
"enteSubscriptionPitch": "ente bewaart uw herinneringen, zodat ze altijd beschikbaar voor u zijn, zelfs als u uw apparaat verliest.",
|
||||
"enteSubscriptionPitch": "Ente bewaart uw herinneringen, zodat ze altijd beschikbaar voor u zijn, zelfs als u uw apparaat verliest.",
|
||||
"enteSubscriptionShareWithFamily": "Je familie kan ook aan je abonnement worden toegevoegd.",
|
||||
"currentUsageIs": "Huidig gebruik is ",
|
||||
"@currentUsageIs": {
|
||||
"description": "This text is followed by storage usaged",
|
||||
"description": "This text is followed by storage usage",
|
||||
"examples": {
|
||||
"0": "Current usage is 1.2 GB"
|
||||
},
|
||||
|
@ -619,7 +620,7 @@
|
|||
"appleId": "Apple ID",
|
||||
"playstoreSubscription": "PlayStore abonnement",
|
||||
"appstoreSubscription": "PlayStore abonnement",
|
||||
"subAlreadyLinkedErrMessage": "Uw {id} is al aan een ander ente account gekoppeld.\nAls u uw {id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice",
|
||||
"subAlreadyLinkedErrMessage": "Jouw {id} is al aan een ander Ente account gekoppeld.\nAls je jouw {id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice",
|
||||
"visitWebToManage": "Bezoek alstublieft web.ente.io om uw abonnement te beheren",
|
||||
"couldNotUpdateSubscription": "Kon abonnement niet wijzigen",
|
||||
"pleaseContactSupportAndWeWillBeHappyToHelp": "Neem alstublieft contact op met support@ente.io en we helpen u graag!",
|
||||
|
@ -640,7 +641,7 @@
|
|||
"thankYou": "Bedankt",
|
||||
"failedToVerifyPaymentStatus": "Betalingsstatus verifiëren mislukt",
|
||||
"pleaseWaitForSometimeBeforeRetrying": "Gelieve even te wachten voordat u opnieuw probeert",
|
||||
"paymentFailedWithReason": "Helaas is uw betaling mislukt vanwege {reason}",
|
||||
"paymentFailedMessage": "Helaas is je betaling mislukt. Neem contact op met support zodat we je kunnen helpen!",
|
||||
"youAreOnAFamilyPlan": "U bent onderdeel van een familie abonnement!",
|
||||
"contactFamilyAdmin": "Neem contact op met <green>{familyAdminEmail}</green> om uw abonnement te beheren",
|
||||
"leaveFamily": "Familie abonnement verlaten",
|
||||
|
@ -664,7 +665,7 @@
|
|||
"everywhere": "overal",
|
||||
"androidIosWebDesktop": "Android, iOS, Web, Desktop",
|
||||
"mobileWebDesktop": "Mobiel, Web, Desktop",
|
||||
"newToEnte": "Nieuw bij ente",
|
||||
"newToEnte": "Nieuw bij Ente",
|
||||
"pleaseLoginAgain": "Log opnieuw in",
|
||||
"devAccountChanged": "Het ontwikkelaarsaccount dat we gebruiken om te publiceren in de App Store is veranderd. Daarom moet je opnieuw inloggen.\n\nOnze excuses voor het ongemak, helaas was dit onvermijdelijk.",
|
||||
"yourSubscriptionHasExpired": "Uw abonnement is verlopen",
|
||||
|
@ -677,12 +678,12 @@
|
|||
},
|
||||
"backupFailed": "Back-up mislukt",
|
||||
"couldNotBackUpTryLater": "We konden uw gegevens niet back-uppen.\nWe zullen het later opnieuw proberen.",
|
||||
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "ente kan bestanden alleen versleutelen en bewaren als u toegang tot ze geeft",
|
||||
"enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente kan bestanden alleen versleutelen en bewaren als u toegang tot ze geeft",
|
||||
"pleaseGrantPermissions": "Geef alstublieft toestemming",
|
||||
"grantPermission": "Toestemming verlenen",
|
||||
"privateSharing": "Privé delen",
|
||||
"shareOnlyWithThePeopleYouWant": "Deel alleen met de mensen die u wilt",
|
||||
"usePublicLinksForPeopleNotOnEnte": "Gebruik publieke links voor mensen die niet op ente zitten",
|
||||
"usePublicLinksForPeopleNotOnEnte": "Gebruik publieke links voor mensen die geen Ente account hebben",
|
||||
"allowPeopleToAddPhotos": "Mensen toestaan foto's toe te voegen",
|
||||
"shareAnAlbumNow": "Deel nu een album",
|
||||
"collectEventPhotos": "Foto's van gebeurtenissen verzamelen",
|
||||
|
@ -694,7 +695,7 @@
|
|||
},
|
||||
"onDevice": "Op het apparaat",
|
||||
"@onEnte": {
|
||||
"description": "The text displayed above albums backed up to ente",
|
||||
"description": "The text displayed above albums backed up to Ente",
|
||||
"type": "text"
|
||||
},
|
||||
"onEnte": "Op <branding>ente</branding>",
|
||||
|
@ -740,7 +741,7 @@
|
|||
"saveCollage": "Sla collage op",
|
||||
"collageSaved": "Collage opgeslagen in gallerij",
|
||||
"collageLayout": "Layout",
|
||||
"addToEnte": "Toevoegen aan ente",
|
||||
"addToEnte": "Toevoegen aan Ente",
|
||||
"addToAlbum": "Toevoegen aan album",
|
||||
"delete": "Verwijderen",
|
||||
"hide": "Verbergen",
|
||||
|
@ -805,9 +806,9 @@
|
|||
"photosAddedByYouWillBeRemovedFromTheAlbum": "Foto's toegevoegd door u zullen worden verwijderd uit het album",
|
||||
"youveNoFilesInThisAlbumThatCanBeDeleted": "Je hebt geen bestanden in dit album die verwijderd kunnen worden",
|
||||
"youDontHaveAnyArchivedItems": "U heeft geen gearchiveerde bestanden.",
|
||||
"ignoredFolderUploadReason": "Sommige bestanden in dit album worden genegeerd voor de upload omdat ze eerder van ente zijn verwijderd.",
|
||||
"ignoredFolderUploadReason": "Sommige bestanden in dit album worden genegeerd voor uploaden omdat ze eerder van Ente zijn verwijderd.",
|
||||
"resetIgnoredFiles": "Reset genegeerde bestanden",
|
||||
"deviceFilesAutoUploading": "Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar ente.",
|
||||
"deviceFilesAutoUploading": "Bestanden toegevoegd aan dit album van dit apparaat zullen automatisch geüpload worden naar Ente.",
|
||||
"turnOnBackupForAutoUpload": "Schakel back-up in om bestanden die toegevoegd zijn aan deze map op dit apparaat automatisch te uploaden.",
|
||||
"noHiddenPhotosOrVideos": "Geen verborgen foto's of video's",
|
||||
"toHideAPhotoOrVideo": "Om een foto of video te verbergen",
|
||||
|
@ -885,7 +886,7 @@
|
|||
"@freeUpSpaceSaving": {
|
||||
"description": "Text to tell user how much space they can free up by deleting items from the device"
|
||||
},
|
||||
"freeUpAccessPostDelete": "U heeft nog steeds toegang tot {count, plural, one {het} other {ze}} op ente zolang u een actief abonnement heeft",
|
||||
"freeUpAccessPostDelete": "Je hebt nog steeds toegang tot {count, plural, one {het} other {ze}} op Ente zolang je een actief abonnement hebt",
|
||||
"@freeUpAccessPostDelete": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
|
@ -936,7 +937,7 @@
|
|||
"renameFile": "Bestandsnaam wijzigen",
|
||||
"enterFileName": "Geef bestandsnaam op",
|
||||
"filesDeleted": "Bestanden verwijderd",
|
||||
"selectedFilesAreNotOnEnte": "Geselecteerde bestanden staan niet op ente",
|
||||
"selectedFilesAreNotOnEnte": "Geselecteerde bestanden staan niet op Ente",
|
||||
"thisActionCannotBeUndone": "Deze actie kan niet ongedaan gemaakt worden",
|
||||
"emptyTrash": "Prullenbak leegmaken?",
|
||||
"permDeleteWarning": "Alle bestanden in de prullenbak zullen permanent worden verwijderd\n\nDeze actie kan niet ongedaan worden gemaakt",
|
||||
|
@ -945,7 +946,7 @@
|
|||
"permanentlyDeleteFromDevice": "Permanent verwijderen van apparaat?",
|
||||
"someOfTheFilesYouAreTryingToDeleteAre": "Sommige bestanden die u probeert te verwijderen zijn alleen beschikbaar op uw apparaat en kunnen niet hersteld worden als deze verwijderd worden",
|
||||
"theyWillBeDeletedFromAllAlbums": "Ze zullen uit alle albums worden verwijderd.",
|
||||
"someItemsAreInBothEnteAndYourDevice": "Sommige bestanden bevinden zich in zowel ente als op uw apparaat.",
|
||||
"someItemsAreInBothEnteAndYourDevice": "Sommige bestanden bevinden zich zowel in Ente als op jouw apparaat.",
|
||||
"selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Geselecteerde bestanden worden verwijderd uit alle albums en verplaatst naar de prullenbak.",
|
||||
"theseItemsWillBeDeletedFromYourDevice": "Deze bestanden zullen worden verwijderd van uw apparaat.",
|
||||
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Het lijkt erop dat er iets fout is gegaan. Probeer het later opnieuw. Als de fout zich blijft voordoen, neem dan contact op met ons supportteam.",
|
||||
|
@ -1051,7 +1052,7 @@
|
|||
},
|
||||
"setRadius": "Radius instellen",
|
||||
"familyPlanPortalTitle": "Familie",
|
||||
"familyPlanOverview": "Voeg 5 gezinsleden toe aan uw bestaande abonnement zonder extra te betalen.\n\nElk lid krijgt zijn eigen privé ruimte en kan elkaars bestanden niet zien, tenzij ze zijn gedeeld.\n\nFamilieplannen zijn beschikbaar voor klanten die een betaald ente abonnement hebben.\n\nAbonneer u nu om aan de slag te gaan!",
|
||||
"familyPlanOverview": "Voeg 5 gezinsleden toe aan je bestaande abonnement zonder extra te betalen.\n\nElk lid krijgt zijn eigen privé ruimte en kan elkaars bestanden niet zien tenzij ze zijn gedeeld.\n\nFamilieplannen zijn beschikbaar voor klanten die een betaald Ente abonnement hebben.\n\nAbonneer nu om aan de slag te gaan!",
|
||||
"androidBiometricHint": "Identiteit verifiëren",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
|
@ -1129,7 +1130,7 @@
|
|||
"noAlbumsSharedByYouYet": "Nog geen albums gedeeld door jou",
|
||||
"sharedWithYou": "Gedeeld met jou",
|
||||
"sharedByYou": "Gedeeld door jou",
|
||||
"inviteYourFriendsToEnte": "Vrienden uitnodigen voor ente",
|
||||
"inviteYourFriendsToEnte": "Vrienden uitnodigen voor Ente",
|
||||
"failedToDownloadVideo": "Downloaden van video mislukt",
|
||||
"hiding": "Verbergen...",
|
||||
"unhiding": "Zichtbaar maken...",
|
||||
|
@ -1139,7 +1140,7 @@
|
|||
"addToHiddenAlbum": "Toevoegen aan verborgen album",
|
||||
"moveToHiddenAlbum": "Verplaatsen naar verborgen album",
|
||||
"fileTypes": "Bestandstype",
|
||||
"deleteConfirmDialogBody": "Dit account is gekoppeld aan andere ente apps, als je er gebruik van maakt.\\n\\nJe geüploade gegevens worden in alle ente apps gepland voor verwijdering, en je account wordt permanent verwijderd voor alle ente diensten.",
|
||||
"deleteConfirmDialogBody": "Dit account is gekoppeld aan andere Ente apps, als je er gebruik van maakt. Je geüploade gegevens worden in alle Ente apps gepland voor verwijdering, en je account wordt permanent verwijderd voor alle Ente diensten.",
|
||||
"hearUsWhereTitle": "Hoe hoorde je over Ente? (optioneel)",
|
||||
"hearUsExplanation": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!",
|
||||
"viewAddOnButton": "Add-ons bekijken",
|
||||
|
@ -1187,16 +1188,29 @@
|
|||
"changeLocationOfSelectedItems": "Locatie van geselecteerde items wijzigen?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Bewerkte locatie wordt alleen gezien binnen Ente",
|
||||
"cleanUncategorized": "Ongecategoriseerd opschonen",
|
||||
"cleanUncategorizedDescription": "Verwijder alle bestanden van Ongecategoriseerd die aanwezig zijn in andere albums",
|
||||
"waitingForVerification": "Wachten op verificatie...",
|
||||
"passkey": "Passkey",
|
||||
"passkeyAuthTitle": "Passkey verificatie",
|
||||
"verifyPasskey": "Bevestig passkey",
|
||||
"playOnTv": "Album afspelen op TV",
|
||||
"pair": "Koppelen",
|
||||
"deviceNotFound": "Apparaat niet gevonden",
|
||||
"castInstruction": "Bezoek cast.ente.io op het apparaat dat u wilt koppelen.\n\nVoer de code hieronder in om het album op uw TV af te spelen.",
|
||||
"deviceCodeHint": "Voer de code in",
|
||||
"joinDiscord": "Join Discord",
|
||||
"locations": "Locations",
|
||||
"descriptions": "Descriptions",
|
||||
"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"
|
||||
"joinDiscord": "Join de Discord",
|
||||
"locations": "Locaties",
|
||||
"descriptions": "Beschrijvingen",
|
||||
"addViewers": "{count, plural, one {Voeg kijker toe} other {Voeg kijkers toe}}",
|
||||
"addCollaborators": "{count, plural, zero {Voeg samenwerker toe} one {Voeg samenwerker toe} other {Voeg samenwerkers toe}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Druk lang op een e-mail om de versleuteling te verifiëren.",
|
||||
"developerSettingsWarning": "Weet je zeker dat je de ontwikkelaarsinstellingen wilt wijzigen?",
|
||||
"developerSettings": "Ontwikkelaarsinstellingen",
|
||||
"serverEndpoint": "Server eindpunt",
|
||||
"invalidEndpoint": "Ongeldig eindpunt",
|
||||
"invalidEndpointMessage": "Sorry, het eindpunt dat je hebt ingevoerd is ongeldig. Voer een geldig eindpunt in en probeer het opnieuw.",
|
||||
"endpointUpdatedMessage": "Eindpunt met succes bijgewerkt",
|
||||
"customEndpoint": "Verbonden met {endpoint}",
|
||||
"createCollaborativeLink": "Maak een gezamenlijke link",
|
||||
"search": "Zoeken"
|
||||
}
|
|
@ -31,5 +31,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -118,5 +118,6 @@
|
|||
"addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}",
|
||||
"addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.",
|
||||
"createCollaborativeLink": "Create collaborative link"
|
||||
"createCollaborativeLink": "Create collaborative link",
|
||||
"search": "Search"
|
||||
}
|
|
@ -1211,5 +1211,6 @@
|
|||
"invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.",
|
||||
"endpointUpdatedMessage": "Endpoint atualizado com sucesso",
|
||||
"customEndpoint": "Conectado a {endpoint}",
|
||||
"createCollaborativeLink": "Criar link colaborativo"
|
||||
"createCollaborativeLink": "Criar link colaborativo",
|
||||
"search": "Pesquisar"
|
||||
}
|
|
@ -1211,5 +1211,6 @@
|
|||
"invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
|
||||
"endpointUpdatedMessage": "端点更新成功",
|
||||
"customEndpoint": "已连接至 {endpoint}",
|
||||
"createCollaborativeLink": "创建协作链接"
|
||||
"createCollaborativeLink": "创建协作链接",
|
||||
"search": "搜索"
|
||||
}
|
|
@ -85,13 +85,24 @@ class EnteFile {
|
|||
|
||||
static int parseFileCreationTime(String? fileTitle, AssetEntity asset) {
|
||||
int creationTime = asset.createDateTime.microsecondsSinceEpoch;
|
||||
final int modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
||||
if (creationTime >= jan011981Time) {
|
||||
// assuming that fileSystem is returning correct creationTime.
|
||||
// During upload, this might get overridden with exif Creation time
|
||||
// When the assetModifiedTime is less than creationTime, than just use
|
||||
// that as creationTime. This is to handle cases where file might be
|
||||
// copied to the fileSystem from somewhere else See #https://superuser.com/a/1091147
|
||||
if (modificationTime >= jan011981Time &&
|
||||
modificationTime < creationTime) {
|
||||
_logger.info(
|
||||
'LocalID: ${asset.id} modification time is less than creation time. Using modification time as creation time',
|
||||
);
|
||||
creationTime = modificationTime;
|
||||
}
|
||||
return creationTime;
|
||||
} else {
|
||||
if (asset.modifiedDateTime.microsecondsSinceEpoch >= jan011981Time) {
|
||||
creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
||||
if (modificationTime >= jan011981Time) {
|
||||
creationTime = modificationTime;
|
||||
} else {
|
||||
creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
|
||||
}
|
||||
|
@ -106,7 +117,6 @@ class EnteFile {
|
|||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,14 @@ import "package:logging/logging.dart";
|
|||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
import "package:photos/models/collection/collection_items.dart";
|
||||
import "package:photos/models/file/file_type.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/favorites_service.dart";
|
||||
import "package:photos/ui/viewer/file/detail_page.dart";
|
||||
import "package:photos/ui/viewer/gallery/collection_page.dart";
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
import "package:photos/utils/preload_util.dart";
|
||||
|
||||
class HomeWidgetService {
|
||||
|
@ -171,4 +176,49 @@ class HomeWidgetService {
|
|||
);
|
||||
_logger.info(">>> SlideshowWidget cleared");
|
||||
}
|
||||
|
||||
Future<void> onLaunchFromWidget(Uri? uri, BuildContext context) async {
|
||||
if (uri == null) return;
|
||||
|
||||
final collectionID =
|
||||
await FavoritesService.instance.getFavoriteCollectionID();
|
||||
if (collectionID == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final collection = CollectionsService.instance.getCollectionByID(
|
||||
collectionID,
|
||||
);
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final thumbnail = await CollectionsService.instance.getCover(collection);
|
||||
|
||||
final previousGeneratedId =
|
||||
await hw.HomeWidget.getWidgetData<int>("home_widget_last_img");
|
||||
|
||||
final res = previousGeneratedId != null
|
||||
? await FilesDB.instance.getFile(
|
||||
previousGeneratedId,
|
||||
)
|
||||
: null;
|
||||
|
||||
routeToPage(
|
||||
context,
|
||||
CollectionPage(
|
||||
CollectionWithThumbnail(
|
||||
collection,
|
||||
thumbnail,
|
||||
),
|
||||
),
|
||||
).ignore();
|
||||
|
||||
if (res == null) return;
|
||||
|
||||
final page = DetailPage(
|
||||
DetailPageConfiguration(List.unmodifiable([res]), null, 0, "collection"),
|
||||
);
|
||||
routeToPage(context, page, forceCustomPageRoute: true).ignore();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import "dart:io";
|
|||
import "package:connectivity_plus/connectivity_plus.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/errors.dart";
|
||||
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/event.dart";
|
||||
import "package:photos/services/remote_assets_service.dart";
|
||||
|
@ -23,7 +22,7 @@ abstract class MLFramework {
|
|||
MLFramework(this.shouldDownloadOverMobileData) {
|
||||
Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen((ConnectivityResult result) async {
|
||||
.listen((List<ConnectivityResult> result) async {
|
||||
_logger.info("Connectivity changed to $result");
|
||||
if (_state == InitializationState.waitingForNetwork &&
|
||||
await _canDownload()) {
|
||||
|
@ -135,9 +134,11 @@ abstract class MLFramework {
|
|||
}
|
||||
|
||||
Future<bool> _canDownload() async {
|
||||
final connectivityResult = await (Connectivity().checkConnectivity());
|
||||
return connectivityResult != ConnectivityResult.mobile ||
|
||||
shouldDownloadOverMobileData;
|
||||
final List<ConnectivityResult> connections =
|
||||
await (Connectivity().checkConnectivity());
|
||||
final bool isConnectedToMobile =
|
||||
connections.contains(ConnectivityResult.mobile);
|
||||
return !isConnectedToMobile || shouldDownloadOverMobileData;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ class MemoriesService extends ChangeNotifier {
|
|||
}
|
||||
final ignoredCollections =
|
||||
CollectionsService.instance.archivedOrHiddenCollectionIds();
|
||||
final files = await _filesDB.getFilesCreatedWithinDurationsSync(
|
||||
final files = await _filesDB.getFilesCreatedWithinDurations(
|
||||
durations,
|
||||
ignoredCollections,
|
||||
visibility: visibleVisibility,
|
||||
|
|
|
@ -45,7 +45,9 @@ class SyncService {
|
|||
sync();
|
||||
});
|
||||
|
||||
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
|
||||
Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen((List<ConnectivityResult> result) {
|
||||
_logger.info("Connectivity change detected " + result.toString());
|
||||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
sync();
|
||||
|
|
|
@ -59,9 +59,9 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
|||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: const PasswordEntryPage(
|
||||
return const PopScope(
|
||||
canPop: false,
|
||||
child: PasswordEntryPage(
|
||||
mode: PasswordEntryMode.reset,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -27,8 +27,8 @@ class LinearProgressDialogState extends State<LinearProgressDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: AlertDialog(
|
||||
title: Text(
|
||||
widget.message,
|
||||
|
|
|
@ -155,8 +155,8 @@ class ProgressDialog {
|
|||
barrierColor: _barrierColor,
|
||||
builder: (BuildContext context) {
|
||||
_dismissingContext = context;
|
||||
return WillPopScope(
|
||||
onWillPop: () async => _barrierDismissible,
|
||||
return PopScope(
|
||||
canPop: _barrierDismissible,
|
||||
child: Dialog(
|
||||
backgroundColor: _backgroundColor,
|
||||
insetAnimationCurve: _insetAnimCurve,
|
||||
|
|
|
@ -52,8 +52,15 @@ class _PaymentWebPageState extends State<PaymentWebPage> {
|
|||
if (initPaymentUrl == null) {
|
||||
return const EnteLoadingWidget();
|
||||
}
|
||||
return WillPopScope(
|
||||
onWillPop: (() async => _buildPageExitWidget(context)),
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (didPop) async {
|
||||
if (didPop) return;
|
||||
final shouldPop = await _buildPageExitWidget(context);
|
||||
if (shouldPop) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context).subscription),
|
||||
|
|
|
@ -83,8 +83,8 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
|||
);
|
||||
final shouldForceUpdate =
|
||||
UpdateService.instance.shouldForceUpdate(widget.latestVersionInfo!);
|
||||
return WillPopScope(
|
||||
onWillPop: () async => !shouldForceUpdate,
|
||||
return PopScope(
|
||||
canPop: !shouldForceUpdate,
|
||||
child: AlertDialog(
|
||||
key: const ValueKey("updateAppDialog"),
|
||||
title: Column(
|
||||
|
|
|
@ -315,7 +315,23 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
final enableDrawer = LocalSyncService.instance.hasCompletedFirstImport();
|
||||
final action = AppLifecycleService.instance.mediaExtensionAction.action;
|
||||
return UserDetailsStateWidget(
|
||||
child: WillPopScope(
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (didPop) async {
|
||||
if (didPop) return;
|
||||
if (_selectedTabIndex == 0) {
|
||||
if (isSettingsOpen) {
|
||||
Navigator.pop(context);
|
||||
} else if (Platform.isAndroid && action == IntentAction.main) {
|
||||
unawaited(MoveToBackground.moveTaskToBack());
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} else {
|
||||
Bus.instance
|
||||
.fire(TabChangedEvent(0, TabChangedEventSource.backButton));
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
drawerScrimColor: getEnteColorScheme(context).strokeFainter,
|
||||
drawerEnableOpenDragGesture: false,
|
||||
|
@ -341,24 +357,6 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
onWillPop: () async {
|
||||
if (_selectedTabIndex == 0) {
|
||||
if (isSettingsOpen) {
|
||||
Navigator.pop(context);
|
||||
return false;
|
||||
}
|
||||
if (Platform.isAndroid && action == IntentAction.main) {
|
||||
unawaited(MoveToBackground.moveTaskToBack());
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
Bus.instance
|
||||
.fire(TabChangedEvent(0, TabChangedEventSource.backButton));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -137,9 +137,9 @@ class _AppLockState extends State<AppLock> with WidgetsBindingObserver {
|
|||
}
|
||||
|
||||
Widget get _lockScreen {
|
||||
return WillPopScope(
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: this.widget.lockScreen,
|
||||
onWillPop: () => Future.value(false),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -63,14 +63,14 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (didPop) async {
|
||||
if (_hasBeenEdited()) {
|
||||
await _showExitConfirmationDialog(context);
|
||||
} else {
|
||||
replacePage(context, DetailPage(widget.detailPageConfig));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
|
@ -53,47 +53,70 @@ class FileAppBar extends StatefulWidget {
|
|||
|
||||
class FileAppBarState extends State<FileAppBar> {
|
||||
final _logger = Logger("FadingAppBar");
|
||||
final List<Widget> _actions = [];
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FileAppBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.file.generatedID != widget.file.generatedID) {
|
||||
_getActions();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.fine("building app bar ${widget.file.generatedID?.toString()}");
|
||||
|
||||
//When the widget is initialized, the actions are not available.
|
||||
//Cannot call _getActions() in initState.
|
||||
if (_actions.isEmpty) {
|
||||
_getActions();
|
||||
}
|
||||
|
||||
final isTrashedFile = widget.file is TrashFile;
|
||||
final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
|
||||
return CustomAppBar(
|
||||
ValueListenableBuilder(
|
||||
valueListenable: widget.enableFullScreenNotifier,
|
||||
builder: (context, bool isFullScreen, _) {
|
||||
builder: (context, bool isFullScreen, child) {
|
||||
return IgnorePointer(
|
||||
ignoring: isFullScreen,
|
||||
child: AnimatedOpacity(
|
||||
opacity: isFullScreen ? 0 : 1,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.72),
|
||||
Colors.black.withOpacity(0.6),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.2, 1],
|
||||
),
|
||||
),
|
||||
child: _buildAppBar(),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.72),
|
||||
Colors.black.withOpacity(0.6),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.2, 1],
|
||||
),
|
||||
),
|
||||
child: AppBar(
|
||||
iconTheme: const IconThemeData(
|
||||
color: Colors.white,
|
||||
), //same for both themes
|
||||
actions: shouldShowActions ? _actions : [],
|
||||
elevation: 0,
|
||||
backgroundColor: const Color(0x00000000),
|
||||
),
|
||||
),
|
||||
),
|
||||
Size.fromHeight(Platform.isAndroid ? 84 : 96),
|
||||
);
|
||||
}
|
||||
|
||||
AppBar _buildAppBar() {
|
||||
_logger.fine("building app bar ${widget.file.generatedID?.toString()}");
|
||||
|
||||
final List<Widget> actions = [];
|
||||
final isTrashedFile = widget.file is TrashFile;
|
||||
final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
|
||||
List<Widget> _getActions() {
|
||||
_actions.clear();
|
||||
final bool isOwnedByUser = widget.file.isOwner;
|
||||
final bool isFileUploaded = widget.file.isUploaded;
|
||||
bool isFileHidden = false;
|
||||
|
@ -104,7 +127,7 @@ class FileAppBarState extends State<FileAppBar> {
|
|||
false;
|
||||
}
|
||||
if (widget.file.isLiveOrMotionPhoto) {
|
||||
actions.add(
|
||||
_actions.add(
|
||||
IconButton(
|
||||
icon: const Icon(Icons.album_outlined),
|
||||
onPressed: () {
|
||||
|
@ -118,7 +141,7 @@ class FileAppBarState extends State<FileAppBar> {
|
|||
}
|
||||
// only show fav option for files owned by the user
|
||||
if (isOwnedByUser && !isFileHidden && isFileUploaded) {
|
||||
actions.add(
|
||||
_actions.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: FavoriteWidget(widget.file),
|
||||
|
@ -126,7 +149,7 @@ class FileAppBarState extends State<FileAppBar> {
|
|||
);
|
||||
}
|
||||
if (!isFileUploaded) {
|
||||
actions.add(
|
||||
_actions.add(
|
||||
UploadIconWidget(
|
||||
file: widget.file,
|
||||
key: ValueKey(widget.file.tag),
|
||||
|
@ -241,7 +264,7 @@ class FileAppBarState extends State<FileAppBar> {
|
|||
}
|
||||
}
|
||||
if (items.isNotEmpty) {
|
||||
actions.add(
|
||||
_actions.add(
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return items;
|
||||
|
@ -262,13 +285,7 @@ class FileAppBarState extends State<FileAppBar> {
|
|||
),
|
||||
);
|
||||
}
|
||||
return AppBar(
|
||||
iconTheme:
|
||||
const IconThemeData(color: Colors.white), //same for both themes
|
||||
actions: shouldShowActions ? actions : [],
|
||||
elevation: 0,
|
||||
backgroundColor: const Color(0x00000000),
|
||||
);
|
||||
return _actions;
|
||||
}
|
||||
|
||||
Future<void> _handleHideRequest(BuildContext context) async {
|
||||
|
|
|
@ -5,6 +5,7 @@ import "package:flutter/scheduler.dart";
|
|||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/clear_and_unfocus_search_bar_event.dart";
|
||||
import "package:photos/events/tab_changed_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/search/index_of_indexed_stack.dart";
|
||||
import "package:photos/models/search/search_result.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
|
@ -130,17 +131,14 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
color: colorScheme.backgroundBase,
|
||||
child: Container(
|
||||
color: colorScheme.fillFaint,
|
||||
child: TextFormField(
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
// Below parameters are to disable auto-suggestion
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
// Above parameters are to disable auto-suggestion
|
||||
decoration: InputDecoration(
|
||||
//TODO: Extract string
|
||||
hintText: "Search",
|
||||
hintText: S.of(context).search,
|
||||
filled: true,
|
||||
fillColor: getEnteColorScheme(context).fillFaint,
|
||||
border: const UnderlineInputBorder(
|
||||
|
@ -161,6 +159,9 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
minHeight: 44,
|
||||
minWidth: 44,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
prefixIcon: Hero(
|
||||
tag: "search_icon",
|
||||
child: Icon(
|
||||
|
@ -168,6 +169,7 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
color: colorScheme.strokeFaint,
|
||||
),
|
||||
),
|
||||
|
||||
/*Using valueListenableBuilder inside a stateful widget because this widget is only rebuild when
|
||||
setState is called when deboucncing is over and the spinner needs to be shown while debouncing */
|
||||
suffixIcon: ValueListenableBuilder(
|
||||
|
|
|
@ -29,6 +29,7 @@ import "package:photos/models/metadata/file_magic.dart";
|
|||
import 'package:photos/models/upload_url.dart';
|
||||
import "package:photos/models/user_details.dart";
|
||||
import 'package:photos/services/collections_service.dart';
|
||||
import "package:photos/services/feature_flag_service.dart";
|
||||
import "package:photos/services/file_magic_service.dart";
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import 'package:photos/services/sync_service.dart';
|
||||
|
@ -37,6 +38,7 @@ import 'package:photos/utils/crypto_util.dart';
|
|||
import 'package:photos/utils/file_download_util.dart';
|
||||
import 'package:photos/utils/file_uploader_util.dart';
|
||||
import "package:photos/utils/file_util.dart";
|
||||
import "package:photos/utils/multipart_upload_util.dart";
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import "package:uuid/uuid.dart";
|
||||
|
@ -353,9 +355,10 @@ class FileUploader {
|
|||
if (isForceUpload) {
|
||||
return;
|
||||
}
|
||||
final connectivityResult = await (Connectivity().checkConnectivity());
|
||||
final List<ConnectivityResult> connections =
|
||||
await (Connectivity().checkConnectivity());
|
||||
bool canUploadUnderCurrentNetworkConditions = true;
|
||||
if (connectivityResult == ConnectivityResult.mobile) {
|
||||
if (connections.any((element) => element == ConnectivityResult.mobile)) {
|
||||
canUploadUnderCurrentNetworkConditions =
|
||||
Configuration.instance.shouldBackupOverMobileData();
|
||||
}
|
||||
|
@ -492,8 +495,23 @@ class FileUploader {
|
|||
final String thumbnailObjectKey =
|
||||
await _putFile(thumbnailUploadURL, encryptedThumbnailFile);
|
||||
|
||||
final fileUploadURL = await _getUploadURL();
|
||||
final String fileObjectKey = await _putFile(fileUploadURL, encryptedFile);
|
||||
// Calculate the number of parts for the file. Multiple part upload
|
||||
// is only enabled for internal users and debug builds till it's battle tested.
|
||||
final count = FeatureFlagService.instance.isInternalUserOrDebugBuild()
|
||||
? await calculatePartCount(
|
||||
await encryptedFile.length(),
|
||||
)
|
||||
: 1;
|
||||
|
||||
late String fileObjectKey;
|
||||
|
||||
if (count <= 1) {
|
||||
final fileUploadURL = await _getUploadURL();
|
||||
fileObjectKey = await _putFile(fileUploadURL, encryptedFile);
|
||||
} else {
|
||||
final fileUploadURLs = await getMultipartUploadURLs(count);
|
||||
fileObjectKey = await putMultipartFile(fileUploadURLs, encryptedFile);
|
||||
}
|
||||
|
||||
final metadata = await file.getMetadataForUpload(mediaUploadData);
|
||||
final encryptedMetadataResult = await CryptoUtil.encryptChaCha(
|
||||
|
|
157
mobile/lib/utils/multipart_upload_util.dart
Normal file
157
mobile/lib/utils/multipart_upload_util.dart
Normal file
|
@ -0,0 +1,157 @@
|
|||
// ignore_for_file: implementation_imports
|
||||
|
||||
import "dart:io";
|
||||
|
||||
import "package:dio/dio.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/network/network.dart";
|
||||
import "package:photos/services/feature_flag_service.dart";
|
||||
import "package:photos/utils/xml_parser_util.dart";
|
||||
|
||||
final _enteDio = NetworkClient.instance.enteDio;
|
||||
final _dio = NetworkClient.instance.getDio();
|
||||
|
||||
class PartETag extends XmlParsableObject {
|
||||
final int partNumber;
|
||||
final String eTag;
|
||||
|
||||
PartETag(this.partNumber, this.eTag);
|
||||
|
||||
@override
|
||||
String get elementName => "Part";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
"PartNumber": partNumber,
|
||||
"ETag": eTag,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MultipartUploadURLs {
|
||||
final String objectKey;
|
||||
final List<String> partsURLs;
|
||||
final String completeURL;
|
||||
|
||||
MultipartUploadURLs({
|
||||
required this.objectKey,
|
||||
required this.partsURLs,
|
||||
required this.completeURL,
|
||||
});
|
||||
|
||||
factory MultipartUploadURLs.fromMap(Map<String, dynamic> map) {
|
||||
return MultipartUploadURLs(
|
||||
objectKey: map["urls"]["objectKey"],
|
||||
partsURLs: (map["urls"]["partURLs"] as List).cast<String>(),
|
||||
completeURL: map["urls"]["completeURL"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> calculatePartCount(int fileSize) async {
|
||||
final partCount = (fileSize / multipartPartSize).ceil();
|
||||
return partCount;
|
||||
}
|
||||
|
||||
Future<MultipartUploadURLs> getMultipartUploadURLs(int count) async {
|
||||
try {
|
||||
assert(
|
||||
FeatureFlagService.instance.isInternalUserOrDebugBuild(),
|
||||
"Multipart upload should not be enabled for external users.",
|
||||
);
|
||||
final response = await _enteDio.get(
|
||||
"/files/multipart-upload-urls",
|
||||
queryParameters: {
|
||||
"count": count,
|
||||
},
|
||||
);
|
||||
|
||||
return MultipartUploadURLs.fromMap(response.data);
|
||||
} on Exception catch (e) {
|
||||
Logger("MultipartUploadURL").severe(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> putMultipartFile(
|
||||
MultipartUploadURLs urls,
|
||||
File encryptedFile,
|
||||
) async {
|
||||
// upload individual parts and get their etags
|
||||
final etags = await uploadParts(urls.partsURLs, encryptedFile);
|
||||
|
||||
// complete the multipart upload
|
||||
await completeMultipartUpload(etags, urls.completeURL);
|
||||
|
||||
return urls.objectKey;
|
||||
}
|
||||
|
||||
Future<Map<int, String>> uploadParts(
|
||||
List<String> partsURLs,
|
||||
File encryptedFile,
|
||||
) async {
|
||||
final partsLength = partsURLs.length;
|
||||
final etags = <int, String>{};
|
||||
|
||||
for (int i = 0; i < partsLength; i++) {
|
||||
final partURL = partsURLs[i];
|
||||
final isLastPart = i == partsLength - 1;
|
||||
final fileSize = isLastPart
|
||||
? encryptedFile.lengthSync() % multipartPartSize
|
||||
: multipartPartSize;
|
||||
|
||||
final response = await _dio.put(
|
||||
partURL,
|
||||
data: encryptedFile.openRead(
|
||||
i * multipartPartSize,
|
||||
isLastPart ? null : multipartPartSize,
|
||||
),
|
||||
options: Options(
|
||||
headers: {
|
||||
Headers.contentLengthHeader: fileSize,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final eTag = response.headers.value("etag");
|
||||
|
||||
if (eTag?.isEmpty ?? true) {
|
||||
throw Exception('ETAG_MISSING');
|
||||
}
|
||||
|
||||
etags[i] = eTag!;
|
||||
}
|
||||
|
||||
return etags;
|
||||
}
|
||||
|
||||
Future<void> completeMultipartUpload(
|
||||
Map<int, String> partEtags,
|
||||
String completeURL,
|
||||
) async {
|
||||
final body = convertJs2Xml({
|
||||
'CompleteMultipartUpload': partEtags.entries
|
||||
.map(
|
||||
(e) => PartETag(
|
||||
e.key + 1,
|
||||
e.value,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
}).replaceAll('"', '').replaceAll('"', '');
|
||||
|
||||
try {
|
||||
await _dio.post(
|
||||
completeURL,
|
||||
data: body,
|
||||
options: Options(
|
||||
contentType: "text/xml",
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Logger("MultipartUpload").severe(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
41
mobile/lib/utils/xml_parser_util.dart
Normal file
41
mobile/lib/utils/xml_parser_util.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
// ignore_for_file: implementation_imports
|
||||
|
||||
import "package:xml/xml.dart";
|
||||
|
||||
// used for classes that can be converted to xml
|
||||
abstract class XmlParsableObject {
|
||||
Map<String, dynamic> toMap();
|
||||
String get elementName;
|
||||
}
|
||||
|
||||
// for converting the response to xml
|
||||
String convertJs2Xml(Map<String, dynamic> json) {
|
||||
final builder = XmlBuilder();
|
||||
buildXml(builder, json);
|
||||
return builder.buildDocument().toXmlString(
|
||||
pretty: true,
|
||||
indent: ' ',
|
||||
);
|
||||
}
|
||||
|
||||
// for building the xml node tree recursively
|
||||
void buildXml(XmlBuilder builder, dynamic node) {
|
||||
if (node is Map<String, dynamic>) {
|
||||
node.forEach((key, value) {
|
||||
builder.element(key, nest: () => buildXml(builder, value));
|
||||
});
|
||||
} else if (node is List<dynamic>) {
|
||||
for (var item in node) {
|
||||
buildXml(builder, item);
|
||||
}
|
||||
} else if (node is XmlParsableObject) {
|
||||
builder.element(
|
||||
node.elementName,
|
||||
nest: () {
|
||||
buildXml(builder, node.toMap());
|
||||
},
|
||||
);
|
||||
} else {
|
||||
builder.text(node.toString());
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -2087,7 +2039,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.3.0"
|
||||
sqlite3:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9"
|
||||
|
@ -2584,7 +2536,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.0.4"
|
||||
xml:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
|
|
|
@ -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.83+603
|
||||
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
|
||||
|
@ -142,7 +137,6 @@ dependencies:
|
|||
shared_preferences: ^2.0.5
|
||||
sqflite: ^2.3.0
|
||||
sqflite_migration: ^0.3.0
|
||||
sqlite3: ^2.1.0
|
||||
sqlite3_flutter_libs: ^0.5.20
|
||||
sqlite_async: ^0.6.1
|
||||
step_progress_indicator: ^1.0.2
|
||||
|
@ -169,9 +163,9 @@ dependencies:
|
|||
wallpaper_manager_flutter: ^0.0.2
|
||||
wechat_assets_picker: ^8.6.3
|
||||
widgets_to_image: ^0.0.2
|
||||
xml: ^6.3.0
|
||||
|
||||
dependency_overrides:
|
||||
connectivity_plus: ^4.0.0
|
||||
# Remove this after removing dependency from flutter_sodium.
|
||||
# Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0
|
||||
ffi: 2.1.0
|
||||
|
|
|
@ -73,8 +73,11 @@ http:
|
|||
|
||||
# Specify the base endpoints for various apps
|
||||
apps:
|
||||
public-albums: "https://albums.ente.io"
|
||||
|
||||
# Default is https://albums.ente.io
|
||||
#
|
||||
# If you're running a self hosted instance and wish to serve public links,
|
||||
# set this to the URL where your albums web app is running.
|
||||
public-albums:
|
||||
|
||||
# Database connection parameters
|
||||
db:
|
||||
|
|
|
@ -59,25 +59,41 @@ const (
|
|||
DeletedObjectQueueLock = "deleted_objects_queue_lock"
|
||||
)
|
||||
|
||||
// Create adds an entry for a file in the respective tables
|
||||
func (c *FileController) Create(ctx context.Context, userID int64, file ente.File, userAgent string, app ente.App) (ente.File, error) {
|
||||
func (c *FileController) validateFileCreateOrUpdateReq(userID int64, file ente.File) error {
|
||||
objectPathPrefix := strconv.FormatInt(userID, 10) + "/"
|
||||
if !strings.HasPrefix(file.File.ObjectKey, objectPathPrefix) || !strings.HasPrefix(file.Thumbnail.ObjectKey, objectPathPrefix) {
|
||||
return file, stacktrace.Propagate(ente.ErrBadRequest, "Incorrect object key reported")
|
||||
return stacktrace.Propagate(ente.ErrBadRequest, "Incorrect object key reported")
|
||||
}
|
||||
if file.EncryptedKey == "" || file.KeyDecryptionNonce == "" {
|
||||
return stacktrace.Propagate(ente.ErrBadRequest, "EncryptedKey and KeyDecryptionNonce are required")
|
||||
}
|
||||
if file.File.DecryptionHeader == "" || file.Thumbnail.DecryptionHeader == "" {
|
||||
return stacktrace.Propagate(ente.ErrBadRequest, "DecryptionHeader for file & thumb is required")
|
||||
}
|
||||
if file.UpdationTime == 0 {
|
||||
return stacktrace.Propagate(ente.ErrBadRequest, "UpdationTime is required")
|
||||
}
|
||||
collection, err := c.CollectionRepo.Get(file.CollectionID)
|
||||
if err != nil {
|
||||
return file, stacktrace.Propagate(err, "")
|
||||
return stacktrace.Propagate(err, "")
|
||||
}
|
||||
// Verify that user owns the collection.
|
||||
// Warning: Do not remove this check
|
||||
if collection.Owner.ID != userID || file.OwnerID != userID {
|
||||
return file, stacktrace.Propagate(ente.ErrPermissionDenied, "")
|
||||
return stacktrace.Propagate(ente.ErrPermissionDenied, "")
|
||||
}
|
||||
if collection.IsDeleted {
|
||||
return file, stacktrace.Propagate(ente.ErrNotFound, "collection has been deleted")
|
||||
return stacktrace.Propagate(ente.ErrNotFound, "collection has been deleted")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create adds an entry for a file in the respective tables
|
||||
func (c *FileController) Create(ctx context.Context, userID int64, file ente.File, userAgent string, app ente.App) (ente.File, error) {
|
||||
err := c.validateFileCreateOrUpdateReq(userID, file)
|
||||
if err != nil {
|
||||
return file, stacktrace.Propagate(err, "")
|
||||
}
|
||||
hotDC := c.S3Config.GetHotDataCenter()
|
||||
// sizeOf will do also HEAD check to ensure that the object exists in the
|
||||
// current hot DC
|
||||
|
@ -115,7 +131,7 @@ func (c *FileController) Create(ctx context.Context, userID int64, file ente.Fil
|
|||
|
||||
// all iz well
|
||||
var usage int64
|
||||
file, usage, err = c.FileRepo.Create(file, fileSize, thumbnailSize, fileSize+thumbnailSize, collection.Owner.ID, app)
|
||||
file, usage, err = c.FileRepo.Create(file, fileSize, thumbnailSize, fileSize+thumbnailSize, userID, app)
|
||||
if err != nil {
|
||||
if err == ente.ErrDuplicateFileObjectFound || err == ente.ErrDuplicateThumbnailObjectFound {
|
||||
var existing ente.File
|
||||
|
@ -144,9 +160,9 @@ func (c *FileController) Create(ctx context.Context, userID int64, file ente.Fil
|
|||
// Update verifies permissions and updates the specified file
|
||||
func (c *FileController) Update(ctx context.Context, userID int64, file ente.File, app ente.App) (ente.UpdateFileResponse, error) {
|
||||
var response ente.UpdateFileResponse
|
||||
objectPathPrefix := strconv.FormatInt(userID, 10) + "/"
|
||||
if !strings.HasPrefix(file.File.ObjectKey, objectPathPrefix) || !strings.HasPrefix(file.Thumbnail.ObjectKey, objectPathPrefix) {
|
||||
return response, stacktrace.Propagate(ente.ErrBadRequest, "Incorrect object key reported")
|
||||
err := c.validateFileCreateOrUpdateReq(userID, file)
|
||||
if err != nil {
|
||||
return response, stacktrace.Propagate(err, "")
|
||||
}
|
||||
ownerID, err := c.FileRepo.GetOwnerID(file.ID)
|
||||
if err != nil {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@/media": "*",
|
||||
"@/next": "*",
|
||||
"@ente/accounts": "*",
|
||||
"@ente/eslint-config": "*",
|
||||
"@ente/shared": "*",
|
||||
"jszip": "3.10.1",
|
||||
"mime-types": "^2.1.35"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,24 @@
|
|||
import { SlideshowContext } from "pages/slideshow";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function PhotoAuditorium({
|
||||
url,
|
||||
nextSlideUrl,
|
||||
}: {
|
||||
interface PhotoAuditoriumProps {
|
||||
url: string;
|
||||
nextSlideUrl: string;
|
||||
}) {
|
||||
const { showNextSlide } = useContext(SlideshowContext);
|
||||
|
||||
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
|
||||
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
|
||||
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
|
||||
|
||||
showNextSlide: () => void;
|
||||
}
|
||||
export const PhotoAuditorium: React.FC<PhotoAuditoriumProps> = ({
|
||||
url,
|
||||
nextSlideUrl,
|
||||
showNextSlide,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
let timeout2: NodeJS.Timeout;
|
||||
|
||||
if (nextSlidePrerendered) {
|
||||
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
|
||||
const delayTime = Math.max(10000 - elapsedTime, 0);
|
||||
|
||||
if (elapsedTime >= 10000) {
|
||||
setShowPreloadedNextSlide(true);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
setShowPreloadedNextSlide(true);
|
||||
}, delayTime);
|
||||
}
|
||||
|
||||
if (showNextSlide) {
|
||||
timeout2 = setTimeout(() => {
|
||||
showNextSlide();
|
||||
setNextSlidePrerendered(false);
|
||||
setPrerenderTime(null);
|
||||
setShowPreloadedNextSlide(false);
|
||||
}, delayTime);
|
||||
}
|
||||
}
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
showNextSlide();
|
||||
}, 10000);
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
if (timeout2) clearTimeout(timeout2);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
|
||||
}, [showNextSlide]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -69,27 +43,22 @@ export default function PhotoAuditorium({
|
|||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "none" : "block",
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={nextSlideUrl}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "block" : "none",
|
||||
display: "none",
|
||||
}}
|
||||
onLoad={() => {
|
||||
setNextSlidePrerendered(true);
|
||||
setPrerenderTime(Date.now());
|
||||
/>
|
||||
<img
|
||||
src={url}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
import { SlideshowContext } from "pages/slideshow";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
export default function PhotoAuditorium({
|
||||
url,
|
||||
nextSlideUrl,
|
||||
}: {
|
||||
url: string;
|
||||
nextSlideUrl: string;
|
||||
}) {
|
||||
const { showNextSlide } = useContext(SlideshowContext);
|
||||
|
||||
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
|
||||
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
|
||||
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
let timeout2: NodeJS.Timeout;
|
||||
|
||||
if (nextSlidePrerendered) {
|
||||
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
|
||||
const delayTime = Math.max(10000 - elapsedTime, 0);
|
||||
|
||||
if (elapsedTime >= 10000) {
|
||||
setShowPreloadedNextSlide(true);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
setShowPreloadedNextSlide(true);
|
||||
}, delayTime);
|
||||
}
|
||||
|
||||
if (showNextSlide) {
|
||||
timeout2 = setTimeout(() => {
|
||||
showNextSlide();
|
||||
setNextSlidePrerendered(false);
|
||||
setPrerenderTime(null);
|
||||
setShowPreloadedNextSlide(false);
|
||||
}, delayTime);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
if (timeout2) clearTimeout(timeout2);
|
||||
};
|
||||
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
backgroundImage: `url(${url})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundBlendMode: "multiply",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "none" : "block",
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={nextSlideUrl}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "block" : "none",
|
||||
}}
|
||||
onLoad={() => {
|
||||
setNextSlidePrerendered(true);
|
||||
setPrerenderTime(Date.now());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import mime from "mime-types";
|
||||
import { SlideshowContext } from "pages/slideshow";
|
||||
import { useContext, useEffect, useRef } from "react";
|
||||
|
||||
export default function VideoAuditorium({
|
||||
name,
|
||||
url,
|
||||
}: {
|
||||
name: string;
|
||||
url: string;
|
||||
}) {
|
||||
const { showNextSlide } = useContext(SlideshowContext);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
attemptPlay();
|
||||
}, [url, videoRef]);
|
||||
|
||||
const attemptPlay = async () => {
|
||||
if (videoRef.current) {
|
||||
try {
|
||||
await videoRef.current.play();
|
||||
} catch {
|
||||
showNextSlide();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
controls
|
||||
style={{
|
||||
maxWidth: "100vw",
|
||||
maxHeight: "100vh",
|
||||
}}
|
||||
onError={showNextSlide}
|
||||
onEnded={showNextSlide}
|
||||
>
|
||||
<source src={url} type={mime.lookup(name)} />
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import { FILE_TYPE } from "constants/file";
|
||||
import PhotoAuditorium from "./PhotoAuditorium";
|
||||
// import VideoAuditorium from './VideoAuditorium';
|
||||
|
||||
interface fileProp {
|
||||
fileName: string;
|
||||
fileURL: string;
|
||||
type: FILE_TYPE;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
file1: fileProp;
|
||||
file2: fileProp;
|
||||
}
|
||||
|
||||
export default function Theatre(props: IProps) {
|
||||
switch (props.file1.type && props.file2.type) {
|
||||
case FILE_TYPE.IMAGE:
|
||||
return (
|
||||
<PhotoAuditorium
|
||||
url={props.file1.fileURL}
|
||||
nextSlideUrl={props.file2.fileURL}
|
||||
/>
|
||||
);
|
||||
// case FILE_TYPE.VIDEO:
|
||||
// return (
|
||||
// <VideoAuditorium name={props.fileName} url={props.fileURL} />
|
||||
// );
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue