Merge remote-tracking branch 'origin/main' into beta

This commit is contained in:
Prateek Sunal 2024-03-25 12:26:31 +05:30
commit bd5c56349a
39 changed files with 464 additions and 691 deletions

View file

@ -62,6 +62,11 @@
{
"title": "Crowdpear"
},
{
"title": "DCS",
"altNames": ["Digital Combat Simulator"],
"slug": "dcs"
},
{
"title": "DEGIRO"
},
@ -360,6 +365,10 @@
{
"title": "Wise"
},
{
"title": "WYZE",
"slug": "wyze"
},
{
"title": "X",
"altNames": ["twitter"],

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;">
<g transform="matrix(1.59257,0,0,1.59257,0,9.06171)">
<path d="M0.87,0.08L3.17,0.08C4.2,0.08 5.13,0.51 5.13,1.67C5.13,2.92 3.91,3.6 2.78,3.6L0.06,3.6C0.06,3.6 0.87,0.09 0.87,0.08ZM2.85,2.82C3.44,2.82 4.14,2.39 4.14,1.74C4.14,1.19 3.7,0.85 3.17,0.85L1.71,0.85L1.26,2.81L2.85,2.81L2.85,2.82Z" style="fill:rgb(245,158,15);fill-rule:nonzero;stroke:rgb(245,158,15);stroke-width:0.09px;"/>
<path d="M11.95,1.33C11.87,1.31 11.65,1.26 11.65,1.14C11.65,0.81 12.52,0.81 12.74,0.81C13.25,0.81 13.96,0.91 14.4,1.17L15,0.59C14.39,0.18 13.59,0.04 12.86,0.04C12.07,0.04 10.65,0.18 10.65,1.24C10.65,2.45 13.65,1.96 13.65,2.52C13.65,2.86 12.89,2.88 12.66,2.88C11.9,2.88 11.39,2.74 10.77,2.29C10.57,2.48 10.36,2.67 10.16,2.86C10.95,3.44 11.7,3.64 12.67,3.64C13.4,3.64 14.68,3.36 14.68,2.42C14.68,1.34 12.67,1.5 11.97,1.32L11.95,1.32L11.95,1.33Z" style="fill:rgb(245,158,15);fill-rule:nonzero;stroke:rgb(245,158,15);stroke-width:0.09px;"/>
<path d="M9.18,2.35L9.16,2.35C9.07,2.43 8.41,2.95 7.67,2.87C7.14,2.81 6.41,2.44 6.41,1.95C6.41,1.15 7.26,0.83 7.94,0.83C8.39,0.83 8.82,0.93 9.17,1.18C9.48,1.07 9.8,0.97 10.11,0.86C9.59,0.3 8.82,0.07 8.06,0.07C6.92,0.07 5.43,0.73 5.43,2.06C5.43,3.22 6.65,3.63 7.61,3.63C8.41,3.63 9.18,3.4 9.78,2.88C9.78,2.88 9.18,2.38 9.17,2.37L9.16,2.37L9.18,2.35Z" style="fill:rgb(245,158,15);fill-rule:nonzero;stroke:rgb(245,158,15);stroke-width:0.09px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1 @@
<svg fill="#1DF0BB" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Wyze</title><path d="M5.475 13.171 7.3 9.469h.974L5.779 14.53h-.608l-1.034-2.082-1.034 2.082h-.609L0 9.469h.973l1.826 3.673.851-1.706-.973-1.967h.973l1.825 3.702Zm8.457-3.702-2.251 3.442v1.591h-.882v-1.591L8.517 9.469h1.034l1.673 2.545 1.673-2.545h1.035Zm5.444 4.194H24v.867h-4.624v-.867Zm0-4.194H24v.868h-4.624v-.868Zm0 2.083H24v.867h-4.624v-.867Zm-.273-2.083-3.438 4.223h3.133v.838H13.84l3.407-4.222h-3.042v-.839h4.898Z"/></svg>

After

Width:  |  Height:  |  Size: 523 B

View file

@ -1,18 +0,0 @@
import { ipcRenderer } from "electron";
import { AppUpdateInfo } from "../types";
export const registerUpdateEventListener = (
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
) => {
ipcRenderer.removeAllListeners("show-update-dialog");
ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => {
showUpdateDialog(updateInfo);
});
};
export const registerForegroundEventListener = (onForeground: () => void) => {
ipcRenderer.removeAllListeners("app-in-foreground");
ipcRenderer.on("app-in-foreground", () => {
onForeground();
});
};

View file

