Merge remote-tracking branch 'origin/main' into beta
This commit is contained in:
commit
bd5c56349a
39 changed files with 464 additions and 691 deletions
|
@ -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"],
|
||||
|
|
9
auth/assets/custom-icons/icons/dcs.svg
Normal file
9
auth/assets/custom-icons/icons/dcs.svg
Normal 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 |
1
auth/assets/custom-icons/icons/wyze.svg
Normal file
1
auth/assets/custom-icons/icons/wyze.svg
Normal 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 |
|
@ -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();
|
||||
});
|
||||
};
|
|
@ -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
12
desktop/src/main/fs.ts
Normal 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 });
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
}
|
|
@ -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) => {
|
||||
|
|
|
@ -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/" },
|
||||
{
|
||||
|
|
17
mobile/gallery_scroll_perf_test.sh
Executable file
17
mobile/gallery_scroll_perf_test.sh
Executable 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 $?
|
|
@ -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();
|
||||
}
|
127
mobile/integration_test/home_gallery_scroll_test.dart
Normal file
127
mobile/integration_test/home_gallery_scroll_test.dart
Normal 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();
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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--;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -174,7 +174,7 @@ async function createCollectionDownloadFolder(
|
|||
downloadDirPath: string,
|
||||
collectionName: string,
|
||||
) {
|
||||
const collectionDownloadName = getUniqueCollectionExportName(
|
||||
const collectionDownloadName = await getUniqueCollectionExportName(
|
||||
downloadDirPath,
|
||||
collectionName,
|
||||
);
|
||||
|
|
|
@ -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]}`;
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`;
|
||||
}
|
|
@ -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>;
|
||||
|
|
28
web/packages/shared/media/video-playback.ts
Normal file
28
web/packages/shared/media/video-playback.ts
Normal 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;
|
||||
});
|
||||
}
|
Loading…
Add table
Reference in a new issue