Merge remote-tracking branch 'origin/main' into mobile-widgetsimproved
This commit is contained in:
commit
0956e3ccc4
26 changed files with 380 additions and 281 deletions
|
@ -9,7 +9,7 @@
|
|||
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
|
||||
*/
|
||||
import { nativeImage } from "electron";
|
||||
import { app, BrowserWindow, Menu, Tray } from "electron/main";
|
||||
import { app, BrowserWindow, Menu, protocol, Tray } from "electron/main";
|
||||
import serveNextAt from "next-electron-server";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
|
@ -27,6 +27,7 @@ import { setupAutoUpdater } from "./main/services/app-update";
|
|||
import autoLauncher from "./main/services/autoLauncher";
|
||||
import { initWatcher } from "./main/services/chokidar";
|
||||
import { userPreferences } from "./main/stores/user-preferences";
|
||||
import { registerStreamProtocol } from "./main/stream";
|
||||
import { isDev } from "./main/util";
|
||||
|
||||
/**
|
||||
|
@ -58,6 +59,21 @@ export const allowWindowClose = (): void => {
|
|||
shouldAllowWindowClose = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Log a standard startup banner.
|
||||
*
|
||||
* This helps us identify app starts and other environment details in the logs.
|
||||
*/
|
||||
const logStartupBanner = () => {
|
||||
const version = isDev ? "dev" : app.getVersion();
|
||||
log.info(`Starting ente-photos-desktop ${version}`);
|
||||
|
||||
const platform = process.platform;
|
||||
const osRelease = os.release();
|
||||
const systemVersion = process.getSystemVersion();
|
||||
log.info("Running on", { platform, osRelease, systemVersion });
|
||||
};
|
||||
|
||||
/**
|
||||
* next-electron-server allows up to directly use the output of `next build` in
|
||||
* production mode and `next dev` in development mode, whilst keeping the rest
|
||||
|
@ -74,18 +90,57 @@ export const allowWindowClose = (): void => {
|
|||
const setupRendererServer = () => serveNextAt(rendererURL);
|
||||
|
||||
/**
|
||||
* Log a standard startup banner.
|
||||
* Register privileged schemes.
|
||||
*
|
||||
* This helps us identify app starts and other environment details in the logs.
|
||||
* We have two privileged schemes:
|
||||
*
|
||||
* 1. "ente", used for serving our web app (@see {@link setupRendererServer}).
|
||||
*
|
||||
* 2. "stream", used for streaming IPC (@see {@link registerStreamProtocol}).
|
||||
*
|
||||
* Both of these need some privileges, however, the documentation for Electron's
|
||||
* [registerSchemesAsPrivileged](https://www.electronjs.org/docs/latest/api/protocol)
|
||||
* says:
|
||||
*
|
||||
* > This method ... can be called only once.
|
||||
*
|
||||
* The library we use for the "ente" scheme, next-electron-server, already calls
|
||||
* it once when we invoke {@link setupRendererServer}.
|
||||
*
|
||||
* In practice calling it multiple times just causes the values to be
|
||||
* overwritten, and the last call wins. So we don't need to modify
|
||||
* next-electron-server to prevent it from calling registerSchemesAsPrivileged.
|
||||
* Instead, we (a) repeat what next-electron-server had done here, and (b)
|
||||
* ensure that we're called after {@link setupRendererServer}.
|
||||
*/
|
||||
const logStartupBanner = () => {
|
||||
const version = isDev ? "dev" : app.getVersion();
|
||||
log.info(`Starting ente-photos-desktop ${version}`);
|
||||
const registerPrivilegedSchemes = () => {
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
// Taken verbatim from next-electron-server's code (index.js)
|
||||
scheme: "ente",
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
allowServiceWorkers: true,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scheme: "stream",
|
||||
privileges: {
|
||||
// TODO(MR): Remove the commented bits if we don't end up
|
||||
// needing them by the time the IPC refactoring is done.
|
||||
|
||||
const platform = process.platform;
|
||||
const osRelease = os.release();
|
||||
const systemVersion = process.getSystemVersion();
|
||||
log.info("Running on", { platform, osRelease, systemVersion });
|
||||
// Prevent the insecure origin issues when fetching this
|
||||
// secure: true,
|
||||
// Allow the web fetch API in the renderer to use this scheme.
|
||||
supportFetchAPI: true,
|
||||
// Allow it to be used with video tags.
|
||||
// stream: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -251,8 +306,10 @@ const main = () => {
|
|||
let mainWindow: BrowserWindow | undefined;
|
||||
|
||||
initLogging();
|
||||
setupRendererServer();
|
||||
logStartupBanner();
|
||||
// The order of the next two calls is important
|
||||
setupRendererServer();
|
||||
registerPrivilegedSchemes();
|
||||
increaseDiskCache();
|
||||
|
||||
app.on("second-instance", () => {
|
||||
|
@ -269,11 +326,11 @@ const main = () => {
|
|||
// Note that some Electron APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
mainWindow = await createMainWindow();
|
||||
const watcher = initWatcher(mainWindow);
|
||||
setupTrayItem(mainWindow);
|
||||
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
|
||||
setupTrayItem(mainWindow);
|
||||
attachIPCHandlers();
|
||||
attachFSWatchIPCHandlers(watcher);
|
||||
attachFSWatchIPCHandlers(initWatcher(mainWindow));
|
||||
registerStreamProtocol();
|
||||
if (!isDev) setupAutoUpdater(mainWindow);
|
||||
handleDownloads(mainWindow);
|
||||
handleExternalLinks(mainWindow);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
/**
|
||||
* @file file system related functions exposed over the context bridge.
|
||||
*/
|
||||
import { createWriteStream, existsSync } from "node:fs";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
export const fsExists = (path: string) => existsSync(path);
|
||||
|
||||
|
@ -17,78 +16,13 @@ export const fsRmdir = (path: string) => fs.rmdir(path);
|
|||
|
||||
export const fsRm = (path: string) => fs.rm(path);
|
||||
|
||||
/**
|
||||
* Write a (web) ReadableStream to a file at the given {@link filePath}.
|
||||
*
|
||||
* The returned promise resolves when the write completes.
|
||||
*
|
||||
* @param filePath The local filesystem path where the file should be written.
|
||||
* @param readableStream A [web
|
||||
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
*/
|
||||
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
|
||||
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
|
||||
export const fsReadTextFile = async (filePath: string) =>
|
||||
fs.readFile(filePath, "utf-8");
|
||||
|
||||
/**
|
||||
* Convert a Web ReadableStream into a Node.js ReadableStream
|
||||
*
|
||||
* This can be used to, for example, write a ReadableStream obtained via
|
||||
* `net.fetch` into a file using the Node.js `fs` APIs
|
||||
*/
|
||||
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
|
||||
const reader = readableStream.getReader();
|
||||
const rs = new Readable();
|
||||
|
||||
rs._read = async () => {
|
||||
try {
|
||||
const result = await reader.read();
|
||||
|
||||
if (!result.done) {
|
||||
rs.push(Buffer.from(result.value));
|
||||
} else {
|
||||
rs.push(null);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
rs.emit("error", e);
|
||||
}
|
||||
};
|
||||
|
||||
return rs;
|
||||
};
|
||||
|
||||
const writeNodeStream = async (
|
||||
filePath: string,
|
||||
fileStream: NodeJS.ReadableStream,
|
||||
) => {
|
||||
const writeable = createWriteStream(filePath);
|
||||
|
||||
fileStream.on("error", (error) => {
|
||||
writeable.destroy(error); // Close the writable stream with an error
|
||||
});
|
||||
|
||||
fileStream.pipe(writeable);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeable.on("finish", resolve);
|
||||
writeable.on("error", async (e: unknown) => {
|
||||
if (existsSync(filePath)) {
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* TODO: Audit below this */
|
||||
|
||||
export const saveStreamToDisk = writeStream;
|
||||
|
||||
export const saveFileToDisk = (path: string, contents: string) =>
|
||||
export const fsWriteFile = (path: string, contents: string) =>
|
||||
fs.writeFile(path, contents);
|
||||
|
||||
export const readTextFile = async (filePath: string) =>
|
||||
fs.readFile(filePath, "utf-8");
|
||||
/* TODO: Audit below this */
|
||||
|
||||
export const isFolder = async (dirPath: string) => {
|
||||
if (!existsSync(dirPath)) return false;
|
||||
|
|
|
@ -20,13 +20,12 @@ import {
|
|||
import {
|
||||
fsExists,
|
||||
fsMkdirIfNeeded,
|
||||
fsReadTextFile,
|
||||
fsRename,
|
||||
fsRm,
|
||||
fsRmdir,
|
||||
fsWriteFile,
|
||||
isFolder,
|
||||
readTextFile,
|
||||
saveFileToDisk,
|
||||
saveStreamToDisk,
|
||||
} from "./fs";
|
||||
import { logToDisk } from "./log";
|
||||
import {
|
||||
|
@ -113,6 +112,26 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version));
|
||||
|
||||
// - FS
|
||||
|
||||
ipcMain.handle("fsExists", (_, path) => fsExists(path));
|
||||
|
||||
ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) =>
|
||||
fsRename(oldPath, newPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath));
|
||||
|
||||
ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path));
|
||||
|
||||
ipcMain.handle("fsRm", (_, path: string) => fsRm(path));
|
||||
|
||||
ipcMain.handle("fsReadTextFile", (_, path: string) => fsReadTextFile(path));
|
||||
|
||||
ipcMain.handle("fsWriteFile", (_, path: string, contents: string) =>
|
||||
fsWriteFile(path, contents),
|
||||
);
|
||||
|
||||
// - Conversion
|
||||
|
||||
ipcMain.handle("convertToJPEG", (_, fileData, filename) =>
|
||||
|
@ -164,34 +183,8 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
ipcMain.handle("showUploadZipDialog", () => showUploadZipDialog());
|
||||
|
||||
// - FS
|
||||
|
||||
ipcMain.handle("fsExists", (_, path) => fsExists(path));
|
||||
|
||||
ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) =>
|
||||
fsRename(oldPath, newPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath));
|
||||
|
||||
ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path));
|
||||
|
||||
ipcMain.handle("fsRm", (_, path: string) => fsRm(path));
|
||||
|
||||
// - FS Legacy
|
||||
|
||||
ipcMain.handle(
|
||||
"saveStreamToDisk",
|
||||
(_, path: string, fileStream: ReadableStream) =>
|
||||
saveStreamToDisk(path, fileStream),
|
||||
);
|
||||
|
||||
ipcMain.handle("saveFileToDisk", (_, path: string, contents: string) =>
|
||||
saveFileToDisk(path, contents),
|
||||
);
|
||||
|
||||
ipcMain.handle("readTextFile", (_, path: string) => readTextFile(path));
|
||||
|
||||
ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath));
|
||||
|
||||
// - Upload
|
||||
|
|
|
@ -2,7 +2,7 @@ import pathToFfmpeg from "ffmpeg-static";
|
|||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { ElectronFile } from "../../types/ipc";
|
||||
import { writeStream } from "../fs";
|
||||
import { writeStream } from "../stream";
|
||||
import log from "../log";
|
||||
import { generateTempFilePath, getTempDirPath } from "../temp";
|
||||
import { execAsync } from "../util";
|
||||
|
|
|
@ -2,7 +2,7 @@ import { existsSync } from "fs";
|
|||
import fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
import { CustomErrors, ElectronFile } from "../../types/ipc";
|
||||
import { writeStream } from "../fs";
|
||||
import { writeStream } from "../stream";
|
||||
import log from "../log";
|
||||
import { isPlatform } from "../platform";
|
||||
import { generateTempFilePath } from "../temp";
|
||||
|
|
|
@ -11,7 +11,7 @@ import fs from "node:fs/promises";
|
|||
import * as ort from "onnxruntime-node";
|
||||
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
|
||||
import { CustomErrors } from "../../types/ipc";
|
||||
import { writeStream } from "../fs";
|
||||
import { writeStream } from "../stream";
|
||||
import log from "../log";
|
||||
import { generateTempFilePath } from "../temp";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
|
|
|
@ -15,7 +15,7 @@ import { existsSync } from "fs";
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as ort from "onnxruntime-node";
|
||||
import { writeStream } from "../fs";
|
||||
import { writeStream } from "../stream";
|
||||
import log from "../log";
|
||||
|
||||
/**
|
||||
|
|
116
desktop/src/main/stream.ts
Normal file
116
desktop/src/main/stream.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* @file stream data to-from renderer using a custom protocol handler.
|
||||
*/
|
||||
import { protocol } from "electron/main";
|
||||
import { createWriteStream, existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Readable } from "node:stream";
|
||||
import log from "./log";
|
||||
|
||||
/**
|
||||
* Register a protocol handler that we use for streaming large files between the
|
||||
* main process (node) and the renderer process (browser) layer.
|
||||
*
|
||||
* [Note: IPC streams]
|
||||
*
|
||||
* When running without node integration, there is no direct way to pass streams
|
||||
* across IPC. And passing the entire contents of the file is not feasible for
|
||||
* large video files because of the memory pressure the copying would entail.
|
||||
*
|
||||
* As an alternative, we register a custom protocol handler that can provided a
|
||||
* bi-directional stream. The renderer can stream data to the node side by
|
||||
* streaming the request. The node side can stream to the renderer side by
|
||||
* streaming the response.
|
||||
*
|
||||
* See also: [Note: Transferring large amount of data over IPC]
|
||||
*
|
||||
* Depends on {@link registerPrivilegedSchemes}.
|
||||
*/
|
||||
export const registerStreamProtocol = () => {
|
||||
protocol.handle("stream", async (request: Request) => {
|
||||
const url = request.url;
|
||||
const { host, pathname } = new URL(url);
|
||||
// Convert e.g. "%20" to spaces.
|
||||
const path = decodeURIComponent(pathname);
|
||||
switch (host) {
|
||||
/* stream://write/path/to/file */
|
||||
/* host-pathname----- */
|
||||
case "write":
|
||||
try {
|
||||
await writeStream(path, request.body);
|
||||
return new Response("", { status: 200 });
|
||||
} catch (e) {
|
||||
log.error(`Failed to write stream for ${url}`, e);
|
||||
return new Response(
|
||||
`Failed to write stream: ${e.message}`,
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
default:
|
||||
return new Response("", { status: 404 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Write a (web) ReadableStream to a file at the given {@link filePath}.
|
||||
*
|
||||
* The returned promise resolves when the write completes.
|
||||
*
|
||||
* @param filePath The local filesystem path where the file should be written.
|
||||
* @param readableStream A [web
|
||||
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
*/
|
||||
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
|
||||
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
|
||||
|
||||
/**
|
||||
* Convert a Web ReadableStream into a Node.js ReadableStream
|
||||
*
|
||||
* This can be used to, for example, write a ReadableStream obtained via
|
||||
* `net.fetch` into a file using the Node.js `fs` APIs
|
||||
*/
|
||||
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
|
||||
const reader = readableStream.getReader();
|
||||
const rs = new Readable();
|
||||
|
||||
rs._read = async () => {
|
||||
try {
|
||||
const result = await reader.read();
|
||||
|
||||
if (!result.done) {
|
||||
rs.push(Buffer.from(result.value));
|
||||
} else {
|
||||
rs.push(null);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
rs.emit("error", e);
|
||||
}
|
||||
};
|
||||
|
||||
return rs;
|
||||
};
|
||||
|
||||
const writeNodeStream = async (
|
||||
filePath: string,
|
||||
fileStream: NodeJS.ReadableStream,
|
||||
) => {
|
||||
const writeable = createWriteStream(filePath);
|
||||
|
||||
fileStream.on("error", (error) => {
|
||||
writeable.destroy(error); // Close the writable stream with an error
|
||||
});
|
||||
|
||||
fileStream.pipe(writeable);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeable.on("finish", resolve);
|
||||
writeable.on("error", async (e: unknown) => {
|
||||
if (existsSync(filePath)) {
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -96,6 +96,8 @@ const skipAppUpdate = (version: string) => {
|
|||
ipcRenderer.send("skipAppUpdate", version);
|
||||
};
|
||||
|
||||
// - FS
|
||||
|
||||
const fsExists = (path: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("fsExists", path);
|
||||
|
||||
|
@ -110,6 +112,12 @@ const fsRmdir = (path: string): Promise<void> =>
|
|||
|
||||
const fsRm = (path: string): Promise<void> => ipcRenderer.invoke("fsRm", path);
|
||||
|
||||
const fsReadTextFile = (path: string): Promise<string> =>
|
||||
ipcRenderer.invoke("fsReadTextFile", path);
|
||||
|
||||
const fsWriteFile = (path: string, contents: string): Promise<void> =>
|
||||
ipcRenderer.invoke("fsWriteFile", path, contents);
|
||||
|
||||
// - AUDIT below this
|
||||
|
||||
// - Conversion
|
||||
|
@ -229,17 +237,6 @@ const updateWatchMappingIgnoredFiles = (
|
|||
|
||||
// - FS Legacy
|
||||
|
||||
const saveStreamToDisk = (
|
||||
path: string,
|
||||
fileStream: ReadableStream,
|
||||
): Promise<void> => ipcRenderer.invoke("saveStreamToDisk", path, fileStream);
|
||||
|
||||
const saveFileToDisk = (path: string, contents: string): Promise<void> =>
|
||||
ipcRenderer.invoke("saveFileToDisk", path, contents);
|
||||
|
||||
const readTextFile = (path: string): Promise<string> =>
|
||||
ipcRenderer.invoke("readTextFile", path);
|
||||
|
||||
const isFolder = (dirPath: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("isFolder", dirPath);
|
||||
|
||||
|
@ -298,7 +295,8 @@ const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
|
|||
// https://www.electronjs.org/docs/latest/api/context-bridge#methods
|
||||
//
|
||||
// The copy itself is relatively fast, but the problem with transfering large
|
||||
// amounts of data is potentially running out of memory during the copy.
|
||||
// amounts of data is potentially running out of memory during the copy. For an
|
||||
// alternative, see [Note: IPC streams].
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
// - General
|
||||
appVersion,
|
||||
|
@ -316,6 +314,17 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
updateOnNextRestart,
|
||||
skipAppUpdate,
|
||||
|
||||
// - FS
|
||||
fs: {
|
||||
exists: fsExists,
|
||||
rename: fsRename,
|
||||
mkdirIfNeeded: fsMkdirIfNeeded,
|
||||
rmdir: fsRmdir,
|
||||
rm: fsRm,
|
||||
readTextFile: fsReadTextFile,
|
||||
writeFile: fsWriteFile,
|
||||
},
|
||||
|
||||
// - Conversion
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
|
@ -341,20 +350,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
updateWatchMappingSyncedFiles,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
|
||||
// - FS
|
||||
fs: {
|
||||
exists: fsExists,
|
||||
rename: fsRename,
|
||||
mkdirIfNeeded: fsMkdirIfNeeded,
|
||||
rmdir: fsRmdir,
|
||||
rm: fsRm,
|
||||
},
|
||||
|
||||
// - FS legacy
|
||||
// TODO: Move these into fs + document + rename if needed
|
||||
saveStreamToDisk,
|
||||
saveFileToDisk,
|
||||
readTextFile,
|
||||
isFolder,
|
||||
|
||||
// - Upload
|
||||
|
|
|
@ -22,6 +22,7 @@ linter:
|
|||
- use_key_in_widget_constructors
|
||||
- cancel_subscriptions
|
||||
|
||||
|
||||
- avoid_empty_else
|
||||
- exhaustive_cases
|
||||
|
||||
|
@ -59,6 +60,7 @@ analyzer:
|
|||
prefer_final_locals: warning
|
||||
unnecessary_const: error
|
||||
cancel_subscriptions: error
|
||||
unrelated_type_equality_checks: error
|
||||
|
||||
|
||||
unawaited_futures: warning # convert to warning after fixing existing issues
|
||||
|
|
|
@ -3,12 +3,9 @@ PODS:
|
|||
- Flutter
|
||||
- battery_info (0.0.1):
|
||||
- Flutter
|
||||
- bonsoir_darwin (3.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
|
@ -171,7 +168,6 @@ PODS:
|
|||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- ReachabilitySwift (5.2.1)
|
||||
- receive_sharing_intent (1.6.8):
|
||||
- Flutter
|
||||
- screen_brightness_ios (0.1.0):
|
||||
|
@ -231,8 +227,7 @@ PODS:
|
|||
DEPENDENCIES:
|
||||
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
|
||||
- battery_info (from `.symlinks/plugins/battery_info/ios`)
|
||||
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
|
@ -296,7 +291,6 @@ SPEC REPOS:
|
|||
- onnxruntime-objc
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- ReachabilitySwift
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- Sentry
|
||||
|
@ -309,10 +303,8 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/background_fetch/ios"
|
||||
battery_info:
|
||||
:path: ".symlinks/plugins/battery_info/ios"
|
||||
bonsoir_darwin:
|
||||
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_saver:
|
||||
|
@ -409,8 +401,7 @@ EXTERNAL SOURCES:
|
|||
SPEC CHECKSUMS:
|
||||
background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a
|
||||
battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c
|
||||
bonsoir_darwin: 127bdc632fdc154ae2f277a4d5c86a6212bc75be
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: 797fd7297b7e1be954432743a0b3f90038e45a71
|
||||
|
@ -458,7 +449,6 @@ SPEC CHECKSUMS:
|
|||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66
|
||||
receive_sharing_intent: 6837b01768e567fe8562182397bf43d63d8c6437
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb
|
||||
|
|
|
@ -285,7 +285,6 @@
|
|||
"${BUILT_PRODUCTS_DIR}/Mantle/Mantle.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
|
||||
|
@ -293,7 +292,6 @@
|
|||
"${BUILT_PRODUCTS_DIR}/Toast/Toast.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/bonsoir_darwin/bonsoir_darwin.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
|
||||
|
@ -369,7 +367,6 @@
|
|||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mantle.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
|
||||
|
@ -377,7 +374,6 @@
|
|||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bonsoir_darwin.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
|
||||
|
|
|
@ -85,13 +85,24 @@ class EnteFile {
|
|||
|
||||
static int parseFileCreationTime(String? fileTitle, AssetEntity asset) {
|
||||
int creationTime = asset.createDateTime.microsecondsSinceEpoch;
|
||||
final int modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
||||
if (creationTime >= jan011981Time) {
|
||||
// assuming that fileSystem is returning correct creationTime.
|
||||
// During upload, this might get overridden with exif Creation time
|
||||
// When the assetModifiedTime is less than creationTime, than just use
|
||||
// that as creationTime. This is to handle cases where file might be
|
||||
// copied to the fileSystem from somewhere else See #https://superuser.com/a/1091147
|
||||
if (modificationTime >= jan011981Time &&
|
||||
modificationTime < creationTime) {
|
||||
_logger.info(
|
||||
'LocalID: ${asset.id} modification time is less than creation time. Using modification time as creation time',
|
||||
);
|
||||
creationTime = modificationTime;
|
||||
}
|
||||
return creationTime;
|
||||
} else {
|
||||
if (asset.modifiedDateTime.microsecondsSinceEpoch >= jan011981Time) {
|
||||
creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
|
||||
if (modificationTime >= jan011981Time) {
|
||||
creationTime = modificationTime;
|
||||
} else {
|
||||
creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
|
||||
}
|
||||
|
@ -106,7 +117,6 @@ class EnteFile {
|
|||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import "dart:io";
|
|||
import "package:connectivity_plus/connectivity_plus.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/errors.dart";
|
||||
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/event.dart";
|
||||
import "package:photos/services/remote_assets_service.dart";
|
||||
|
@ -23,7 +22,7 @@ abstract class MLFramework {
|
|||
MLFramework(this.shouldDownloadOverMobileData) {
|
||||
Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen((ConnectivityResult result) async {
|
||||
.listen((List<ConnectivityResult> result) async {
|
||||
_logger.info("Connectivity changed to $result");
|
||||
if (_state == InitializationState.waitingForNetwork &&
|
||||
await _canDownload()) {
|
||||
|
@ -135,9 +134,11 @@ abstract class MLFramework {
|
|||
}
|
||||
|
||||
Future<bool> _canDownload() async {
|
||||
final connectivityResult = await (Connectivity().checkConnectivity());
|
||||
return connectivityResult != ConnectivityResult.mobile ||
|
||||
shouldDownloadOverMobileData;
|
||||
final List<ConnectivityResult> connections =
|
||||
await (Connectivity().checkConnectivity());
|
||||
final bool isConnectedToMobile =
|
||||
connections.contains(ConnectivityResult.mobile);
|
||||
return !isConnectedToMobile || shouldDownloadOverMobileData;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,9 @@ class SyncService {
|
|||
sync();
|
||||
});
|
||||
|
||||
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
|
||||
Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen((List<ConnectivityResult> result) {
|
||||
_logger.info("Connectivity change detected " + result.toString());
|
||||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
sync();
|
||||
|
|
|
@ -355,9 +355,10 @@ class FileUploader {
|
|||
if (isForceUpload) {
|
||||
return;
|
||||
}
|
||||
final connectivityResult = await (Connectivity().checkConnectivity());
|
||||
final List<ConnectivityResult> connections =
|
||||
await (Connectivity().checkConnectivity());
|
||||
bool canUploadUnderCurrentNetworkConditions = true;
|
||||
if (connectivityResult == ConnectivityResult.mobile) {
|
||||
if (connections.any((element) => element == ConnectivityResult.mobile)) {
|
||||
canUploadUnderCurrentNetworkConditions =
|
||||
Configuration.instance.shouldBackupOverMobileData();
|
||||
}
|
||||
|
|
|
@ -113,38 +113,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
bonsoir:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir
|
||||
sha256: "800d77c0581fff06cc43ef2b7723dfe5ee9b899ab0fdf80fb1c7b8829a5deb5c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0+1"
|
||||
bonsoir_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_android
|
||||
sha256: "7207c36fd7e0f3c7c2d8cf353f02bd640d96e2387d575837f8ac051c9cbf4aa7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0+1"
|
||||
bonsoir_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_darwin
|
||||
sha256: "7211042c85da2d6efa80c0976bbd9568f2b63624097779847548ed4530675ade"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
bonsoir_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bonsoir_platform_interface
|
||||
sha256: "64d57cd52bd477b4891e9b9d419e6408da171ed9e0efc8aa716e7e343d5d93ad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -241,14 +209,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
cast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cast
|
||||
sha256: b70f6be547a53481dffec93ad3cc4974fae5ed707f0b677d4a50c329d7299b98
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -326,18 +286,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
|
||||
sha256: ebe15d94de9dd7c31dc2ac54e42780acdf3384b1497c69290c9f3c5b0279fc57
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
version: "6.0.2"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
|
||||
sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.4"
|
||||
version: "2.0.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1769,14 +1729,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -27,7 +27,6 @@ dependencies:
|
|||
battery_info: ^1.1.1
|
||||
bip39: ^1.0.6
|
||||
cached_network_image: ^3.0.0
|
||||
cast: ^2.0.0
|
||||
chewie:
|
||||
git:
|
||||
url: https://github.com/ente-io/chewie.git
|
||||
|
@ -37,11 +36,7 @@ dependencies:
|
|||
collection: # dart
|
||||
computer:
|
||||
git: "https://github.com/ente-io/computer.git"
|
||||
connectivity_plus:
|
||||
git:
|
||||
url: https://github.com/ente-io/plus_plugins.git
|
||||
ref: check_mobile_first
|
||||
path: packages/connectivity_plus/connectivity_plus/
|
||||
connectivity_plus: ^6.0.2
|
||||
cross_file: ^0.3.3
|
||||
crypto: ^3.0.2
|
||||
cupertino_icons: ^1.0.0
|
||||
|
@ -175,7 +170,6 @@ dependencies:
|
|||
xml: ^6.3.0
|
||||
|
||||
dependency_overrides:
|
||||
connectivity_plus: ^4.0.0
|
||||
# Remove this after removing dependency from flutter_sodium.
|
||||
# Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0
|
||||
ffi: 2.1.0
|
||||
|
|
|
@ -56,8 +56,11 @@ export default function SearchInput(props: Iprops) {
|
|||
const [value, setValue] = useState<SearchOption>(null);
|
||||
const appContext = useContext(AppContext);
|
||||
const handleChange = (value: SearchOption) => {
|
||||
setValue(value);
|
||||
setQuery(value.label);
|
||||
if (value) {
|
||||
setValue(value);
|
||||
setQuery(value.label);
|
||||
}
|
||||
|
||||
blur();
|
||||
};
|
||||
const handleInputChange = (value: string, actionMeta: InputActionMeta) => {
|
||||
|
|
|
@ -7,7 +7,6 @@ import { CustomError } from "@ente/shared/error";
|
|||
import { Events, eventBus } from "@ente/shared/events";
|
||||
import { Remote } from "comlink";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import isElectron from "is-electron";
|
||||
import { EnteFile } from "types/file";
|
||||
import {
|
||||
generateStreamFromArrayBuffer,
|
||||
|
@ -89,11 +88,12 @@ class DownloadManagerImpl {
|
|||
e,
|
||||
);
|
||||
}
|
||||
try {
|
||||
if (isElectron()) this.fileCache = await openCache("files");
|
||||
} catch (e) {
|
||||
log.error("Failed to open file cache, will continue without it", e);
|
||||
}
|
||||
// TODO (MR): Revisit full file caching cf disk space usage
|
||||
// try {
|
||||
// if (isElectron()) this.fileCache = await openCache("files");
|
||||
// } catch (e) {
|
||||
// log.error("Failed to open file cache, will continue without it", e);
|
||||
// }
|
||||
this.cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
this.ready = true;
|
||||
eventBus.on(Events.LOGOUT, this.logoutHandler.bind(this), this);
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
mergeMetadata,
|
||||
} from "utils/file";
|
||||
import { safeDirectoryName, safeFileName } from "utils/native-fs";
|
||||
import { writeStream } from "utils/native-stream";
|
||||
import { getAllLocalCollections } from "../collectionService";
|
||||
import downloadManager from "../download";
|
||||
import { getAllLocalFiles } from "../fileService";
|
||||
|
@ -884,7 +885,7 @@ class ExportService {
|
|||
try {
|
||||
const exportRecord = await this.getExportRecord(folder);
|
||||
const newRecord: ExportRecord = { ...exportRecord, ...newData };
|
||||
await ensureElectron().saveFileToDisk(
|
||||
await ensureElectron().fs.writeFile(
|
||||
`${folder}/${exportRecordFileName}`,
|
||||
JSON.stringify(newRecord, null, 2),
|
||||
);
|
||||
|
@ -907,8 +908,7 @@ class ExportService {
|
|||
if (!(await fs.exists(exportRecordJSONPath))) {
|
||||
return this.createEmptyExportRecord(exportRecordJSONPath);
|
||||
}
|
||||
const recordFile =
|
||||
await electron.readTextFile(exportRecordJSONPath);
|
||||
const recordFile = await fs.readTextFile(exportRecordJSONPath);
|
||||
try {
|
||||
return JSON.parse(recordFile);
|
||||
} catch (e) {
|
||||
|
@ -993,7 +993,7 @@ class ExportService {
|
|||
fileExportName,
|
||||
file,
|
||||
);
|
||||
await electron.saveStreamToDisk(
|
||||
await writeStream(
|
||||
`${collectionExportPath}/${fileExportName}`,
|
||||
updatedFileStream,
|
||||
);
|
||||
|
@ -1044,7 +1044,7 @@ class ExportService {
|
|||
imageExportName,
|
||||
file,
|
||||
);
|
||||
await electron.saveStreamToDisk(
|
||||
await writeStream(
|
||||
`${collectionExportPath}/${imageExportName}`,
|
||||
imageStream,
|
||||
);
|
||||
|
@ -1056,7 +1056,7 @@ class ExportService {
|
|||
file,
|
||||
);
|
||||
try {
|
||||
await electron.saveStreamToDisk(
|
||||
await writeStream(
|
||||
`${collectionExportPath}/${videoExportName}`,
|
||||
videoStream,
|
||||
);
|
||||
|
@ -1077,7 +1077,7 @@ class ExportService {
|
|||
fileExportName: string,
|
||||
file: EnteFile,
|
||||
) {
|
||||
await ensureElectron().saveFileToDisk(
|
||||
await ensureElectron().fs.writeFile(
|
||||
getFileMetadataExportPath(collectionExportPath, fileExportName),
|
||||
getGoogleLikeMetadataFile(fileExportName, file),
|
||||
);
|
||||
|
@ -1106,7 +1106,7 @@ class ExportService {
|
|||
|
||||
private createEmptyExportRecord = async (exportRecordJSONPath: string) => {
|
||||
const exportRecord: ExportRecord = NULL_EXPORT_RECORD;
|
||||
await ensureElectron().saveFileToDisk(
|
||||
await ensureElectron().fs.writeFile(
|
||||
exportRecordJSONPath,
|
||||
JSON.stringify(exportRecord, null, 2),
|
||||
);
|
||||
|
|
|
@ -53,6 +53,7 @@ import { VISIBILITY_STATE } from "types/magicMetadata";
|
|||
import { FileTypeInfo } from "types/upload";
|
||||
import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata";
|
||||
import { safeFileName } from "utils/native-fs";
|
||||
import { writeStream } from "utils/native-stream";
|
||||
|
||||
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
|
||||
|
||||
|
@ -798,55 +799,47 @@ async function downloadFileDesktop(
|
|||
electron: Electron,
|
||||
fileReader: FileReader,
|
||||
file: EnteFile,
|
||||
downloadPath: string,
|
||||
downloadDir: string,
|
||||
) {
|
||||
const fileStream = (await DownloadManager.getFile(
|
||||
const fs = electron.fs;
|
||||
const stream = (await DownloadManager.getFile(
|
||||
file,
|
||||
)) as ReadableStream<Uint8Array>;
|
||||
const updatedFileStream = await getUpdatedEXIFFileForDownload(
|
||||
const updatedStream = await getUpdatedEXIFFileForDownload(
|
||||
fileReader,
|
||||
file,
|
||||
fileStream,
|
||||
stream,
|
||||
);
|
||||
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const fileBlob = await new Response(updatedFileStream).blob();
|
||||
const fileBlob = await new Response(updatedStream).blob();
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const imageExportName = await safeFileName(
|
||||
downloadPath,
|
||||
downloadDir,
|
||||
livePhoto.imageNameTitle,
|
||||
electron.fs.exists,
|
||||
fs.exists,
|
||||
);
|
||||
const imageStream = generateStreamFromArrayBuffer(livePhoto.image);
|
||||
await electron.saveStreamToDisk(
|
||||
`${downloadPath}/${imageExportName}`,
|
||||
imageStream,
|
||||
);
|
||||
await writeStream(`${downloadDir}/${imageExportName}`, imageStream);
|
||||
try {
|
||||
const videoExportName = await safeFileName(
|
||||
downloadPath,
|
||||
downloadDir,
|
||||
livePhoto.videoNameTitle,
|
||||
electron.fs.exists,
|
||||
fs.exists,
|
||||
);
|
||||
const videoStream = generateStreamFromArrayBuffer(livePhoto.video);
|
||||
await electron.saveStreamToDisk(
|
||||
`${downloadPath}/${videoExportName}`,
|
||||
videoStream,
|
||||
);
|
||||
await writeStream(`${downloadDir}/${videoExportName}`, videoStream);
|
||||
} catch (e) {
|
||||
await electron.fs.rm(`${downloadPath}/${imageExportName}`);
|
||||
await fs.rm(`${downloadDir}/${imageExportName}`);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
const fileExportName = await safeFileName(
|
||||
downloadPath,
|
||||
downloadDir,
|
||||
file.metadata.title,
|
||||
electron.fs.exists,
|
||||
);
|
||||
await electron.saveStreamToDisk(
|
||||
`${downloadPath}/${fileExportName}`,
|
||||
updatedFileStream,
|
||||
fs.exists,
|
||||
);
|
||||
await writeStream(`${downloadDir}/${fileExportName}`, updatedStream);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
39
web/apps/photos/src/utils/native-stream.ts
Normal file
39
web/apps/photos/src/utils/native-stream.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @file Streaming IPC communication with the Node.js layer of our desktop app.
|
||||
*
|
||||
* NOTE: These functions only work when we're running in our desktop app.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Write the given stream to a file on the local machine.
|
||||
*
|
||||
* **This only works when we're running in our desktop app**. It uses the
|
||||
* "stream://" protocol handler exposed by our custom code in the Node.js layer.
|
||||
* See: [Note: IPC streams].
|
||||
*
|
||||
* @param path The path on the local machine where to write the file to.
|
||||
* @param stream The stream which should be written into the file.
|
||||
* */
|
||||
export const writeStream = async (path: string, stream: ReadableStream) => {
|
||||
// The duplex parameter needs to be set to 'half' when streaming requests.
|
||||
//
|
||||
// Currently browsers, and specifically in our case, since this code runs
|
||||
// only within our desktop (Electron) app, Chromium, don't support 'full'
|
||||
// duplex mode (i.e. streaming both the request and the response).
|
||||
// https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests
|
||||
//
|
||||
// In another twist, the TypeScript libdom.d.ts does not include the
|
||||
// "duplex" parameter, so we need to cast to get TypeScript to let this code
|
||||
// through. e.g. see https://github.com/node-fetch/node-fetch/issues/1769
|
||||
const req = new Request(`stream://write${path}`, {
|
||||
// GET can't have a body
|
||||
method: "POST",
|
||||
body: stream,
|
||||
duplex: "half",
|
||||
} as unknown as RequestInit);
|
||||
const res = await fetch(req);
|
||||
if (!res.ok)
|
||||
throw new Error(
|
||||
`Failed to write stream to ${path}: HTTP ${res.status}`,
|
||||
);
|
||||
};
|
|
@ -59,11 +59,21 @@ const nextConfig = {
|
|||
GIT_SHA: gitSHA(),
|
||||
},
|
||||
|
||||
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
|
||||
// Customize the webpack configuration used by Next.js
|
||||
webpack: (config, { isServer }) => {
|
||||
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
|
||||
if (!isServer) {
|
||||
config.resolve.fallback.fs = false;
|
||||
}
|
||||
|
||||
// Suppress the warning "Critical dependency: require function is used
|
||||
// in a way in which dependencies cannot be statically extracted" when
|
||||
// import heic-convert.
|
||||
//
|
||||
// Upstream issue, which currently doesn't have a workaround.
|
||||
// https://github.com/catdad-experiments/libheif-js/issues/23
|
||||
config.ignoreWarnings = [{ module: /libheif-js/ }];
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -188,6 +188,17 @@ export interface Electron {
|
|||
* Delete the file at {@link path}.
|
||||
*/
|
||||
rm: (path: string) => Promise<void>;
|
||||
|
||||
/** Read the string contents of a file at {@link path}. */
|
||||
readTextFile: (path: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Write a string to a file, replacing the file if it already exists.
|
||||
*
|
||||
* @param path The path of the file.
|
||||
* @param contents The string contents to write.
|
||||
*/
|
||||
writeFile: (path: string, contents: string) => Promise<void>;
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -300,12 +311,6 @@ export interface Electron {
|
|||
) => Promise<void>;
|
||||
|
||||
// - FS legacy
|
||||
saveStreamToDisk: (
|
||||
path: string,
|
||||
fileStream: ReadableStream,
|
||||
) => Promise<void>;
|
||||
saveFileToDisk: (path: string, contents: string) => Promise<void>;
|
||||
readTextFile: (path: string) => Promise<string>;
|
||||
isFolder: (dirPath: string) => Promise<boolean>;
|
||||
|
||||
// - Upload
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
export async function sleep(time: number) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(null), time);
|
||||
});
|
||||
/**
|
||||
* Wait for {@link ms} milliseconds
|
||||
*
|
||||
* This function is a promisified `setTimeout`. It returns a promise that
|
||||
* resolves after {@link ms} milliseconds.
|
||||
*/
|
||||
export async function sleep(ms: number) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function downloadAsFile(filename: string, content: string) {
|
||||
|
|
Loading…
Add table
Reference in a new issue