@ -19,7 +19,7 @@ import { logErrorSentry, setupLogging } from "./main/log";
import { initWatcher } from "./services/chokidar";
import { addAllowOriginHeader } from "./utils/cors";
import { createWindow } from "./utils/createWindow";
import { setupAppEventEmitter } from "./utils/events";
import setupIpcComs from "./utils/ipcComms";
import {
handleDockIconHideOnAutoLaunch,
@ -127,6 +127,13 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
}
};
function setupAppEventEmitter(mainWindow: BrowserWindow) {
// fire event when mainWindow is in foreground
mainWindow.on("focus", () => {
mainWindow.webContents.send("app-in-foreground");
});
}
const main = () => {
setupLogging(isDev);

12
desktop/src/main/fs.ts Normal file
View file

@ -0,0 +1,12 @@
/**
* @file file system related functions exposed over the context bridge.
*/
import { existsSync } from "node:fs";
import * as fs from "node:fs/promises";
export const fsExists = (path: string) => existsSync(path);
/* TODO: Audit below this */
export const checkExistsAndCreateDir = (dirPath: string) =>
fs.mkdir(dirPath, { recursive: true });

View file

@ -7,12 +7,17 @@
*/
import { ipcMain } from "electron/main";
import { appVersion } from "../services/appUpdater";
import { clearElectronStore } from "../api/electronStore";
import {
appVersion,
muteUpdateNotification,
skipAppUpdate,
updateAndRestart,
} from "../services/appUpdater";
import { checkExistsAndCreateDir, fsExists } from "./fs";
import { openDirectory, openLogDirectory } from "./general";
import { logToDisk } from "./log";
// - General
export const attachIPCHandlers = () => {
// Notes:
//
@ -39,5 +44,26 @@ export const attachIPCHandlers = () => {
ipcMain.handle("openLogDirectory", (_) => openLogDirectory());
// See: [Note: Catching exception during .send/.on]
ipcMain.on("logToDisk", (_, msg) => logToDisk(msg));
ipcMain.on("logToDisk", (_, message) => logToDisk(message));
ipcMain.handle("fsExists", (_, path) => fsExists(path));
ipcMain.handle("checkExistsAndCreateDir", (_, dirPath) =>
checkExistsAndCreateDir(dirPath),
);
ipcMain.on("clear-electron-store", (_) => {
clearElectronStore();
});
ipcMain.on("update-and-restart", (_) => {
updateAndRestart();
});
ipcMain.on("skip-app-update", (_, version) => {
skipAppUpdate(version);
});
ipcMain.on("mute-update-notification", (_, version) => {
muteUpdateNotification(version);
});
};

View file

@ -35,10 +35,6 @@ import { runFFmpegCmd } from "./api/ffmpeg";
import { getDirFiles } from "./api/fs";
import { convertToJPEG, generateImageThumbnail } from "./api/imageProcessor";
import { getEncryptionKey, setEncryptionKey } from "./api/safeStorage";
import {
registerForegroundEventListener,
registerUpdateEventListener,
} from "./api/system";
import {
getElectronFilesFromGoogleZip,
getPendingUploads,
@ -88,6 +84,59 @@ const openLogDirectory = (): Promise<void> =>
const logToDisk = (message: string): void =>
ipcRenderer.send("logToDisk", message);
/**
* Return true if there is a file or directory at the given
* {@link path}.
*/
const fsExists = (path: string): Promise<boolean> =>
ipcRenderer.invoke("fsExists", path);
// - AUDIT below this
const checkExistsAndCreateDir = (dirPath: string): Promise<void> =>
ipcRenderer.invoke("checkExistsAndCreateDir", dirPath);
/* preload: duplicated */
interface AppUpdateInfo {
autoUpdatable: boolean;
version: string;
}
const registerUpdateEventListener = (
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
) => {
ipcRenderer.removeAllListeners("show-update-dialog");
ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => {
showUpdateDialog(updateInfo);
});
};
const registerForegroundEventListener = (onForeground: () => void) => {
ipcRenderer.removeAllListeners("app-in-foreground");
ipcRenderer.on("app-in-foreground", () => {
onForeground();
});
};
const clearElectronStore = () => {
ipcRenderer.send("clear-electron-store");
};
// - App update
const updateAndRestart = () => {
ipcRenderer.send("update-and-restart");
};
const skipAppUpdate = (version: string) => {
ipcRenderer.send("skip-app-update", version);
};
const muteUpdateNotification = (version: string) => {
ipcRenderer.send("mute-update-notification", version);
};
// - FIXME below this
/* preload: duplicated logError */
@ -161,11 +210,6 @@ const writeNodeStream = async (
// - Export
const exists = (path: string) => existsSync(path);
const checkExistsAndCreateDir = (dirPath: string) =>
fs.mkdir(dirPath, { recursive: true });
const saveStreamToDisk = writeStream;
const saveFileToDisk = (path: string, contents: string) =>
@ -363,24 +407,6 @@ const selectDirectory = async (): Promise<string> => {
}
};
const clearElectronStore = () => {
ipcRenderer.send("clear-electron-store");
};
// - App update
const updateAndRestart = () => {
ipcRenderer.send("update-and-restart");
};
const skipAppUpdate = (version: string) => {
ipcRenderer.send("skip-app-update", version);
};
const muteUpdateNotification = (version: string) => {
ipcRenderer.send("mute-update-notification", version);
};
// -
// These objects exposed here will become available to the JS code in our
@ -419,6 +445,8 @@ contextBridge.exposeInMainWorld("ElectronAPIs", {
// General
appVersion,
openDirectory,
registerForegroundEventListener,
clearElectronStore,
// Logging
openLogDirectory,
@ -428,15 +456,22 @@ contextBridge.exposeInMainWorld("ElectronAPIs", {
updateAndRestart,
skipAppUpdate,
muteUpdateNotification,
registerUpdateEventListener,
// - FS
fs: {
exists: fsExists,
},
// - FS legacy
// TODO: Move these into fs + document + rename if needed
checkExistsAndCreateDir,
// - Export
exists,
checkExistsAndCreateDir,
saveStreamToDisk,
saveFileToDisk,
selectDirectory,
clearElectronStore,
readTextFile,
showUploadFilesDialog,
showUploadDirsDialog,
@ -456,11 +491,9 @@ contextBridge.exposeInMainWorld("ElectronAPIs", {
updateWatchMappingSyncedFiles,
updateWatchMappingIgnoredFiles,
convertToJPEG,
registerUpdateEventListener,
runFFmpegCmd,
generateImageThumbnail,
registerForegroundEventListener,
moveFile,
deleteFolder,
rename,

View file

@ -1,8 +0,0 @@
import { BrowserWindow } from "electron";
export function setupAppEventEmitter(mainWindow: BrowserWindow) {
// fire event when mainWindow is in foreground
mainWindow.on("focus", () => {
mainWindow.webContents.send("app-in-foreground");
});
}

View file

@ -9,7 +9,6 @@ import {
Tray,
} from "electron";
import path from "path";
import { clearElectronStore } from "../api/electronStore";
import { attachIPCHandlers } from "../main/ipc";
import {
muteUpdateNotification,
@ -88,43 +87,10 @@ export default function setupIpcComs(
return safeStorage.decryptString(message);
});
ipcMain.on("clear-electron-store", () => {
clearElectronStore();
});
ipcMain.handle("convert-to-jpeg", (_, fileData, filename) => {
return convertToJPEG(fileData, filename);
});
ipcMain.handle("open-log-dir", () => {
// [Note: Electron app paths]
//
// By default, these paths are at the following locations:
//
// * macOS: `~/Library/Application Support/ente`
// * Linux: `~/.config/ente`
// * Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local\ente`
// * Windows: C:\Users\<you>\AppData\Local\<Your App Name>
//
// https://www.electronjs.org/docs/latest/api/app
shell.openPath(app.getPath("logs"));
});
ipcMain.handle("open-dir", (_, dirPath) => {
shell.openPath(path.normalize(dirPath));
});
ipcMain.on("update-and-restart", () => {
updateAndRestart();
});
ipcMain.on("skip-app-update", (_, version) => {
skipAppUpdate(version);
});
ipcMain.on("mute-update-notification", (_, version) => {
muteUpdateNotification(version);
});
ipcMain.handle(
"run-ffmpeg-cmd",
(_, cmd, inputFilePath, outputFileName, dontTimeout) => {

View file

@ -138,7 +138,7 @@ export const sidebar = [
{ text: "FAQ", link: "/auth/faq/" },
{
text: "Migration",
collapsed: false,
collapsed: true,
items: [
{ text: "Introduction", link: "/auth/migration-guides/" },
{

View file

@ -0,0 +1,17 @@
#!/bin/bash
# Make sure to go through home_gallery_scroll_test.dart and
# fill in email and password.
# Specify destination directory for the perf results in perf_driver.dart.
export ENDPOINT="https://api.ente.io"
flutter drive \
--driver=test_driver/perf_driver.dart \
--target=integration_test/home_gallery_scroll_test.dart \
--dart-define=endpoint=$ENDPOINT \
--profile --flavor independent \
--no-dds
exit $?

View file

@ -1,122 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "package:photos/main.dart" as app;
import "package:scrollable_positioned_list/scrollable_positioned_list.dart";
void main() {
group("App test", () {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
testWidgets("Demo test", (tester) async {
app.main();
await tester.pumpAndSettle(const Duration(seconds: 5));
await dismissUpdateAppDialog(tester);
//Automatically clicks the sign in button on the landing page
final signInButton = find.byKey(const ValueKey("signInButton"));
await tester.tap(signInButton);
await tester.pumpAndSettle();
//Need to enter email address manually and clicks the login button automatically
final emailInputField = find.byKey(const ValueKey("emailInputField"));
final logInButton = find.byKey(const ValueKey("logInButton"));
await tester.tap(emailInputField);
await tester.pumpAndSettle(const Duration(seconds: 12));
await findAndTapFAB(tester, logInButton);
//Need to enter OTT manually and clicks the verify button automatically
final ottVerificationInputField =
find.byKey(const ValueKey("ottVerificationInputField"));
final verifyOttButton = find.byKey(const ValueKey("verifyOttButton"));
await tester.tap(ottVerificationInputField);
await tester.pumpAndSettle(const Duration(seconds: 6));
await findAndTapFAB(tester, verifyOttButton);
//Need to enter password manually and clicks the verify button automatically
final passwordInputField =
find.byKey(const ValueKey("passwordInputField"));
final verifyPasswordButton =
find.byKey(const ValueKey("verifyPasswordButton"));
await tester.tap(passwordInputField);
await tester.pumpAndSettle(const Duration(seconds: 10));
await findAndTapFAB(tester, verifyPasswordButton);
await tester.pumpAndSettle(const Duration(seconds: 1));
await dismissUpdateAppDialog(tester);
//Grant permission to access photos. Must manually click the system dialog.
final grantPermissionButton =
find.byKey(const ValueKey("grantPermissionButton"));
await tester.tap(grantPermissionButton);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.pumpAndSettle(const Duration(seconds: 3));
//Automatically skips backup
final skipBackupButton = find.byKey(const ValueKey("skipBackupButton"));
await tester.tap(skipBackupButton);
await tester.pumpAndSettle(const Duration(seconds: 2));
await binding.traceAction(
() async {
//scroll gallery
final scrollablePositionedList =
find.byType(ScrollablePositionedList);
await tester.fling(
scrollablePositionedList,
const Offset(0, -5000),
4500,
);
await tester.pumpAndSettle();
await tester.fling(
scrollablePositionedList,
const Offset(0, 5000),
4500,
);
await tester.fling(
scrollablePositionedList,
const Offset(0, -7000),
4500,
);
await tester.pumpAndSettle();
await tester.fling(
scrollablePositionedList,
const Offset(0, 7000),
4500,
);
await tester.fling(
scrollablePositionedList,
const Offset(0, -9000),
4500,
);
await tester.pumpAndSettle();
await tester.fling(
scrollablePositionedList,
const Offset(0, 9000),
4500,
);
await tester.pumpAndSettle();
},
reportKey: 'scrolling_summary',
);
});
});
}
Future<void> findAndTapFAB(WidgetTester tester, Finder finder) async {
final RenderBox box = tester.renderObject(finder);
final Offset desiredOffset = Offset(box.size.width - 10, box.size.height / 2);
// Calculate the global position of the desired offset within the widget.
final Offset globalPosition = box.localToGlobal(desiredOffset);
await tester.tapAt(globalPosition);
await tester.pumpAndSettle(const Duration(seconds: 3));
}
Future<void> dismissUpdateAppDialog(WidgetTester tester) async {
await tester.tapAt(const Offset(0, 0));
await tester.pumpAndSettle();
}

View file

@ -0,0 +1,127 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "package:logging/logging.dart";
import "package:photos/main.dart" as app;
import "package:scrollable_positioned_list/scrollable_positioned_list.dart";
void main() {
group("Home gallery scroll test", () {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
testWidgets("Home gallery scroll test", semanticsEnabled: false,
(tester) async {
// https://github.com/flutter/flutter/issues/89749#issuecomment-1029965407
tester.testTextInput.register();
await runZonedGuarded(
() async {
///Ignore exceptions thrown by the app for the test to pass
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (FlutterErrorDetails errorDetails) {
FlutterError.dumpErrorToConsole(errorDetails);
};
app.main();
await tester.pumpAndSettle(const Duration(seconds: 1));
await dismissUpdateAppDialog(tester);
final signInButton = find.byKey(const ValueKey("signInButton"));
await tester.tap(signInButton);
await tester.pumpAndSettle();
final emailInputField = find.byType(TextFormField);
final logInButton = find.byKey(const ValueKey("logInButton"));
//Fill email id here
await tester.enterText(emailInputField, "enter email here");
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.tap(logInButton);
await tester.pumpAndSettle(const Duration(seconds: 3));
final passwordInputField =
find.byKey(const ValueKey("passwordInputField"));
final verifyPasswordButton =
find.byKey(const ValueKey("verifyPasswordButton"));
//Fill password here
await tester.enterText(passwordInputField, "enter password here");
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.tap(verifyPasswordButton);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(seconds: 1));
await dismissUpdateAppDialog(tester);
//Grant permission to access photos. Must manually click the system dialog.
final grantPermissionButton =
find.byKey(const ValueKey("grantPermissionButton"));
await tester.tap(grantPermissionButton);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.pumpAndSettle(const Duration(seconds: 3));
//Automatically skips backup
final skipBackupButton =
find.byKey(const ValueKey("skipBackupButton"));
await tester.tap(skipBackupButton);
await tester.pumpAndSettle(const Duration(seconds: 2));
await binding.traceAction(
() async {
//scroll gallery
final scrollablePositionedList =
find.byType(ScrollablePositionedList);
await tester.fling(
scrollablePositionedList,
const Offset(0, -5000),
4500,
);
await tester.pumpAndSettle();
await tester.fling(
scrollablePositionedList,
const Offset(0, 5000),
4500,
);
await tester.fling(
scrollablePositionedList,
const Offset(0, -7000),
4500,
);
await tester.pumpAndSettle();
await tester.fling(
scrollablePositionedList,
const Offset(0, 7000),
4500,
);
await tester.fling(
scrollablePositionedList,
const Offset(0, -9000),
4500,
);
await tester.pumpAndSettle();
await tester.fling(
scrollablePositionedList,
const Offset(0, 9000),
4500,
);
await tester.pumpAndSettle();
},
reportKey: 'home_gallery_scrolling_summary',
);
},
(error, stack) {
Logger("gallery_scroll_test").info(error, stack);
},
);
});
});
}
Future<void> dismissUpdateAppDialog(WidgetTester tester) async {
await tester.tapAt(const Offset(0, 0));
await tester.pumpAndSettle();
}

View file

@ -650,7 +650,7 @@ packages:
source: hosted
version: "0.6.0"
flutter_driver:
dependency: "direct dev"
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"

View file

@ -191,8 +191,6 @@ flutter_intl:
dev_dependencies:
build_runner: ^2.4.7
flutter_driver:
sdk: flutter
flutter_lints: ^2.0.1
flutter_test:
sdk: flutter

View file

@ -8,13 +8,13 @@ Future<void> main() {
responseDataCallback: (data) async {
if (data != null) {
final timeline = driver.Timeline.fromJson(
data['scrolling_summary'] as Map<String, dynamic>,
data['home_gallery_scrolling_summary'] as Map<String, dynamic>,
);
final summary = driver.TimelineSummary.summarize(timeline);
await summary.writeTimelineToFile(
'scrolling_summary',
'home_gallery_scrolling_summary',
pretty: true,
includeSummary: true,
//Specify destination directory for the timeline files.

View file

@ -1,10 +1,10 @@
import { addLogLine } from "@ente/shared/logging";
import { promiseWithTimeout } from "@ente/shared/promise";
import { logError } from "@ente/shared/sentry";
import QueueProcessor from "@ente/shared/utils/queueProcessor";
import { generateTempName } from "@ente/shared/utils/temp";
import { createFFmpeg, FFmpeg } from "ffmpeg-wasm";
import QueueProcessor from "services/queueProcessor";
import { getUint8ArrayView } from "services/readerService";
import { generateTempName } from "utils/temp";
const INPUT_PATH_PLACEHOLDER = "INPUT";
const FFMPEG_PLACEHOLDER = "FFMPEG";

View file

@ -2,8 +2,8 @@ import { CustomError } from "@ente/shared/error";
import { addLogLine } from "@ente/shared/logging";
import { retryAsyncFunction } from "@ente/shared/promise";
import { logError } from "@ente/shared/sentry";
import QueueProcessor from "@ente/shared/utils/queueProcessor";
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
import QueueProcessor from "services/queueProcessor";
import { getDedicatedConvertWorker } from "utils/comlink/ComlinkConvertWorker";
import { ComlinkWorker } from "utils/comlink/comlinkWorker";
import { DedicatedConvertWorker } from "worker/convert.worker";

View file

@ -1,57 +0,0 @@
// import { CollectionDownloadProgressAttributes } from 'components/Collections/CollectionDownloadProgress';
// import { CollectionSelectorAttributes } from 'components/Collections/CollectionSelector';
// import { TimeStampListItem } from 'components/PhotoList';
import { User } from "@ente/shared/user/types";
import { Collection } from "types/collection";
import { EnteFile } from "types/file";
export type SelectedState = {
[k: number]: boolean;
ownCount: number;
count: number;
collectionID: number;
};
export type SetFiles = React.Dispatch<React.SetStateAction<EnteFile[]>>;
export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
export type SetLoading = React.Dispatch<React.SetStateAction<boolean>>;
// export type SetCollectionSelectorAttributes = React.Dispatch<
// React.SetStateAction<CollectionSelectorAttributes>
// >;
// export type SetCollectionDownloadProgressAttributes = React.Dispatch<
// React.SetStateAction<CollectionDownloadProgressAttributes>
// >;
export type MergedSourceURL = {
original: string;
converted: string;
};
export enum UploadTypeSelectorIntent {
normalUpload,
import,
collectPhotos,
}
export type GalleryContextType = {
thumbs: Map<number, string>;
files: Map<number, MergedSourceURL>;
showPlanSelectorModal: () => void;
setActiveCollectionID: (collectionID: number) => void;
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
setBlockingLoad: (value: boolean) => void;
setIsInSearchMode: (value: boolean) => void;
// photoListHeader: TimeStampListItem;
openExportModal: () => void;
authenticateUser: (callback: () => void) => void;
user: User;
userIDToEmailMap: Map<number, string>;
emailList: string[];
openHiddenSection: (callback?: () => void) => void;
isClipSearchResult: boolean;
};
export enum CollectionSelectorIntent {
upload,
add,
move,
restore,
unhide,
}

View file

@ -17,59 +17,20 @@ import {
FileMagicMetadata,
FilePublicMagicMetadata,
} from "types/file";
import { SelectedState } from "types/gallery";
import { isArchivedFile } from "utils/magicMetadata";
import { CustomError } from "@ente/shared/error";
import { addLocalLog, addLogLine } from "@ente/shared/logging";
import { isPlaybackPossible } from "@ente/shared/media/video-playback";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { User } from "@ente/shared/user/types";
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
import isElectron from "is-electron";
import { FileTypeInfo } from "types/upload";
import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker";
import { isPlaybackPossible } from "utils/photoFrame";
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
export enum FILE_OPS_TYPE {
DOWNLOAD,
FIX_TIME,
ARCHIVE,
UNARCHIVE,
HIDE,
TRASH,
DELETE_PERMANENTLY,
}
export function groupFilesBasedOnCollectionID(files: EnteFile[]) {
const collectionWiseFiles = new Map<number, EnteFile[]>();
for (const file of files) {
if (!collectionWiseFiles.has(file.collectionID)) {
collectionWiseFiles.set(file.collectionID, []);
}
collectionWiseFiles.get(file.collectionID).push(file);
}
return collectionWiseFiles;
}
function getSelectedFileIds(selectedFiles: SelectedState) {
const filesIDs: number[] = [];
for (const [key, val] of Object.entries(selectedFiles)) {
if (typeof val === "boolean" && val) {
filesIDs.push(Number(key));
}
}
return new Set(filesIDs);
}
export function getSelectedFiles(
selected: SelectedState,
files: EnteFile[],
): EnteFile[] {
const selectedFilesIDs = getSelectedFileIds(selected);
return files.filter((file) => selectedFilesIDs.has(file.id));
}
export function sortFiles(files: EnteFile[], sortAsc = false) {
// sort based on the time of creation time of the file,
// for files with same creation time, sort based on the time of last modification

View file

@ -1,148 +0,0 @@
import { logError } from "@ente/shared/sentry";
import { FILE_TYPE } from "constants/file";
import { EnteFile } from "types/file";
import { MergedSourceURL } from "types/gallery";
const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000;
export async function isPlaybackPossible(url: string): Promise<boolean> {
return await new Promise((resolve) => {
const t = setTimeout(() => {
resolve(false);
}, WAIT_FOR_VIDEO_PLAYBACK);
const video = document.createElement("video");
video.addEventListener("canplay", function () {
clearTimeout(t);
video.remove(); // Clean up the video element
// also check for duration > 0 to make sure it is not a broken video
if (video.duration > 0) {
resolve(true);
} else {
resolve(false);
}
});
video.addEventListener("error", function () {
clearTimeout(t);
video.remove();
resolve(false);
});
video.src = url;
});
}
export async function playVideo(livePhotoVideo, livePhotoImage) {
const videoPlaying = !livePhotoVideo.paused;
if (videoPlaying) return;
livePhotoVideo.style.opacity = 1;
livePhotoImage.style.opacity = 0;
livePhotoVideo.load();
livePhotoVideo.play().catch(() => {
pauseVideo(livePhotoVideo, livePhotoImage);
});
}
export async function pauseVideo(livePhotoVideo, livePhotoImage) {
const videoPlaying = !livePhotoVideo.paused;
if (!videoPlaying) return;
livePhotoVideo.pause();
livePhotoVideo.style.opacity = 0;
livePhotoImage.style.opacity = 1;
}
export function updateFileMsrcProps(file: EnteFile, url: string) {
file.msrc = url;
file.isSourceLoaded = false;
file.conversionFailed = false;
file.isConverted = false;
if (file.metadata.fileType === FILE_TYPE.IMAGE) {
file.src = url;
} else {
file.html = `
<div class = 'pswp-item-container'>
<img src="${url}"/>
</div>
`;
}
}
export async function updateFileSrcProps(
file: EnteFile,
mergedURL: MergedSourceURL,
) {
const urls = {
original: mergedURL.original.split(","),
converted: mergedURL.converted.split(","),
};
let originalImageURL;
let originalVideoURL;
let convertedImageURL;
let convertedVideoURL;
let originalURL;
let isConverted;
let conversionFailed;
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
[originalImageURL, originalVideoURL] = urls.original;
[convertedImageURL, convertedVideoURL] = urls.converted;
isConverted =
originalVideoURL !== convertedVideoURL ||
originalImageURL !== convertedImageURL;
conversionFailed = !convertedVideoURL || !convertedImageURL;
} else if (file.metadata.fileType === FILE_TYPE.VIDEO) {
[originalVideoURL] = urls.original;
[convertedVideoURL] = urls.converted;
isConverted = originalVideoURL !== convertedVideoURL;
conversionFailed = !convertedVideoURL;
} else if (file.metadata.fileType === FILE_TYPE.IMAGE) {
[originalImageURL] = urls.original;
[convertedImageURL] = urls.converted;
isConverted = originalImageURL !== convertedImageURL;
conversionFailed = !convertedImageURL;
} else {
[originalURL] = urls.original;
isConverted = false;
conversionFailed = false;
}
const isPlayable = !isConverted || (isConverted && !conversionFailed);
file.w = window.innerWidth;
file.h = window.innerHeight;
file.isSourceLoaded = true;
file.originalImageURL = originalImageURL;
file.originalVideoURL = originalVideoURL;
file.isConverted = isConverted;
file.conversionFailed = conversionFailed;
if (!isPlayable) {
return;
}
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
file.html = `
<video controls onContextMenu="return false;">
<source src="${convertedVideoURL}" />
Your browser does not support the video tag.
</video>
`;
} else if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
file.html = `
<div class = 'pswp-item-container'>
<img id = "live-photo-image-${file.id}" src="${convertedImageURL}" onContextMenu="return false;"/>
<video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;">
<source src="${convertedVideoURL}" />
Your browser does not support the video tag.
</video>
</div>
`;
} else if (file.metadata.fileType === FILE_TYPE.IMAGE) {
file.src = convertedImageURL;
} else {
logError(
Error(`unknown file type - ${file.metadata.fileType}`),
"Unknown file type",
);
file.src = originalURL;
}
}

View file

@ -98,8 +98,8 @@ export default function ExportModal(props: Props) {
// HELPER FUNCTIONS
// =======================
const verifyExportFolderExists = () => {
if (!exportService.exportFolderExists(exportFolder)) {
const verifyExportFolderExists = async () => {
if (!(await exportService.exportFolderExists(exportFolder))) {
appContext.setDialogMessage(
getExportDirectoryDoesNotExistMessage(),
);
@ -109,7 +109,7 @@ export default function ExportModal(props: Props) {
const syncExportRecord = async (exportFolder: string): Promise<void> => {
try {
if (!exportService.exportFolderExists(exportFolder)) {
if (!(await exportService.exportFolderExists(exportFolder))) {
const pendingExports =
await exportService.getPendingExports(null);
setPendingExports(pendingExports);
@ -145,9 +145,9 @@ export default function ExportModal(props: Props) {
}
};
const toggleContinuousExport = () => {
const toggleContinuousExport = async () => {
try {
verifyExportFolderExists();
await verifyExportFolderExists();
const newContinuousExport = !continuousExport;
if (newContinuousExport) {
exportService.enableContinuousExport();
@ -162,7 +162,7 @@ export default function ExportModal(props: Props) {
const startExport = async () => {
try {
verifyExportFolderExists();
await verifyExportFolderExists();
await exportService.scheduleExport();
} catch (e) {
if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {

View file

@ -241,7 +241,11 @@ export default function App(props: EnteAppProps) {
}
await DownloadManager.init(APPS.PHOTOS, { token });
const exportSettings = exportService.getExportSettings();
if (!exportService.exportFolderExists(exportSettings?.folder)) {
if (
!(await exportService.exportFolderExists(
exportSettings?.folder,
))
) {
return;
}
const exportRecord = await exportService.getExportRecord(

View file

@ -42,6 +42,10 @@ import { CustomError } from "@ente/shared/error";
import { Events, eventBus } from "@ente/shared/events";
import { addLogLine } from "@ente/shared/logging";
import { User } from "@ente/shared/user/types";
import QueueProcessor, {
CancellationStatus,
RequestCanceller,
} from "@ente/shared/utils/queueProcessor";
import { ExportStage } from "constants/export";
import { FILE_TYPE } from "constants/file";
import { Collection } from "types/collection";
@ -56,10 +60,6 @@ import {
getCollectionUserFacingName,
getNonEmptyPersonalCollections,
} from "utils/collection";
import QueueProcessor, {
CancellationStatus,
RequestCanceller,
} from "../queueProcessor";
import { migrateExport } from "./migration";
const EXPORT_RECORD_FILE_NAME = "export_status.json";
@ -245,7 +245,7 @@ class ExportService {
};
async preExport(exportFolder: string) {
this.verifyExportFolderExists(exportFolder);
await this.verifyExportFolderExists(exportFolder);
const exportRecord = await this.getExportRecord(exportFolder);
await this.updateExportStage(ExportStage.MIGRATION);
await this.runMigration(
@ -259,7 +259,7 @@ class ExportService {
async postExport() {
try {
const exportFolder = this.getExportSettings()?.folder;
if (!this.exportFolderExists(exportFolder)) {
if (!(await this.exportFolderExists(exportFolder))) {
this.uiUpdater.setExportStage(ExportStage.INIT);
return;
}
@ -484,7 +484,7 @@ class ExportService {
if (isCanceled.status) {
throw Error(CustomError.EXPORT_STOPPED);
}
this.verifyExportFolderExists(exportFolder);
await this.verifyExportFolderExists(exportFolder);
const oldCollectionExportName =
collectionIDExportNameMap.get(collection.id);
const oldCollectionExportPath = getCollectionExportPath(
@ -493,7 +493,7 @@ class ExportService {
);
const newCollectionExportName =
getUniqueCollectionExportName(
await getUniqueCollectionExportName(
exportFolder,
getCollectionUserFacingName(collection),
);
@ -574,7 +574,7 @@ class ExportService {
if (isCanceled.status) {
throw Error(CustomError.EXPORT_STOPPED);
}
this.verifyExportFolderExists(exportFolder);
await this.verifyExportFolderExists(exportFolder);
addLogLine(
`removing collection with id ${collectionID} from export folder`,
);
@ -662,7 +662,7 @@ class ExportService {
throw Error(CustomError.EXPORT_STOPPED);
}
try {
this.verifyExportFolderExists(exportDir);
await this.verifyExportFolderExists(exportDir);
let collectionExportName = collectionIDFolderNameMap.get(
file.collectionID,
);
@ -743,7 +743,7 @@ class ExportService {
exportRecord.fileExportNames,
);
for (const fileUID of removedFileUIDs) {
this.verifyExportFolderExists(exportDir);
await this.verifyExportFolderExists(exportDir);
addLogLine(`trashing file with id ${fileUID}`);
if (isCanceled.status) {
throw Error(CustomError.EXPORT_STOPPED);
@ -769,10 +769,10 @@ class ExportService {
addLogLine(
`moving image file ${imageExportPath} to trash folder`,
);
if (this.exists(imageExportPath)) {
if (await this.exists(imageExportPath)) {
await ElectronAPIs.moveFile(
imageExportPath,
getTrashedFileExportPath(
await getTrashedFileExportPath(
exportDir,
imageExportPath,
),
@ -782,10 +782,12 @@ class ExportService {
const imageMetadataFileExportPath =
getMetadataFileExportPath(imageExportPath);
if (this.exists(imageMetadataFileExportPath)) {
if (
await this.exists(imageMetadataFileExportPath)
) {
await ElectronAPIs.moveFile(
imageMetadataFileExportPath,
getTrashedFileExportPath(
await getTrashedFileExportPath(
exportDir,
imageMetadataFileExportPath,
),
@ -799,10 +801,10 @@ class ExportService {
addLogLine(
`moving video file ${videoExportPath} to trash folder`,
);
if (this.exists(videoExportPath)) {
if (await this.exists(videoExportPath)) {
await ElectronAPIs.moveFile(
videoExportPath,
getTrashedFileExportPath(
await getTrashedFileExportPath(
exportDir,
videoExportPath,
),
@ -810,10 +812,12 @@ class ExportService {
}
const videoMetadataFileExportPath =
getMetadataFileExportPath(videoExportPath);
if (this.exists(videoMetadataFileExportPath)) {
if (
await this.exists(videoMetadataFileExportPath)
) {
await ElectronAPIs.moveFile(
videoMetadataFileExportPath,
getTrashedFileExportPath(
await getTrashedFileExportPath(
exportDir,
videoMetadataFileExportPath,
),
@ -824,14 +828,15 @@ class ExportService {
collectionExportPath,
fileExportName,
);
const trashedFilePath = getTrashedFileExportPath(
exportDir,
fileExportPath,
);
const trashedFilePath =
await getTrashedFileExportPath(
exportDir,
fileExportPath,
);
addLogLine(
`moving file ${fileExportPath} to ${trashedFilePath} trash folder`,
);
if (this.exists(fileExportPath)) {
if (await this.exists(fileExportPath)) {
await ElectronAPIs.moveFile(
fileExportPath,
trashedFilePath,
@ -839,10 +844,10 @@ class ExportService {
}
const metadataFileExportPath =
getMetadataFileExportPath(fileExportPath);
if (this.exists(metadataFileExportPath)) {
if (await this.exists(metadataFileExportPath)) {
await ElectronAPIs.moveFile(
metadataFileExportPath,
getTrashedFileExportPath(
await getTrashedFileExportPath(
exportDir,
metadataFileExportPath,
),
@ -995,9 +1000,9 @@ class ExportService {
async getExportRecord(folder: string, retry = true): Promise<ExportRecord> {
try {
this.verifyExportFolderExists(folder);
await this.verifyExportFolderExists(folder);
const exportRecordJSONPath = `${folder}/${EXPORT_RECORD_FILE_NAME}`;
if (!this.exists(exportRecordJSONPath)) {
if (!(await this.exists(exportRecordJSONPath))) {
return this.createEmptyExportRecord(exportRecordJSONPath);
}
const recordFile =
@ -1027,9 +1032,9 @@ class ExportService {
collectionID: number,
collectionIDNameMap: Map<number, string>,
) {
this.verifyExportFolderExists(exportFolder);
await this.verifyExportFolderExists(exportFolder);
const collectionName = collectionIDNameMap.get(collectionID);
const collectionExportName = getUniqueCollectionExportName(
const collectionExportName = await getUniqueCollectionExportName(
exportFolder,
collectionName,
);
@ -1070,7 +1075,7 @@ class ExportService {
file,
);
} else {
const fileExportName = getUniqueFileExportName(
const fileExportName = await getUniqueFileExportName(
collectionExportPath,
file.metadata.title,
);
@ -1109,11 +1114,11 @@ class ExportService {
) {
const fileBlob = await new Response(fileStream).blob();
const livePhoto = await decodeLivePhoto(file, fileBlob);
const imageExportName = getUniqueFileExportName(
const imageExportName = await getUniqueFileExportName(
collectionExportPath,
livePhoto.imageNameTitle,
);
const videoExportName = getUniqueFileExportName(
const videoExportName = await getUniqueFileExportName(
collectionExportPath,
livePhoto.videoNameTitle,
);
@ -1177,7 +1182,7 @@ class ExportService {
};
exists = (path: string) => {
return ElectronAPIs.exists(path);
return ElectronAPIs.fs.exists(path);
};
rename = (oldPath: string, newPath: string) => {
@ -1188,13 +1193,13 @@ class ExportService {
return ElectronAPIs.checkExistsAndCreateDir(path);
};
exportFolderExists = (exportFolder: string) => {
return exportFolder && this.exists(exportFolder);
exportFolderExists = async (exportFolder: string) => {
return exportFolder && (await this.exists(exportFolder));
};
private verifyExportFolderExists = (exportFolder: string) => {
private verifyExportFolderExists = async (exportFolder: string) => {
try {
if (!this.exportFolderExists(exportFolder)) {
if (!(await this.exportFolderExists(exportFolder))) {
throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST);
}
} catch (e) {

View file

@ -195,7 +195,7 @@ async function migrationV4ToV5(exportDir: string, exportRecord: ExportRecord) {
}
/*
This updates the folder name of already exported folders from the earlier format of
This updates the folder name of already exported folders from the earlier format of
`collectionID_collectionName` to newer `collectionName(numbered)` format
*/
async function migrateCollectionFolders(
@ -209,12 +209,12 @@ async function migrateCollectionFolders(
collection.id,
collection.name,
);
const newCollectionExportPath = getUniqueCollectionFolderPath(
const newCollectionExportPath = await getUniqueCollectionFolderPath(
exportDir,
collection.name,
);
collectionIDPathMap.set(collection.id, newCollectionExportPath);
if (!exportService.exists(oldCollectionExportPath)) {
if (!(await exportService.exists(oldCollectionExportPath))) {
continue;
}
await exportService.rename(
@ -230,7 +230,7 @@ async function migrateCollectionFolders(
}
/*
This updates the file name of already exported files from the earlier format of
This updates the file name of already exported files from the earlier format of
`fileID_fileName` to newer `fileName(numbered)` format
*/
async function migrateFiles(
@ -246,7 +246,7 @@ async function migrateFiles(
collectionIDPathMap.get(file.collectionID),
file,
);
const newFileSaveName = getUniqueFileSaveName(
const newFileSaveName = await getUniqueFileSaveName(
collectionIDPathMap.get(file.collectionID),
file.metadata.title,
);
@ -260,7 +260,7 @@ async function migrateFiles(
collectionIDPathMap.get(file.collectionID),
newFileSaveName,
);
if (!exportService.exists(oldFileSavePath)) {
if (!(await exportService.exists(oldFileSavePath))) {
continue;
}
await exportService.rename(oldFileSavePath, newFileSavePath);
@ -306,7 +306,7 @@ async function getCollectionExportNamesFromExportedCollectionPaths(
return exportedCollectionNames;
}
/*
/*
Earlier the file were sorted by id,
which we can use to determine which file got which number suffix
this can be used to determine the filepaths of the those already exported files
@ -432,17 +432,27 @@ async function removeCollectionExportMissingMetadataFolder(
return;
}
const properlyExportedCollections = Object.entries(
const properlyExportedCollectionsAll = Object.entries(
exportRecord.collectionExportNames,
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.filter(([_, collectionExportName]) =>
exportService.exists(
);
const properlyExportedCollections = [];
for (const [
collectionID,
collectionExportName,
] of properlyExportedCollectionsAll) {
if (
await exportService.exists(
getMetadataFolderExportPath(
getCollectionExportPath(exportDir, collectionExportName),
),
),
);
)
) {
properlyExportedCollections.push([
collectionID,
collectionExportName,
]);
}
}
const properlyExportedCollectionIDs = properlyExportedCollections.map(
([collectionID]) => collectionID,

View file

@ -1,86 +0,0 @@
import { CustomError } from "@ente/shared/error";
interface RequestQueueItem {
request: (canceller?: RequestCanceller) => Promise<any>;
successCallback: (response: any) => void;
failureCallback: (error: Error) => void;
isCanceled: { status: boolean };
canceller: { exec: () => void };
}
export enum PROCESSING_STRATEGY {
FIFO,
LIFO,
}
export interface RequestCanceller {
exec: () => void;
}
export interface CancellationStatus {
status: boolean;
}
export default class QueueProcessor<T> {
private requestQueue: RequestQueueItem[] = [];
private requestInProcessing = 0;
constructor(
private maxParallelProcesses: number,
private processingStrategy = PROCESSING_STRATEGY.FIFO,
) {}
public queueUpRequest(
request: (canceller?: RequestCanceller) => Promise<T>,
) {
const isCanceled: CancellationStatus = { status: false };
const canceller: RequestCanceller = {
exec: () => {
isCanceled.status = true;
},
};
const promise = new Promise<T>((resolve, reject) => {
this.requestQueue.push({
request,
successCallback: resolve,
failureCallback: reject,
isCanceled,
canceller,
});
this.pollQueue();
});
return { promise, canceller };
}
private async pollQueue() {
if (this.requestInProcessing < this.maxParallelProcesses) {
this.requestInProcessing++;
this.processQueue();
}
}
private async processQueue() {
while (this.requestQueue.length > 0) {
const queueItem =
this.processingStrategy === PROCESSING_STRATEGY.LIFO
? this.requestQueue.pop()
: this.requestQueue.shift();
let response = null;
if (queueItem.isCanceled.status) {
queueItem.failureCallback(Error(CustomError.REQUEST_CANCELLED));
} else {
try {
response = await queueItem.request(queueItem.canceller);
queueItem.successCallback(response);
} catch (e) {
queueItem.failureCallback(e);
}
}
}
this.requestInProcessing--;
}
}

View file

@ -1,10 +1,10 @@
import { addLogLine } from "@ente/shared/logging";
import { logError } from "@ente/shared/sentry";
import QueueProcessor from "@ente/shared/utils/queueProcessor";
import { generateTempName } from "@ente/shared/utils/temp";
import { createFFmpeg, FFmpeg } from "ffmpeg-wasm";
import QueueProcessor from "services/queueProcessor";
import { getUint8ArrayView } from "services/readerService";
import { promiseWithTimeout } from "utils/common";
import { generateTempName } from "utils/temp";
const INPUT_PATH_PLACEHOLDER = "INPUT";
const FFMPEG_PLACEHOLDER = "FFMPEG";

View file

@ -2,9 +2,9 @@ import { CustomError } from "@ente/shared/error";
import { addLogLine } from "@ente/shared/logging";
import { retryAsyncFunction } from "@ente/shared/promise";
import { logError } from "@ente/shared/sentry";
import QueueProcessor from "@ente/shared/utils/queueProcessor";
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
import { ComlinkWorker } from "@ente/shared/worker/comlinkWorker";
import QueueProcessor from "services/queueProcessor";
import { getDedicatedConvertWorker } from "utils/comlink/ComlinkConvertWorker";
import { DedicatedConvertWorker } from "worker/convert.worker";

View file

@ -174,7 +174,7 @@ async function createCollectionDownloadFolder(
downloadDirPath: string,
collectionName: string,
) {
const collectionDownloadName = getUniqueCollectionExportName(
const collectionDownloadName = await getUniqueCollectionExportName(
downloadDirPath,
collectionName,
);

View file

@ -197,16 +197,16 @@ export const getGoogleLikeMetadataFile = (
export const sanitizeName = (name: string) =>
sanitize(name, { replacement: "_" });
export const getUniqueCollectionExportName = (
export const getUniqueCollectionExportName = async (
dir: string,
collectionName: string,
): string => {
): Promise<string> => {
let collectionExportName = sanitizeName(collectionName);
let count = 1;
while (
exportService.exists(
(await exportService.exists(
getCollectionExportPath(dir, collectionExportName),
) ||
)) ||
collectionExportName === ENTE_TRASH_FOLDER
) {
collectionExportName = `${sanitizeName(collectionName)}(${count})`;
@ -218,14 +218,14 @@ export const getUniqueCollectionExportName = (
export const getMetadataFolderExportPath = (collectionExportPath: string) =>
`${collectionExportPath}/${ENTE_METADATA_FOLDER}`;
export const getUniqueFileExportName = (
export const getUniqueFileExportName = async (
collectionExportPath: string,
filename: string,
) => {
let fileExportName = sanitizeName(filename);
let count = 1;
while (
exportService.exists(
await exportService.exists(
getFileExportPath(collectionExportPath, fileExportName),
)
) {
@ -255,11 +255,14 @@ export const getFileExportPath = (
fileExportName: string,
) => `${collectionExportPath}/${fileExportName}`;
export const getTrashedFileExportPath = (exportDir: string, path: string) => {
export const getTrashedFileExportPath = async (
exportDir: string,
path: string,
) => {
const fileRelativePath = path.replace(`${exportDir}/`, "");
let trashedFilePath = `${exportDir}/${ENTE_TRASH_FOLDER}/${fileRelativePath}`;
let count = 1;
while (exportService.exists(trashedFilePath)) {
while (await exportService.exists(trashedFilePath)) {
const trashedFilePathParts = splitFilenameAndExtension(trashedFilePath);
if (trashedFilePathParts[1]) {
trashedFilePath = `${trashedFilePathParts[0]}(${count}).${trashedFilePathParts[1]}`;

View file

@ -41,13 +41,13 @@ export const getExportedFiles = (
export const oldSanitizeName = (name: string) =>
name.replaceAll("/", "_").replaceAll(" ", "_");
export const getUniqueCollectionFolderPath = (
export const getUniqueCollectionFolderPath = async (
dir: string,
collectionName: string,
): string => {
): Promise<string> => {
let collectionFolderPath = `${dir}/${sanitizeName(collectionName)}`;
let count = 1;
while (exportService.exists(collectionFolderPath)) {
while (await exportService.exists(collectionFolderPath)) {
collectionFolderPath = `${dir}/${sanitizeName(
collectionName,
)}(${count})`;
@ -59,14 +59,16 @@ export const getUniqueCollectionFolderPath = (
export const getMetadataFolderPath = (collectionFolderPath: string) =>
`${collectionFolderPath}/${ENTE_METADATA_FOLDER}`;
export const getUniqueFileSaveName = (
export const getUniqueFileSaveName = async (
collectionPath: string,
filename: string,
) => {
let fileSaveName = sanitizeName(filename);
let count = 1;
while (
exportService.exists(getFileSavePath(collectionPath, fileSaveName))
await exportService.exists(
getFileSavePath(collectionPath, fileSaveName),
)
) {
const filenameParts = splitFilenameAndExtension(sanitizeName(filename));
if (filenameParts[1]) {

View file

@ -39,6 +39,7 @@ import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { CustomError } from "@ente/shared/error";
import { addLocalLog, addLogLine } from "@ente/shared/logging";
import { isPlaybackPossible } from "@ente/shared/media/video-playback";
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
import isElectron from "is-electron";
import { moveToHiddenCollection } from "services/collectionService";
@ -49,7 +50,6 @@ import {
updateFilePublicMagicMetadata,
} from "services/fileService";
import { FileTypeInfo } from "types/upload";
import { isPlaybackPossible } from "utils/photoFrame";
import { default as ElectronAPIs } from "@ente/shared/electron";
import { downloadUsingAnchor } from "@ente/shared/utils";
@ -778,7 +778,7 @@ export async function downloadFileDesktop(
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const fileBlob = await new Response(updatedFileStream).blob();
const livePhoto = await decodeLivePhoto(file, fileBlob);
const imageExportName = getUniqueFileExportName(
const imageExportName = await getUniqueFileExportName(
downloadPath,
livePhoto.imageNameTitle,
);
@ -788,7 +788,7 @@ export async function downloadFileDesktop(
imageStream,
);
try {
const videoExportName = getUniqueFileExportName(
const videoExportName = await getUniqueFileExportName(
downloadPath,
livePhoto.videoNameTitle,
);
@ -804,7 +804,7 @@ export async function downloadFileDesktop(
throw e;
}
} else {
const fileExportName = getUniqueFileExportName(
const fileExportName = await getUniqueFileExportName(
downloadPath,
file.metadata.title,
);

View file

@ -4,35 +4,6 @@ import { LivePhotoSourceURL, SourceURLs } from "services/download";
import { EnteFile } from "types/file";
import { SetSelectedState } from "types/gallery";
const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000;
export async function isPlaybackPossible(url: string): Promise<boolean> {
return await new Promise((resolve) => {
const t = setTimeout(() => {
resolve(false);
}, WAIT_FOR_VIDEO_PLAYBACK);
const video = document.createElement("video");
video.addEventListener("canplay", function () {
clearTimeout(t);
video.remove(); // Clean up the video element
// also check for duration > 0 to make sure it is not a broken video
if (video.duration > 0) {
resolve(true);
} else {
resolve(false);
}
});
video.addEventListener("error", function () {
clearTimeout(t);
video.remove();
resolve(false);
});
video.src = url;
});
}
export async function playVideo(livePhotoVideo, livePhotoImage) {
const videoPlaying = !livePhotoVideo.paused;
if (videoPlaying) return;

View file

@ -1,14 +0,0 @@
const CHARACTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
export function generateTempName(length: number, suffix: string) {
let tempName = "";
const charactersLength = CHARACTERS.length;
for (let i = 0; i < length; i++) {
tempName += CHARACTERS.charAt(
Math.floor(Math.random() * charactersLength),
);
}
return `${tempName}-${suffix}`;
}

View file

@ -50,8 +50,52 @@ export interface ElectronAPIsType {
*/
logToDisk: (message: string) => void;
exists: (path: string) => boolean;
/**
* A subset of filesystem access APIs.
*
* The renderer process, being a web process, does not have full access to
* the local filesystem apart from files explicitly dragged and dropped (or
* selected by the user in a native file open dialog).
*
* The main process, however, has full filesystem access (limited only be an
* OS level sandbox on the entire process).
*
* When we're running in the desktop app, we want to better utilize the
* local filesystem access to provide more integrated features to the user -
* things that are not currently possible using web technologies. For
* example, continuous exports to an arbitrary user chosen location on disk,
* or watching some folders for changes and syncing them automatically.
*
* Towards this end, this fs object provides some generic file system access
* functions that are needed for such features. In addition, there are other
* feature specific methods too in the top level electron object.
*/
fs: {
/**
* Return true if there is a file or directory at the given
* {@link path}.
*/
exists: (path: string) => Promise<boolean>;
};
/** TODO: AUDIT below this */
// - General
registerForegroundEventListener: (onForeground: () => void) => void;
clearElectronStore: () => void;
// - FS legacy
checkExistsAndCreateDir: (dirPath: string) => Promise<void>;
// - App update
updateAndRestart: () => void;
skipAppUpdate: (version: string) => void;
muteUpdateNotification: (version: string) => void;
registerUpdateEventListener: (
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
) => void;
/** TODO: FIXME or migrate below this */
saveStreamToDisk: (
path: string,
fileStream: ReadableStream<any>,
@ -97,31 +141,24 @@ export interface ElectronAPIsType {
removeFolder: (folderPath: string) => Promise<void>,
) => void;
isFolder: (dirPath: string) => Promise<boolean>;
clearElectronStore: () => void;
setEncryptionKey: (encryptionKey: string) => Promise<void>;
getEncryptionKey: () => Promise<string>;
convertToJPEG: (
fileData: Uint8Array,
filename: string,
) => Promise<Uint8Array>;
registerUpdateEventListener: (
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
) => void;
updateAndRestart: () => void;
skipAppUpdate: (version: string) => void;
runFFmpegCmd: (
cmd: string[],
inputFile: File | ElectronFile,
outputFileName: string,
dontTimeout?: boolean,
) => Promise<File>;
muteUpdateNotification: (version: string) => void;
generateImageThumbnail: (
inputFile: File | ElectronFile,
maxDimension: number,
maxSize: number,
) => Promise<Uint8Array>;
registerForegroundEventListener: (onForeground: () => void) => void;
moveFile: (oldPath: string, newPath: string) => Promise<void>;
deleteFolder: (path: string) => Promise<void>;
deleteFile: (path: string) => Promise<void>;

View file

@ -0,0 +1,28 @@
const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000;
export async function isPlaybackPossible(url: string): Promise<boolean> {
return await new Promise((resolve) => {
const t = setTimeout(() => {
resolve(false);
}, WAIT_FOR_VIDEO_PLAYBACK);
const video = document.createElement("video");
video.addEventListener("canplay", function () {
clearTimeout(t);
video.remove(); // Clean up the video element
// also check for duration > 0 to make sure it is not a broken video
if (video.duration > 0) {
resolve(true);
} else {
resolve(false);
}
});
video.addEventListener("error", function () {
clearTimeout(t);
video.remove();
resolve(false);
});
video.src = url;
});
}