[web] Remove dead code from cast (#1256)
This commit is contained in:
commit
d52f873c92
35 changed files with 113 additions and 1613 deletions
95
web/apps/cast/src/components/PhotoAuditorium.tsx
Normal file
95
web/apps/cast/src/components/PhotoAuditorium.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { SlideshowContext } from "pages/slideshow";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
export default function PhotoAuditorium({
|
||||
url,
|
||||
nextSlideUrl,
|
||||
}: {
|
||||
url: string;
|
||||
nextSlideUrl: string;
|
||||
}) {
|
||||
const { showNextSlide } = useContext(SlideshowContext);
|
||||
|
||||
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
|
||||
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
|
||||
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
let timeout2: NodeJS.Timeout;
|
||||
|
||||
if (nextSlidePrerendered) {
|
||||
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
|
||||
const delayTime = Math.max(10000 - elapsedTime, 0);
|
||||
|
||||
if (elapsedTime >= 10000) {
|
||||
setShowPreloadedNextSlide(true);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
setShowPreloadedNextSlide(true);
|
||||
}, delayTime);
|
||||
}
|
||||
|
||||
if (showNextSlide) {
|
||||
timeout2 = setTimeout(() => {
|
||||
showNextSlide();
|
||||
setNextSlidePrerendered(false);
|
||||
setPrerenderTime(null);
|
||||
setShowPreloadedNextSlide(false);
|
||||
}, delayTime);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
if (timeout2) clearTimeout(timeout2);
|
||||
};
|
||||
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
backgroundImage: `url(${url})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundBlendMode: "multiply",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "none" : "block",
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={nextSlideUrl}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
display: showPreloadedNextSlide ? "block" : "none",
|
||||
}}
|
||||
onLoad={() => {
|
||||
setNextSlidePrerendered(true);
|
||||
setPrerenderTime(Date.now());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function TimerBar({ percentage }: { percentage: number }) {
|
||||
const okColor = "#75C157";
|
||||
const warningColor = "#FFC000";
|
||||
const lateColor = "#FF0000";
|
||||
|
||||
const [backgroundColor, setBackgroundColor] = useState(okColor);
|
||||
|
||||
useEffect(() => {
|
||||
if (percentage >= 40) {
|
||||
setBackgroundColor(okColor);
|
||||
} else if (percentage >= 20) {
|
||||
setBackgroundColor(warningColor);
|
||||
} else {
|
||||
setBackgroundColor(lateColor);
|
||||
}
|
||||
}, [percentage]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: `${percentage}%`, // Set the width based on the time left
|
||||
height: "10px", // Same as the border thickness
|
||||
backgroundColor, // The color of the moving border
|
||||
transition: "width 1s linear", // Smooth transition for the width change
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import { getAlbumsURL } from "@ente/shared/network/api";
|
||||
import { runningInBrowser } from "@ente/shared/platform";
|
||||
import { PAGES } from "constants/pages";
|
||||
|
||||
export enum APPS {
|
||||
PHOTOS = "PHOTOS",
|
||||
AUTH = "AUTH",
|
||||
ALBUMS = "ALBUMS",
|
||||
}
|
||||
|
||||
export const ALLOWED_APP_PAGES = new Map([
|
||||
[APPS.ALBUMS, [PAGES.SHARED_ALBUMS, PAGES.ROOT]],
|
||||
[
|
||||
APPS.AUTH,
|
||||
[
|
||||
PAGES.ROOT,
|
||||
PAGES.LOGIN,
|
||||
PAGES.SIGNUP,
|
||||
PAGES.VERIFY,
|
||||
PAGES.CREDENTIALS,
|
||||
PAGES.RECOVER,
|
||||
PAGES.CHANGE_PASSWORD,
|
||||
PAGES.GENERATE,
|
||||
PAGES.AUTH,
|
||||
PAGES.TWO_FACTOR_VERIFY,
|
||||
PAGES.TWO_FACTOR_RECOVER,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
export const CLIENT_PACKAGE_NAMES = new Map([
|
||||
[APPS.ALBUMS, "io.ente.albums.web"],
|
||||
[APPS.PHOTOS, "io.ente.photos.web"],
|
||||
[APPS.AUTH, "io.ente.auth.web"],
|
||||
]);
|
||||
|
||||
export const getAppNameAndTitle = () => {
|
||||
if (!runningInBrowser()) {
|
||||
return {};
|
||||
}
|
||||
const currentURL = new URL(window.location.href);
|
||||
const albumsURL = new URL(getAlbumsURL());
|
||||
if (currentURL.origin === albumsURL.origin) {
|
||||
return { name: APPS.ALBUMS, title: "ente Photos" };
|
||||
} else {
|
||||
return { name: APPS.PHOTOS, title: "ente Photos" };
|
||||
}
|
||||
};
|
||||
|
||||
export const getAppTitle = () => {
|
||||
return getAppNameAndTitle().title;
|
||||
};
|
||||
|
||||
export const getAppName = () => {
|
||||
return getAppNameAndTitle().name;
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
export enum CACHES {
|
||||
THUMBS = "thumbs",
|
||||
FACE_CROPS = "face-crops",
|
||||
FILES = "files",
|
||||
}
|
|
@ -1,14 +1,3 @@
|
|||
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
|
||||
export const MAX_EDITED_CREATION_TIME = new Date();
|
||||
|
||||
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
||||
export const MAX_CAPTION_SIZE = 5000;
|
||||
|
||||
export const TYPE_HEIC = "heic";
|
||||
export const TYPE_HEIF = "heif";
|
||||
export const TYPE_JPEG = "jpeg";
|
||||
export const TYPE_JPG = "jpg";
|
||||
|
||||
export enum FILE_TYPE {
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
|
@ -29,15 +18,3 @@ export const RAW_FORMATS = [
|
|||
"dng",
|
||||
"tif",
|
||||
];
|
||||
export const SUPPORTED_RAW_FORMATS = [
|
||||
"heic",
|
||||
"rw2",
|
||||
"tiff",
|
||||
"arw",
|
||||
"cr3",
|
||||
"cr2",
|
||||
"nef",
|
||||
"psd",
|
||||
"dng",
|
||||
"tif",
|
||||
];
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
export const GAP_BTW_TILES = 4;
|
||||
export const DATE_CONTAINER_HEIGHT = 48;
|
||||
export const SIZE_AND_COUNT_CONTAINER_HEIGHT = 72;
|
||||
export const IMAGE_CONTAINER_MAX_HEIGHT = 180;
|
||||
export const IMAGE_CONTAINER_MAX_WIDTH = 180;
|
||||
export const MIN_COLUMNS = 4;
|
||||
export const SPACE_BTW_DATES = 44;
|
||||
export const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
|
||||
|
||||
export enum PLAN_PERIOD {
|
||||
MONTH = "month",
|
||||
YEAR = "year",
|
||||
}
|
||||
|
||||
export const SYNC_INTERVAL_IN_MICROSECONDS = 1000 * 60 * 5; // 5 minutes
|
|
@ -1,20 +0,0 @@
|
|||
export enum PAGES {
|
||||
CHANGE_EMAIL = "/change-email",
|
||||
CHANGE_PASSWORD = "/change-password",
|
||||
CREDENTIALS = "/credentials",
|
||||
GALLERY = "/gallery",
|
||||
GENERATE = "/generate",
|
||||
LOGIN = "/login",
|
||||
RECOVER = "/recover",
|
||||
SIGNUP = "/signup",
|
||||
TWO_FACTOR_SETUP = "/two-factor/setup",
|
||||
TWO_FACTOR_VERIFY = "/two-factor/verify",
|
||||
TWO_FACTOR_RECOVER = "/two-factor/recover",
|
||||
VERIFY = "/verify",
|
||||
ROOT = "/",
|
||||
SHARED_ALBUMS = "/shared-albums",
|
||||
// ML_DEBUG = '/ml-debug',
|
||||
DEDUPLICATE = "/deduplicate",
|
||||
// AUTH page is used to show (auth)enticator codes
|
||||
AUTH = "/auth",
|
||||
}
|
|
@ -1,11 +1,5 @@
|
|||
import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import {
|
||||
FileTypeInfo,
|
||||
ImportSuggestion,
|
||||
Location,
|
||||
ParsedExtractedMetadata,
|
||||
} from "types/upload";
|
||||
import { FileTypeInfo } from "types/upload";
|
||||
|
||||
// list of format that were missed by type-detection for some files.
|
||||
export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
|
||||
|
@ -45,98 +39,3 @@ export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
|
|||
];
|
||||
|
||||
export const KNOWN_NON_MEDIA_FORMATS = ["xmp", "html", "txt"];
|
||||
|
||||
export const EXIFLESS_FORMATS = ["gif", "bmp"];
|
||||
|
||||
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
|
||||
export const MULTIPART_PART_SIZE = 20 * 1024 * 1024;
|
||||
|
||||
export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE;
|
||||
|
||||
export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor(
|
||||
MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE,
|
||||
);
|
||||
|
||||
export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random();
|
||||
|
||||
export const NULL_LOCATION: Location = { latitude: null, longitude: null };
|
||||
|
||||
export enum UPLOAD_STAGES {
|
||||
START,
|
||||
READING_GOOGLE_METADATA_FILES,
|
||||
EXTRACTING_METADATA,
|
||||
UPLOADING,
|
||||
CANCELLING,
|
||||
FINISH,
|
||||
}
|
||||
|
||||
export enum UPLOAD_STRATEGY {
|
||||
SINGLE_COLLECTION,
|
||||
COLLECTION_PER_FOLDER,
|
||||
}
|
||||
|
||||
export enum UPLOAD_RESULT {
|
||||
FAILED,
|
||||
ALREADY_UPLOADED,
|
||||
UNSUPPORTED,
|
||||
BLOCKED,
|
||||
TOO_LARGE,
|
||||
LARGER_THAN_AVAILABLE_STORAGE,
|
||||
UPLOADED,
|
||||
UPLOADED_WITH_STATIC_THUMBNAIL,
|
||||
ADDED_SYMLINK,
|
||||
}
|
||||
|
||||
export enum PICKED_UPLOAD_TYPE {
|
||||
FILES = "files",
|
||||
FOLDERS = "folders",
|
||||
ZIPS = "zips",
|
||||
}
|
||||
|
||||
export const MAX_FILE_SIZE_SUPPORTED = 4 * 1024 * 1024 * 1024; // 4 GB
|
||||
|
||||
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB
|
||||
|
||||
export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
|
||||
location: NULL_LOCATION,
|
||||
creationTime: null,
|
||||
width: null,
|
||||
height: null,
|
||||
};
|
||||
|
||||
export const A_SEC_IN_MICROSECONDS = 1e6;
|
||||
|
||||
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
|
||||
rootFolderName: "",
|
||||
hasNestedFolders: false,
|
||||
hasRootLevelFileWithFolder: false,
|
||||
};
|
||||
|
||||
export const BLACK_THUMBNAIL_BASE64 =
|
||||
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB" +
|
||||
"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ" +
|
||||
"EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC" +
|
||||
"ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF" +
|
||||
"BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk" +
|
||||
"6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL" +
|
||||
"W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA" +
|
||||
"AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY" +
|
||||
"nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK" +
|
||||
"kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD" +
|
||||
"AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
|
||||
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
|
||||
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC" +
|
||||
"gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
|
||||
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
|
||||
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
|
||||
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
|
||||
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
|
||||
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" +
|
||||
"KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
|
||||
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
|
||||
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
|
||||
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK" +
|
||||
"ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" +
|
||||
"KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
|
||||
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
|
||||
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=";
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
export const ENTE_WEBSITE_LINK = "https://ente.io";
|
||||
|
||||
export const ML_BLOG_LINK = "https://ente.io/blog/desktop-ml-beta";
|
||||
|
||||
export const FACE_SEARCH_PRIVACY_POLICY_LINK =
|
||||
"https://ente.io/privacy#8-biometric-information-privacy-policy";
|
||||
|
||||
export const SUPPORT_EMAIL = "support@ente.io";
|
||||
|
||||
export const APP_DOWNLOAD_URL = "https://ente.io/download/desktop";
|
||||
|
||||
export const FEEDBACK_EMAIL = "feedback@ente.io";
|
||||
|
||||
export const DELETE_ACCOUNT_EMAIL = "account-deletion@ente.io";
|
||||
|
||||
export const WEB_ROADMAP_URL = "https://github.com/ente-io/ente/discussions";
|
||||
|
||||
export const DESKTOP_ROADMAP_URL =
|
||||
"https://github.com/ente-io/ente/discussions";
|
|
@ -1,31 +0,0 @@
|
|||
export enum MS_KEYS {
|
||||
SRP_CONFIGURE_IN_PROGRESS = "srpConfigureInProgress",
|
||||
REDIRECT_URL = "redirectUrl",
|
||||
}
|
||||
|
||||
type StoreType = Map<Partial<MS_KEYS>, any>;
|
||||
|
||||
class InMemoryStore {
|
||||
private store: StoreType = new Map();
|
||||
|
||||
get(key: MS_KEYS) {
|
||||
return this.store.get(key);
|
||||
}
|
||||
|
||||
set(key: MS_KEYS, value: any) {
|
||||
this.store.set(key, value);
|
||||
}
|
||||
|
||||
delete(key: MS_KEYS) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
|
||||
has(key: MS_KEYS) {
|
||||
return this.store.has(key);
|
||||
}
|
||||
clear() {
|
||||
this.store.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export default new InMemoryStore();
|
|
@ -1,25 +0,0 @@
|
|||
import { LimitedCacheStorage } from "types/cache/index";
|
||||
|
||||
class cacheStorageFactory {
|
||||
getCacheStorage(): LimitedCacheStorage {
|
||||
return transformBrowserCacheStorageToLimitedCacheStorage(caches);
|
||||
}
|
||||
}
|
||||
|
||||
export const CacheStorageFactory = new cacheStorageFactory();
|
||||
|
||||
function transformBrowserCacheStorageToLimitedCacheStorage(
|
||||
caches: CacheStorage,
|
||||
): LimitedCacheStorage {
|
||||
return {
|
||||
async open(cacheName) {
|
||||
const cache = await caches.open(cacheName);
|
||||
return {
|
||||
match: cache.match.bind(cache),
|
||||
put: cache.put.bind(cache),
|
||||
delete: cache.delete.bind(cache),
|
||||
};
|
||||
},
|
||||
delete: caches.delete.bind(caches),
|
||||
};
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import { logError } from "@ente/shared/sentry";
|
||||
import { CacheStorageFactory } from "./cacheStorageFactory";
|
||||
|
||||
const SecurityError = "SecurityError";
|
||||
const INSECURE_OPERATION = "The operation is insecure.";
|
||||
async function openCache(cacheName: string) {
|
||||
try {
|
||||
return await CacheStorageFactory.getCacheStorage().open(cacheName);
|
||||
} catch (e) {
|
||||
// ignoring insecure operation error, as it is thrown in incognito mode in firefox
|
||||
if (e.name === SecurityError && e.message === INSECURE_OPERATION) {
|
||||
// no-op
|
||||
} else {
|
||||
// log and ignore, we don't want to break the caller flow, when cache is not available
|
||||
logError(e, "openCache failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
async function deleteCache(cacheName: string) {
|
||||
try {
|
||||
return await CacheStorageFactory.getCacheStorage().delete(cacheName);
|
||||
} catch (e) {
|
||||
// ignoring insecure operation error, as it is thrown in incognito mode in firefox
|
||||
if (e.name === SecurityError && e.message === INSECURE_OPERATION) {
|
||||
// no-op
|
||||
} else {
|
||||
// log and ignore, we don't want to break the caller flow, when cache is not available
|
||||
logError(e, "deleteCache failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CacheStorageService = { open: openCache, delete: deleteCache };
|
|
@ -1,162 +1,14 @@
|
|||
import { EnteFile } from "types/file";
|
||||
import {
|
||||
createTypedObjectURL,
|
||||
generateStreamFromArrayBuffer,
|
||||
getRenderableFileURL,
|
||||
} from "utils/file";
|
||||
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getCastFileURL, getCastThumbnailURL } from "@ente/shared/network/api";
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import { CACHES } from "constants/cache";
|
||||
import { getCastFileURL } from "@ente/shared/network/api";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import { LimitedCache } from "types/cache";
|
||||
import { EnteFile } from "types/file";
|
||||
import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker";
|
||||
import { CacheStorageService } from "./cache/cacheStorageService";
|
||||
import { generateStreamFromArrayBuffer } from "utils/file";
|
||||
|
||||
class CastDownloadManager {
|
||||
private fileObjectURLPromise = new Map<
|
||||
string,
|
||||
Promise<{ original: string[]; converted: string[] }>
|
||||
>();
|
||||
private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
|
||||
|
||||
private fileDownloadProgress = new Map<number, number>();
|
||||
|
||||
private progressUpdater: (value: Map<number, number>) => void;
|
||||
|
||||
setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) {
|
||||
this.progressUpdater = progressUpdater;
|
||||
}
|
||||
|
||||
private async getThumbnailCache() {
|
||||
try {
|
||||
const thumbnailCache = await CacheStorageService.open(
|
||||
CACHES.THUMBS,
|
||||
);
|
||||
return thumbnailCache;
|
||||
} catch (e) {
|
||||
return null;
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public async getCachedThumbnail(
|
||||
file: EnteFile,
|
||||
thumbnailCache?: LimitedCache,
|
||||
) {
|
||||
try {
|
||||
if (!thumbnailCache) {
|
||||
thumbnailCache = await this.getThumbnailCache();
|
||||
}
|
||||
const cacheResp: Response = await thumbnailCache?.match(
|
||||
file.id.toString(),
|
||||
);
|
||||
|
||||
if (cacheResp) {
|
||||
return URL.createObjectURL(await cacheResp.blob());
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
logError(e, "failed to get cached thumbnail");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async getThumbnail(file: EnteFile, castToken: string) {
|
||||
try {
|
||||
if (!this.thumbnailObjectURLPromise.has(file.id)) {
|
||||
const downloadPromise = async () => {
|
||||
const thumbnailCache = await this.getThumbnailCache();
|
||||
const cachedThumb = await this.getCachedThumbnail(
|
||||
file,
|
||||
thumbnailCache,
|
||||
);
|
||||
if (cachedThumb) {
|
||||
return cachedThumb;
|
||||
}
|
||||
|
||||
const thumb = await this.downloadThumb(castToken, file);
|
||||
const thumbBlob = new Blob([thumb]);
|
||||
try {
|
||||
await thumbnailCache?.put(
|
||||
file.id.toString(),
|
||||
new Response(thumbBlob),
|
||||
);
|
||||
} catch (e) {
|
||||
// TODO: handle storage full exception.
|
||||
}
|
||||
return URL.createObjectURL(thumbBlob);
|
||||
};
|
||||
this.thumbnailObjectURLPromise.set(file.id, downloadPromise());
|
||||
}
|
||||
|
||||
return await this.thumbnailObjectURLPromise.get(file.id);
|
||||
} catch (e) {
|
||||
this.thumbnailObjectURLPromise.delete(file.id);
|
||||
logError(e, "get castDownloadManager preview Failed");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private downloadThumb = async (castToken: string, file: EnteFile) => {
|
||||
const resp = await HTTPService.get(
|
||||
getCastThumbnailURL(file.id),
|
||||
null,
|
||||
{
|
||||
"X-Cast-Access-Token": castToken,
|
||||
},
|
||||
{ responseType: "arraybuffer" },
|
||||
);
|
||||
if (typeof resp.data === "undefined") {
|
||||
throw Error(CustomError.REQUEST_FAILED);
|
||||
}
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const decrypted = await cryptoWorker.decryptThumbnail(
|
||||
new Uint8Array(resp.data),
|
||||
await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
|
||||
file.key,
|
||||
);
|
||||
return decrypted;
|
||||
};
|
||||
|
||||
getFile = async (file: EnteFile, castToken: string, forPreview = false) => {
|
||||
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
|
||||
try {
|
||||
const getFilePromise = async () => {
|
||||
const fileStream = await this.downloadFile(castToken, file);
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
if (forPreview) {
|
||||
return await getRenderableFileURL(file, fileBlob);
|
||||
} else {
|
||||
const fileURL = await createTypedObjectURL(
|
||||
fileBlob,
|
||||
file.metadata.title,
|
||||
);
|
||||
return { converted: [fileURL], original: [fileURL] };
|
||||
}
|
||||
};
|
||||
|
||||
if (!this.fileObjectURLPromise.get(fileKey)) {
|
||||
this.fileObjectURLPromise.set(fileKey, getFilePromise());
|
||||
}
|
||||
const fileURLs = await this.fileObjectURLPromise.get(fileKey);
|
||||
return fileURLs;
|
||||
} catch (e) {
|
||||
this.fileObjectURLPromise.delete(fileKey);
|
||||
logError(e, "castDownloadManager failed to get file");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
public async getCachedOriginalFile(file: EnteFile) {
|
||||
return await this.fileObjectURLPromise.get(file.id.toString());
|
||||
}
|
||||
|
||||
async downloadFile(castToken: string, file: EnteFile) {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const onDownloadProgress = this.trackDownloadProgress(file.id);
|
||||
|
||||
if (
|
||||
file.metadata.fileType === FILE_TYPE.IMAGE ||
|
||||
|
@ -187,9 +39,6 @@ class CastDownloadManager {
|
|||
});
|
||||
const reader = resp.body.getReader();
|
||||
|
||||
const contentLength = +resp.headers.get("Content-Length");
|
||||
let downloadedBytes = 0;
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const decryptionHeader = await cryptoWorker.fromB64(
|
||||
|
@ -208,11 +57,6 @@ class CastDownloadManager {
|
|||
reader.read().then(async ({ done, value }) => {
|
||||
// Is there more data to read?
|
||||
if (!done) {
|
||||
downloadedBytes += value.byteLength;
|
||||
onDownloadProgress({
|
||||
loaded: downloadedBytes,
|
||||
total: contentLength,
|
||||
});
|
||||
const buffer = new Uint8Array(
|
||||
data.byteLength + value.byteLength,
|
||||
);
|
||||
|
@ -254,20 +98,6 @@ class CastDownloadManager {
|
|||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
trackDownloadProgress = (fileID: number) => {
|
||||
return (event: { loaded: number; total: number }) => {
|
||||
if (event.loaded === event.total) {
|
||||
this.fileDownloadProgress.delete(fileID);
|
||||
} else {
|
||||
this.fileDownloadProgress.set(
|
||||
fileID,
|
||||
Math.round((event.loaded * 100) / event.total),
|
||||
);
|
||||
}
|
||||
this.progressUpdater(new Map(this.fileDownloadProgress));
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default new CastDownloadManager();
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { EventEmitter } from "eventemitter3";
|
||||
|
||||
// When registering event handlers,
|
||||
// handle errors to avoid unhandled rejection or propagation to emit call
|
||||
|
||||
export enum Events {
|
||||
LOGOUT = "logout",
|
||||
FILE_UPLOADED = "fileUploaded",
|
||||
LOCAL_FILES_UPDATED = "localFilesUpdated",
|
||||
}
|
||||
|
||||
export const eventBus = new EventEmitter<Events>();
|
|
@ -1,26 +1,19 @@
|
|||
// import isElectron from 'is-electron';
|
||||
// import { ElectronFFmpeg } from 'services/electron/ffmpeg';
|
||||
import { ElectronFile } from "types/upload";
|
||||
import ComlinkFFmpegWorker from "utils/comlink/ComlinkFFmpegWorker";
|
||||
|
||||
export interface IFFmpeg {
|
||||
run: (
|
||||
cmd: string[],
|
||||
inputFile: File | ElectronFile,
|
||||
inputFile: File,
|
||||
outputFilename: string,
|
||||
dontTimeout?: boolean,
|
||||
) => Promise<File | ElectronFile>;
|
||||
) => Promise<File>;
|
||||
}
|
||||
|
||||
class FFmpegFactory {
|
||||
private client: IFFmpeg;
|
||||
async getFFmpegClient() {
|
||||
if (!this.client) {
|
||||
// if (isElectron()) {
|
||||
// this.client = new ElectronFFmpeg();
|
||||
// } else {
|
||||
this.client = await ComlinkFFmpegWorker.getInstance();
|
||||
// }
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
|
|
|
@ -4,10 +4,9 @@ import {
|
|||
INPUT_PATH_PLACEHOLDER,
|
||||
OUTPUT_PATH_PLACEHOLDER,
|
||||
} from "constants/ffmpeg";
|
||||
import { ElectronFile } from "types/upload";
|
||||
import ffmpegFactory from "./ffmpegFactory";
|
||||
|
||||
export async function convertToMP4(file: File | ElectronFile) {
|
||||
export async function convertToMP4(file: File) {
|
||||
try {
|
||||
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
|
||||
return await ffmpegClient.run(
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import { logError } from "@ente/shared/sentry";
|
||||
import WasmHEICConverterService from "./wasmHeicConverter/wasmHEICConverterService";
|
||||
|
||||
class HeicConversionService {
|
||||
async convert(heicFileData: Blob): Promise<Blob> {
|
||||
try {
|
||||
return await WasmHEICConverterService.convert(heicFileData);
|
||||
} catch (e) {
|
||||
logError(e, "failed to convert heic file");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new HeicConversionService();
|
|
@ -30,16 +30,3 @@ export const decodeLivePhoto = async (file: EnteFile, zipBlob: Blob) => {
|
|||
}
|
||||
return livePhoto;
|
||||
};
|
||||
|
||||
export const encodeLivePhoto = async (livePhoto: LivePhoto) => {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"image" + getFileExtensionWithDot(livePhoto.imageNameTitle),
|
||||
livePhoto.image,
|
||||
);
|
||||
zip.file(
|
||||
"video" + getFileExtensionWithDot(livePhoto.videoNameTitle),
|
||||
livePhoto.video,
|
||||
);
|
||||
return await zip.generateAsync({ type: "uint8array" });
|
||||
};
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { logError } from "@ente/shared/sentry";
|
||||
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
|
||||
import { ElectronFile } from "types/upload";
|
||||
|
||||
export async function getUint8ArrayView(
|
||||
file: Blob | ElectronFile,
|
||||
): Promise<Uint8Array> {
|
||||
export async function getUint8ArrayView(file: Blob): Promise<Uint8Array> {
|
||||
try {
|
||||
return new Uint8Array(await file.arrayBuffer());
|
||||
} catch (e) {
|
||||
|
@ -14,45 +11,3 @@ export async function getUint8ArrayView(
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFileStream(file: File, chunkSize: number) {
|
||||
const fileChunkReader = fileChunkReaderMaker(file, chunkSize);
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async pull(controller: ReadableStreamDefaultController) {
|
||||
const chunk = await fileChunkReader.next();
|
||||
if (chunk.done) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.enqueue(chunk.value);
|
||||
}
|
||||
},
|
||||
});
|
||||
const chunkCount = Math.ceil(file.size / chunkSize);
|
||||
return {
|
||||
stream,
|
||||
chunkCount,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getElectronFileStream(
|
||||
file: ElectronFile,
|
||||
chunkSize: number,
|
||||
) {
|
||||
const chunkCount = Math.ceil(file.size / chunkSize);
|
||||
return {
|
||||
stream: await file.stream(),
|
||||
chunkCount,
|
||||
};
|
||||
}
|
||||
|
||||
async function* fileChunkReaderMaker(file: File, chunkSize: number) {
|
||||
let offset = 0;
|
||||
while (offset < file.size) {
|
||||
const blob = file.slice(offset, chunkSize + offset);
|
||||
const fileChunk = await getUint8ArrayView(blob);
|
||||
yield fileChunk;
|
||||
offset += chunkSize;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -6,37 +6,25 @@ import {
|
|||
KNOWN_NON_MEDIA_FORMATS,
|
||||
WHITELISTED_FILE_FORMATS,
|
||||
} from "constants/upload";
|
||||
import FileType, { FileTypeResult } from "file-type";
|
||||
import { ElectronFile, FileTypeInfo } from "types/upload";
|
||||
import FileType from "file-type";
|
||||
import { FileTypeInfo } from "types/upload";
|
||||
import { getFileExtension } from "utils/file";
|
||||
import { getUint8ArrayView } from "./readerService";
|
||||
|
||||
function getFileSize(file: File | ElectronFile) {
|
||||
return file.size;
|
||||
}
|
||||
|
||||
const TYPE_VIDEO = "video";
|
||||
const TYPE_IMAGE = "image";
|
||||
const CHUNK_SIZE_FOR_TYPE_DETECTION = 4100;
|
||||
|
||||
export async function getFileType(
|
||||
receivedFile: File | ElectronFile,
|
||||
): Promise<FileTypeInfo> {
|
||||
export async function getFileType(receivedFile: File): Promise<FileTypeInfo> {
|
||||
try {
|
||||
let fileType: FILE_TYPE;
|
||||
let typeResult: FileTypeResult;
|
||||
|
||||
if (receivedFile instanceof File) {
|
||||
typeResult = await extractFileType(receivedFile);
|
||||
} else {
|
||||
typeResult = await extractElectronFileType(receivedFile);
|
||||
}
|
||||
|
||||
const typeResult = await extractFileType(receivedFile);
|
||||
const mimTypeParts: string[] = typeResult.mime?.split("/");
|
||||
|
||||
if (mimTypeParts?.length !== 2) {
|
||||
throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime));
|
||||
}
|
||||
|
||||
switch (mimTypeParts[0]) {
|
||||
case TYPE_IMAGE:
|
||||
fileType = FILE_TYPE.IMAGE;
|
||||
|
@ -54,7 +42,7 @@ export async function getFileType(
|
|||
};
|
||||
} catch (e) {
|
||||
const fileFormat = getFileExtension(receivedFile.name);
|
||||
const fileSize = convertBytesToHumanReadable(getFileSize(receivedFile));
|
||||
const fileSize = convertBytesToHumanReadable(receivedFile.size);
|
||||
const whiteListedFormat = WHITELISTED_FILE_FORMATS.find(
|
||||
(a) => a.exactType === fileFormat,
|
||||
);
|
||||
|
@ -85,14 +73,6 @@ async function extractFileType(file: File) {
|
|||
return getFileTypeFromBuffer(fileDataChunk);
|
||||
}
|
||||
|
||||
async function extractElectronFileType(file: ElectronFile) {
|
||||
const stream = await file.stream();
|
||||
const reader = stream.getReader();
|
||||
const { value: fileDataChunk } = await reader.read();
|
||||
await reader.cancel();
|
||||
return getFileTypeFromBuffer(fileDataChunk);
|
||||
}
|
||||
|
||||
async function getFileTypeFromBuffer(buffer: Uint8Array) {
|
||||
const result = await FileType.fromBuffer(buffer);
|
||||
if (!result?.mime) {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import * as HeicConvert from "heic-convert";
|
||||
import { getUint8ArrayView } from "services/readerService";
|
||||
|
||||
export async function convertHEIC(
|
||||
fileBlob: Blob,
|
||||
format: string,
|
||||
): Promise<Blob> {
|
||||
const filedata = await getUint8ArrayView(fileBlob);
|
||||
const result = await HeicConvert({ buffer: filedata, format });
|
||||
const convertedFileData = new Uint8Array(result);
|
||||
const convertedFileBlob = new Blob([convertedFileData]);
|
||||
return convertedFileBlob;
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
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 { getDedicatedConvertWorker } from "utils/comlink/ComlinkConvertWorker";
|
||||
import { ComlinkWorker } from "utils/comlink/comlinkWorker";
|
||||
import { DedicatedConvertWorker } from "worker/convert.worker";
|
||||
|
||||
const WORKER_POOL_SIZE = 2;
|
||||
const MAX_CONVERSION_IN_PARALLEL = 1;
|
||||
const WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS = [100, 100];
|
||||
const WAIT_TIME_IN_MICROSECONDS = 30 * 1000;
|
||||
const BREATH_TIME_IN_MICROSECONDS = 1000;
|
||||
const CONVERT_FORMAT = "JPEG";
|
||||
|
||||
class HEICConverter {
|
||||
private convertProcessor = new QueueProcessor<Blob>(
|
||||
MAX_CONVERSION_IN_PARALLEL,
|
||||
);
|
||||
private workerPool: ComlinkWorker<typeof DedicatedConvertWorker>[] = [];
|
||||
private ready: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
private async init() {
|
||||
this.workerPool = [];
|
||||
for (let i = 0; i < WORKER_POOL_SIZE; i++) {
|
||||
this.workerPool.push(getDedicatedConvertWorker());
|
||||
}
|
||||
}
|
||||
async convert(fileBlob: Blob): Promise<Blob> {
|
||||
await this.ready;
|
||||
const response = this.convertProcessor.queueUpRequest(() =>
|
||||
retryAsyncFunction<Blob>(async () => {
|
||||
const convertWorker = this.workerPool.shift();
|
||||
const worker = await convertWorker.remote;
|
||||
try {
|
||||
const convertedHEIC = await new Promise<Blob>(
|
||||
(resolve, reject) => {
|
||||
const main = async () => {
|
||||
try {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(Error("wait time exceeded"));
|
||||
}, WAIT_TIME_IN_MICROSECONDS);
|
||||
const startTime = Date.now();
|
||||
const convertedHEIC =
|
||||
await worker.convertHEIC(
|
||||
fileBlob,
|
||||
CONVERT_FORMAT,
|
||||
);
|
||||
addLogLine(
|
||||
`originalFileSize:${convertBytesToHumanReadable(
|
||||
fileBlob?.size,
|
||||
)},convertedFileSize:${convertBytesToHumanReadable(
|
||||
convertedHEIC?.size,
|
||||
)}, heic conversion time: ${
|
||||
Date.now() - startTime
|
||||
}ms `,
|
||||
);
|
||||
clearTimeout(timeout);
|
||||
resolve(convertedHEIC);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
main();
|
||||
},
|
||||
);
|
||||
if (!convertedHEIC || convertedHEIC?.size === 0) {
|
||||
logError(
|
||||
Error(`converted heic fileSize is Zero`),
|
||||
"converted heic fileSize is Zero",
|
||||
{
|
||||
originalFileSize: convertBytesToHumanReadable(
|
||||
fileBlob?.size ?? 0,
|
||||
),
|
||||
convertedFileSize: convertBytesToHumanReadable(
|
||||
convertedHEIC?.size ?? 0,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(
|
||||
() => resolve(null),
|
||||
BREATH_TIME_IN_MICROSECONDS,
|
||||
);
|
||||
});
|
||||
this.workerPool.push(convertWorker);
|
||||
return convertedHEIC;
|
||||
} catch (e) {
|
||||
logError(e, "heic conversion failed");
|
||||
convertWorker.terminate();
|
||||
this.workerPool.push(getDedicatedConvertWorker());
|
||||
throw e;
|
||||
}
|
||||
}, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS),
|
||||
);
|
||||
try {
|
||||
return await response.promise;
|
||||
} catch (e) {
|
||||
if (e.message === CustomError.REQUEST_CANCELLED) {
|
||||
// ignore
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new HEICConverter();
|
10
web/apps/cast/src/types/cache/index.ts
vendored
10
web/apps/cast/src/types/cache/index.ts
vendored
|
@ -1,10 +0,0 @@
|
|||
export interface LimitedCacheStorage {
|
||||
open: (cacheName: string) => Promise<LimitedCache>;
|
||||
delete: (cacheName: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface LimitedCache {
|
||||
match: (key: string) => Promise<Response>;
|
||||
put: (key: string, data: Response) => Promise<void>;
|
||||
delete: (key: string) => Promise<boolean>;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export interface CastPayload {
|
||||
collectionID: number;
|
||||
collectionKey: string;
|
||||
castToken: string;
|
||||
}
|
|
@ -3,7 +3,6 @@ import {
|
|||
LocalFileAttributes,
|
||||
} from "@ente/shared/crypto/types";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import { Collection } from "types/collection";
|
||||
import {
|
||||
FilePublicMagicMetadata,
|
||||
FilePublicMagicMetadataProps,
|
||||
|
@ -39,24 +38,6 @@ export interface Metadata {
|
|||
deviceFolder?: string;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface ParsedMetadataJSON {
|
||||
creationTime: number;
|
||||
modificationTime: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface MultipartUploadURLs {
|
||||
objectKey: string;
|
||||
partURLs: string[];
|
||||
completeURL: string;
|
||||
}
|
||||
|
||||
export interface FileTypeInfo {
|
||||
fileType: FILE_TYPE;
|
||||
exactType: string;
|
||||
|
@ -65,43 +46,6 @@ export interface FileTypeInfo {
|
|||
videoType?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* ElectronFile is a custom interface that is used to represent
|
||||
* any file on disk as a File-like object in the Electron desktop app.
|
||||
*
|
||||
* This was added to support the auto-resuming of failed uploads
|
||||
* which needed absolute paths to the files which the
|
||||
* normal File interface does not provide.
|
||||
*/
|
||||
export interface ElectronFile {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
lastModified: number;
|
||||
stream: () => Promise<ReadableStream<Uint8Array>>;
|
||||
blob: () => Promise<Blob>;
|
||||
arrayBuffer: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface UploadAsset {
|
||||
isLivePhoto?: boolean;
|
||||
file?: File | ElectronFile;
|
||||
livePhotoAssets?: LivePhotoAssets;
|
||||
isElectron?: boolean;
|
||||
}
|
||||
export interface LivePhotoAssets {
|
||||
image: globalThis.File | ElectronFile;
|
||||
video: globalThis.File | ElectronFile;
|
||||
}
|
||||
|
||||
export interface FileWithCollection extends UploadAsset {
|
||||
localID: number;
|
||||
collection?: Collection;
|
||||
collectionID?: number;
|
||||
}
|
||||
|
||||
export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>;
|
||||
|
||||
export interface UploadURL {
|
||||
url: string;
|
||||
objectKey: string;
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { UPLOAD_RESULT, UPLOAD_STAGES } from "constants/upload";
|
||||
|
||||
export type FileID = number;
|
||||
export type FileName = string;
|
||||
|
||||
export type PercentageUploaded = number;
|
||||
export type UploadFileNames = Map<FileID, FileName>;
|
||||
|
||||
export interface UploadCounter {
|
||||
finished: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface InProgressUpload {
|
||||
localFileID: FileID;
|
||||
progress: PercentageUploaded;
|
||||
}
|
||||
|
||||
export interface FinishedUpload {
|
||||
localFileID: FileID;
|
||||
result: UPLOAD_RESULT;
|
||||
}
|
||||
|
||||
export type InProgressUploads = Map<FileID, PercentageUploaded>;
|
||||
|
||||
export type FinishedUploads = Map<FileID, UPLOAD_RESULT>;
|
||||
|
||||
export type SegregatedFinishedUploads = Map<UPLOAD_RESULT, FileID[]>;
|
||||
|
||||
export interface ProgressUpdater {
|
||||
setPercentComplete: React.Dispatch<React.SetStateAction<number>>;
|
||||
setUploadCounter: React.Dispatch<React.SetStateAction<UploadCounter>>;
|
||||
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
|
||||
setInProgressUploads: React.Dispatch<
|
||||
React.SetStateAction<InProgressUpload[]>
|
||||
>;
|
||||
setFinishedUploads: React.Dispatch<
|
||||
React.SetStateAction<SegregatedFinishedUploads>
|
||||
>;
|
||||
setUploadFilenames: React.Dispatch<React.SetStateAction<UploadFileNames>>;
|
||||
setHasLivePhotos: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setUploadProgressView: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
import { User } from "@ente/shared/user/types";
|
||||
import {
|
||||
CollectionSummaryType,
|
||||
CollectionType,
|
||||
HIDE_FROM_COLLECTION_BAR_TYPES,
|
||||
OPTIONS_NOT_HAVING_COLLECTION_TYPES,
|
||||
} from "constants/collection";
|
||||
import { COLLECTION_ROLE, Collection } from "types/collection";
|
||||
import { SUB_TYPE, VISIBILITY_STATE } from "types/magicMetadata";
|
||||
|
||||
export enum COLLECTION_OPS_TYPE {
|
||||
ADD,
|
||||
MOVE,
|
||||
REMOVE,
|
||||
RESTORE,
|
||||
UNHIDE,
|
||||
}
|
||||
|
||||
export function getSelectedCollection(
|
||||
collectionID: number,
|
||||
collections: Collection[],
|
||||
) {
|
||||
return collections.find((collection) => collection.id === collectionID);
|
||||
}
|
||||
|
||||
export const shouldShowOptions = (type: CollectionSummaryType) => {
|
||||
return !OPTIONS_NOT_HAVING_COLLECTION_TYPES.has(type);
|
||||
};
|
||||
export const showEmptyTrashQuickOption = (type: CollectionSummaryType) => {
|
||||
return type === CollectionSummaryType.trash;
|
||||
};
|
||||
export const showDownloadQuickOption = (type: CollectionSummaryType) => {
|
||||
return (
|
||||
type === CollectionSummaryType.folder ||
|
||||
type === CollectionSummaryType.favorites ||
|
||||
type === CollectionSummaryType.album ||
|
||||
type === CollectionSummaryType.uncategorized ||
|
||||
type === CollectionSummaryType.hiddenItems ||
|
||||
type === CollectionSummaryType.incomingShareViewer ||
|
||||
type === CollectionSummaryType.incomingShareCollaborator ||
|
||||
type === CollectionSummaryType.outgoingShare ||
|
||||
type === CollectionSummaryType.sharedOnlyViaLink ||
|
||||
type === CollectionSummaryType.archived ||
|
||||
type === CollectionSummaryType.pinned
|
||||
);
|
||||
};
|
||||
export const showShareQuickOption = (type: CollectionSummaryType) => {
|
||||
return (
|
||||
type === CollectionSummaryType.folder ||
|
||||
type === CollectionSummaryType.album ||
|
||||
type === CollectionSummaryType.outgoingShare ||
|
||||
type === CollectionSummaryType.sharedOnlyViaLink ||
|
||||
type === CollectionSummaryType.archived ||
|
||||
type === CollectionSummaryType.incomingShareViewer ||
|
||||
type === CollectionSummaryType.incomingShareCollaborator ||
|
||||
type === CollectionSummaryType.pinned
|
||||
);
|
||||
};
|
||||
export const shouldBeShownOnCollectionBar = (type: CollectionSummaryType) => {
|
||||
return !HIDE_FROM_COLLECTION_BAR_TYPES.has(type);
|
||||
};
|
||||
|
||||
export const getUserOwnedCollections = (collections: Collection[]) => {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
if (!user?.id) {
|
||||
throw Error("user missing");
|
||||
}
|
||||
return collections.filter((collection) => collection.owner.id === user.id);
|
||||
};
|
||||
|
||||
export const isDefaultHiddenCollection = (collection: Collection) =>
|
||||
collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN;
|
||||
|
||||
export const isHiddenCollection = (collection: Collection) =>
|
||||
collection.magicMetadata?.data.visibility === VISIBILITY_STATE.HIDDEN;
|
||||
|
||||
export const isQuickLinkCollection = (collection: Collection) =>
|
||||
collection.magicMetadata?.data.subType === SUB_TYPE.QUICK_LINK_COLLECTION;
|
||||
|
||||
export function isOutgoingShare(collection: Collection, user: User): boolean {
|
||||
return collection.owner.id === user.id && collection.sharees?.length > 0;
|
||||
}
|
||||
|
||||
export function isIncomingShare(collection: Collection, user: User) {
|
||||
return collection.owner.id !== user.id;
|
||||
}
|
||||
|
||||
export function isIncomingViewerShare(collection: Collection, user: User) {
|
||||
const sharee = collection.sharees?.find((sharee) => sharee.id === user.id);
|
||||
return sharee?.role === COLLECTION_ROLE.VIEWER;
|
||||
}
|
||||
|
||||
export function isIncomingCollabShare(collection: Collection, user: User) {
|
||||
const sharee = collection.sharees?.find((sharee) => sharee.id === user.id);
|
||||
return sharee?.role === COLLECTION_ROLE.COLLABORATOR;
|
||||
}
|
||||
|
||||
export function isSharedOnlyViaLink(collection: Collection) {
|
||||
return collection.publicURLs?.length && !collection.sharees?.length;
|
||||
}
|
||||
|
||||
export function isValidMoveTarget(
|
||||
sourceCollectionID: number,
|
||||
targetCollection: Collection,
|
||||
user: User,
|
||||
) {
|
||||
return (
|
||||
sourceCollectionID !== targetCollection.id &&
|
||||
!isHiddenCollection(targetCollection) &&
|
||||
!isQuickLinkCollection(targetCollection) &&
|
||||
!isIncomingShare(targetCollection, user)
|
||||
);
|
||||
}
|
||||
|
||||
export function isValidReplacementAlbum(
|
||||
collection: Collection,
|
||||
user: User,
|
||||
wantedCollectionName: string,
|
||||
) {
|
||||
return (
|
||||
collection.name === wantedCollectionName &&
|
||||
(collection.type === CollectionType.album ||
|
||||
collection.type === CollectionType.folder) &&
|
||||
!isHiddenCollection(collection) &&
|
||||
!isQuickLinkCollection(collection) &&
|
||||
!isIncomingShare(collection, user)
|
||||
);
|
||||
}
|
||||
|
||||
export function getCollectionNameMap(
|
||||
collections: Collection[],
|
||||
): Map<number, string> {
|
||||
return new Map<number, string>(
|
||||
collections.map((collection) => [collection.id, collection.name]),
|
||||
);
|
||||
}
|
||||
|
||||
export function getNonHiddenCollections(
|
||||
collections: Collection[],
|
||||
): Collection[] {
|
||||
return collections.filter((collection) => !isHiddenCollection(collection));
|
||||
}
|
||||
|
||||
export function getHiddenCollections(collections: Collection[]): Collection[] {
|
||||
return collections.filter((collection) => isHiddenCollection(collection));
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import { runningInBrowser } from "@ente/shared/platform";
|
||||
import { Remote } from "comlink";
|
||||
import { DedicatedConvertWorker } from "worker/convert.worker";
|
||||
import { ComlinkWorker } from "./comlinkWorker";
|
||||
|
||||
class ComlinkConvertWorker {
|
||||
private comlinkWorkerInstance: Remote<DedicatedConvertWorker>;
|
||||
|
||||
async getInstance() {
|
||||
if (!this.comlinkWorkerInstance) {
|
||||
this.comlinkWorkerInstance =
|
||||
await getDedicatedConvertWorker().remote;
|
||||
}
|
||||
return this.comlinkWorkerInstance;
|
||||
}
|
||||
}
|
||||
|
||||
export const getDedicatedConvertWorker = () => {
|
||||
if (runningInBrowser()) {
|
||||
const cryptoComlinkWorker = new ComlinkWorker<
|
||||
typeof DedicatedConvertWorker
|
||||
>(
|
||||
"ente-convert-worker",
|
||||
new Worker(new URL("worker/convert.worker.ts", import.meta.url)),
|
||||
);
|
||||
return cryptoComlinkWorker;
|
||||
}
|
||||
};
|
||||
|
||||
export default new ComlinkConvertWorker();
|
|
@ -1,6 +1,5 @@
|
|||
import { addLocalLog } from "@ente/shared/logging";
|
||||
import { Remote, wrap } from "comlink";
|
||||
// import { WorkerElectronCacheStorageClient } from 'services/workerElectronCache/client';
|
||||
|
||||
export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
||||
public remote: Promise<Remote<InstanceType<T>>>;
|
||||
|
@ -17,7 +16,6 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
|
|||
addLocalLog(() => `Initiated ${this.name}`);
|
||||
const comlink = wrap<T>(this.worker);
|
||||
this.remote = new comlink() as Promise<Remote<InstanceType<T>>>;
|
||||
// expose(WorkerElectronCacheStorageClient, this.worker);
|
||||
}
|
||||
|
||||
public terminate() {
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
export const readAsDataURL = (blob) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => resolve(fileReader.result as string);
|
||||
fileReader.onerror = () => reject(fileReader.error);
|
||||
fileReader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
export const readAsText = (blob) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => resolve(fileReader.result as string);
|
||||
fileReader.onerror = () => reject(fileReader.error);
|
||||
fileReader.readAsText(blob);
|
||||
});
|
|
@ -1,14 +1,6 @@
|
|||
import { logError } from "@ente/shared/sentry";
|
||||
import {
|
||||
FILE_TYPE,
|
||||
RAW_FORMATS,
|
||||
SUPPORTED_RAW_FORMATS,
|
||||
TYPE_HEIC,
|
||||
TYPE_HEIF,
|
||||
} from "constants/file";
|
||||
import { FILE_TYPE, RAW_FORMATS } from "constants/file";
|
||||
import CastDownloadManager from "services/castDownloadManager";
|
||||
import * as ffmpegService from "services/ffmpeg/ffmpegService";
|
||||
import heicConversionService from "services/heicConversionService";
|
||||
import { decodeLivePhoto } from "services/livePhotoService";
|
||||
import { getFileType } from "services/typeDetectionService";
|
||||
import {
|
||||
|
@ -17,20 +9,8 @@ import {
|
|||
FileMagicMetadata,
|
||||
FilePublicMagicMetadata,
|
||||
} from "types/file";
|
||||
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";
|
||||
|
||||
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
|
||||
|
||||
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
|
||||
|
@ -46,20 +26,6 @@ export function sortFiles(files: EnteFile[], sortAsc = false) {
|
|||
});
|
||||
}
|
||||
|
||||
export function sortTrashFiles(files: EnteFile[]) {
|
||||
return files.sort((a, b) => {
|
||||
if (a.deleteBy === b.deleteBy) {
|
||||
if (a.metadata.creationTime === b.metadata.creationTime) {
|
||||
return (
|
||||
b.metadata.modificationTime - a.metadata.modificationTime
|
||||
);
|
||||
}
|
||||
return b.metadata.creationTime - a.metadata.creationTime;
|
||||
}
|
||||
return a.deleteBy - b.deleteBy;
|
||||
});
|
||||
}
|
||||
|
||||
export async function decryptFile(
|
||||
file: EncryptedEnteFile,
|
||||
collectionKey: string,
|
||||
|
@ -154,176 +120,6 @@ export function generateStreamFromArrayBuffer(data: Uint8Array) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getRenderableFileURL(file: EnteFile, fileBlob: Blob) {
|
||||
switch (file.metadata.fileType) {
|
||||
case FILE_TYPE.IMAGE: {
|
||||
const convertedBlob = await getRenderableImage(
|
||||
file.metadata.title,
|
||||
fileBlob,
|
||||
);
|
||||
const { originalURL, convertedURL } = getFileObjectURLs(
|
||||
fileBlob,
|
||||
convertedBlob,
|
||||
);
|
||||
return {
|
||||
converted: [convertedURL],
|
||||
original: [originalURL],
|
||||
};
|
||||
}
|
||||
case FILE_TYPE.LIVE_PHOTO: {
|
||||
return await getRenderableLivePhotoURL(file, fileBlob);
|
||||
}
|
||||
case FILE_TYPE.VIDEO: {
|
||||
const convertedBlob = await getPlayableVideo(
|
||||
file.metadata.title,
|
||||
fileBlob,
|
||||
);
|
||||
const { originalURL, convertedURL } = getFileObjectURLs(
|
||||
fileBlob,
|
||||
convertedBlob,
|
||||
);
|
||||
return {
|
||||
converted: [convertedURL],
|
||||
original: [originalURL],
|
||||
};
|
||||
}
|
||||
default: {
|
||||
const previewURL = await createTypedObjectURL(
|
||||
fileBlob,
|
||||
file.metadata.title,
|
||||
);
|
||||
return {
|
||||
converted: [previewURL],
|
||||
original: [previewURL],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getRenderableLivePhotoURL(
|
||||
file: EnteFile,
|
||||
fileBlob: Blob,
|
||||
): Promise<{ original: string[]; converted: string[] }> {
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const imageBlob = new Blob([livePhoto.image]);
|
||||
const videoBlob = new Blob([livePhoto.video]);
|
||||
const convertedImageBlob = await getRenderableImage(
|
||||
livePhoto.imageNameTitle,
|
||||
imageBlob,
|
||||
);
|
||||
const convertedVideoBlob = await getPlayableVideo(
|
||||
livePhoto.videoNameTitle,
|
||||
videoBlob,
|
||||
true,
|
||||
);
|
||||
const { originalURL: originalImageURL, convertedURL: convertedImageURL } =
|
||||
getFileObjectURLs(imageBlob, convertedImageBlob);
|
||||
|
||||
const { originalURL: originalVideoURL, convertedURL: convertedVideoURL } =
|
||||
getFileObjectURLs(videoBlob, convertedVideoBlob);
|
||||
return {
|
||||
converted: [convertedImageURL, convertedVideoURL],
|
||||
original: [originalImageURL, originalVideoURL],
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPlayableVideo(
|
||||
videoNameTitle: string,
|
||||
videoBlob: Blob,
|
||||
forceConvert = false,
|
||||
) {
|
||||
try {
|
||||
const isPlayable = await isPlaybackPossible(
|
||||
URL.createObjectURL(videoBlob),
|
||||
);
|
||||
if (isPlayable && !forceConvert) {
|
||||
return videoBlob;
|
||||
} else {
|
||||
if (!forceConvert && !isElectron()) {
|
||||
return null;
|
||||
}
|
||||
addLogLine(
|
||||
"video format not supported, converting it name:",
|
||||
videoNameTitle,
|
||||
);
|
||||
const mp4ConvertedVideo = await ffmpegService.convertToMP4(
|
||||
new File([videoBlob], videoNameTitle),
|
||||
);
|
||||
addLogLine("video successfully converted", videoNameTitle);
|
||||
return new Blob([await mp4ConvertedVideo.arrayBuffer()]);
|
||||
}
|
||||
} catch (e) {
|
||||
addLogLine("video conversion failed", videoNameTitle);
|
||||
logError(e, "video conversion failed");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRenderableImage(fileName: string, imageBlob: Blob) {
|
||||
let fileTypeInfo: FileTypeInfo;
|
||||
try {
|
||||
const tempFile = new File([imageBlob], fileName);
|
||||
fileTypeInfo = await getFileType(tempFile);
|
||||
addLocalLog(() => `file type info: ${JSON.stringify(fileTypeInfo)}`);
|
||||
const { exactType } = fileTypeInfo;
|
||||
let convertedImageBlob: Blob;
|
||||
if (isRawFile(exactType)) {
|
||||
try {
|
||||
if (!isSupportedRawFormat(exactType)) {
|
||||
throw Error(CustomError.UNSUPPORTED_RAW_FORMAT);
|
||||
}
|
||||
|
||||
if (!isElectron()) {
|
||||
throw Error(CustomError.NOT_AVAILABLE_ON_WEB);
|
||||
}
|
||||
addLogLine(
|
||||
`RawConverter called for ${fileName}-${convertBytesToHumanReadable(
|
||||
imageBlob.size,
|
||||
)}`,
|
||||
);
|
||||
// convertedImageBlob = await imageProcessor.convertToJPEG(
|
||||
// imageBlob,
|
||||
// fileName
|
||||
// );
|
||||
addLogLine(`${fileName} successfully converted`);
|
||||
} catch (e) {
|
||||
try {
|
||||
if (!isFileHEIC(exactType)) {
|
||||
throw e;
|
||||
}
|
||||
addLogLine(
|
||||
`HEICConverter called for ${fileName}-${convertBytesToHumanReadable(
|
||||
imageBlob.size,
|
||||
)}`,
|
||||
);
|
||||
convertedImageBlob =
|
||||
await heicConversionService.convert(imageBlob);
|
||||
addLogLine(`${fileName} successfully converted`);
|
||||
} catch (e) {
|
||||
throw Error(CustomError.NON_PREVIEWABLE_FILE);
|
||||
}
|
||||
}
|
||||
return convertedImageBlob;
|
||||
} else {
|
||||
return imageBlob;
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, "get Renderable Image failed", { fileTypeInfo });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isFileHEIC(exactType: string) {
|
||||
return (
|
||||
exactType.toLowerCase().endsWith(TYPE_HEIC) ||
|
||||
exactType.toLowerCase().endsWith(TYPE_HEIF)
|
||||
);
|
||||
}
|
||||
|
||||
export function isRawFile(exactType: string) {
|
||||
return RAW_FORMATS.includes(exactType.toLowerCase());
|
||||
}
|
||||
|
||||
export function isRawFileFromFileName(fileName: string) {
|
||||
for (const rawFormat of RAW_FORMATS) {
|
||||
if (fileName.toLowerCase().endsWith(rawFormat)) {
|
||||
|
@ -333,10 +129,6 @@ export function isRawFileFromFileName(fileName: string) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function isSupportedRawFormat(exactType: string) {
|
||||
return SUPPORTED_RAW_FORMATS.includes(exactType.toLowerCase());
|
||||
}
|
||||
|
||||
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
|
||||
return files.map((file) => {
|
||||
if (file.pubMagicMetadata?.data.editedTime) {
|
||||
|
@ -350,187 +142,24 @@ export function mergeMetadata(files: EnteFile[]): EnteFile[] {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getFileFromURL(fileURL: string) {
|
||||
const fileBlob = await (await fetch(fileURL)).blob();
|
||||
const fileFile = new File([fileBlob], "temp");
|
||||
return fileFile;
|
||||
}
|
||||
|
||||
export function getUniqueFiles(files: EnteFile[]) {
|
||||
const idSet = new Set<number>();
|
||||
const uniqueFiles = files.filter((file) => {
|
||||
if (!idSet.has(file.id)) {
|
||||
idSet.add(file.id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return uniqueFiles;
|
||||
}
|
||||
|
||||
export const isImageOrVideo = (fileType: FILE_TYPE) =>
|
||||
[FILE_TYPE.IMAGE, FILE_TYPE.VIDEO].includes(fileType);
|
||||
|
||||
export const getArchivedFiles = (files: EnteFile[]) => {
|
||||
return files.filter(isArchivedFile).map((file) => file.id);
|
||||
};
|
||||
|
||||
export const createTypedObjectURL = async (blob: Blob, fileName: string) => {
|
||||
const type = await getFileType(new File([blob], fileName));
|
||||
return URL.createObjectURL(new Blob([blob], { type: type.mimeType }));
|
||||
};
|
||||
|
||||
export const getUserOwnedFiles = (files: EnteFile[]) => {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
if (!user?.id) {
|
||||
throw Error("user missing");
|
||||
}
|
||||
return files.filter((file) => file.ownerID === user.id);
|
||||
};
|
||||
|
||||
// doesn't work on firefox
|
||||
export const copyFileToClipboard = async (fileUrl: string) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvasCTX = canvas.getContext("2d");
|
||||
const image = new Image();
|
||||
|
||||
const blobPromise = new Promise<Blob>((resolve, reject) => {
|
||||
let timeout: NodeJS.Timeout = null;
|
||||
try {
|
||||
image.setAttribute("src", fileUrl);
|
||||
image.onload = () => {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
canvasCTX.drawImage(image, 0, 0, image.width, image.height);
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
resolve(blob);
|
||||
},
|
||||
"image/png",
|
||||
1,
|
||||
);
|
||||
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
} catch (e) {
|
||||
void logError(e, "failed to copy to clipboard");
|
||||
reject(e);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(
|
||||
() => reject(Error(CustomError.WAIT_TIME_EXCEEDED)),
|
||||
WAIT_TIME_IMAGE_CONVERSION,
|
||||
);
|
||||
});
|
||||
|
||||
const { ClipboardItem } = window;
|
||||
|
||||
await navigator.clipboard
|
||||
.write([new ClipboardItem({ "image/png": blobPromise })])
|
||||
.catch((e) => logError(e, "failed to copy to clipboard"));
|
||||
};
|
||||
|
||||
export function getLatestVersionFiles(files: EnteFile[]) {
|
||||
const latestVersionFiles = new Map<string, EnteFile>();
|
||||
files.forEach((file) => {
|
||||
const uid = `${file.collectionID}-${file.id}`;
|
||||
if (
|
||||
!latestVersionFiles.has(uid) ||
|
||||
latestVersionFiles.get(uid).updationTime < file.updationTime
|
||||
) {
|
||||
latestVersionFiles.set(uid, file);
|
||||
}
|
||||
});
|
||||
return Array.from(latestVersionFiles.values()).filter(
|
||||
(file) => !file.isDeleted,
|
||||
);
|
||||
}
|
||||
|
||||
export function getPersonalFiles(files: EnteFile[], user: User) {
|
||||
if (!user?.id) {
|
||||
throw Error("user missing");
|
||||
}
|
||||
return files.filter((file) => file.ownerID === user.id);
|
||||
}
|
||||
|
||||
export function getIDBasedSortedFiles(files: EnteFile[]) {
|
||||
return files.sort((a, b) => a.id - b.id);
|
||||
}
|
||||
|
||||
export function constructFileToCollectionMap(files: EnteFile[]) {
|
||||
const fileToCollectionsMap = new Map<number, number[]>();
|
||||
(files ?? []).forEach((file) => {
|
||||
if (!fileToCollectionsMap.get(file.id)) {
|
||||
fileToCollectionsMap.set(file.id, []);
|
||||
}
|
||||
fileToCollectionsMap.get(file.id).push(file.collectionID);
|
||||
});
|
||||
return fileToCollectionsMap;
|
||||
}
|
||||
|
||||
export const shouldShowAvatar = (file: EnteFile, user: User) => {
|
||||
if (!file || !user) {
|
||||
return false;
|
||||
}
|
||||
// is Shared file
|
||||
else if (file.ownerID !== user.id) {
|
||||
return true;
|
||||
}
|
||||
// is public collected file
|
||||
else if (
|
||||
file.ownerID === user.id &&
|
||||
file.pubMagicMetadata?.data?.uploaderName
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPreviewableImage = async (
|
||||
file: EnteFile,
|
||||
castToken: string,
|
||||
): Promise<Blob> => {
|
||||
try {
|
||||
let fileBlob: Blob;
|
||||
const fileURL =
|
||||
await CastDownloadManager.getCachedOriginalFile(file)[0];
|
||||
if (!fileURL) {
|
||||
fileBlob = await new Response(
|
||||
await CastDownloadManager.downloadFile(castToken, file),
|
||||
).blob();
|
||||
} else {
|
||||
fileBlob = await (await fetch(fileURL)).blob();
|
||||
}
|
||||
let fileBlob = await new Response(
|
||||
await CastDownloadManager.downloadFile(castToken, file),
|
||||
).blob();
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
fileBlob = new Blob([livePhoto.image]);
|
||||
}
|
||||
const convertedBlob = await getRenderableImage(
|
||||
file.metadata.title,
|
||||
fileBlob,
|
||||
);
|
||||
fileBlob = convertedBlob;
|
||||
const fileType = await getFileType(
|
||||
new File([fileBlob], file.metadata.title),
|
||||
);
|
||||
|
||||
fileBlob = new Blob([fileBlob], { type: fileType.mimeType });
|
||||
return fileBlob;
|
||||
} catch (e) {
|
||||
logError(e, "failed to download file");
|
||||
}
|
||||
};
|
||||
|
||||
const getFileObjectURLs = (originalBlob: Blob, convertedBlob: Blob) => {
|
||||
const originalURL = URL.createObjectURL(originalBlob);
|
||||
const convertedURL = convertedBlob
|
||||
? convertedBlob === originalBlob
|
||||
? originalURL
|
||||
: URL.createObjectURL(convertedBlob)
|
||||
: null;
|
||||
return { originalURL, convertedURL };
|
||||
};
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import { FILE_TYPE } from "constants/file";
|
||||
import { getFileExtension } from "utils/file";
|
||||
|
||||
const IMAGE_EXTENSIONS = [
|
||||
"heic",
|
||||
"heif",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"png",
|
||||
"gif",
|
||||
"bmp",
|
||||
"tiff",
|
||||
"webp",
|
||||
];
|
||||
|
||||
const VIDEO_EXTENSIONS = [
|
||||
"mov",
|
||||
"mp4",
|
||||
"m4v",
|
||||
"avi",
|
||||
"wmv",
|
||||
"flv",
|
||||
"mkv",
|
||||
"webm",
|
||||
"3gp",
|
||||
"3g2",
|
||||
"avi",
|
||||
"ogv",
|
||||
"mpg",
|
||||
"mp",
|
||||
];
|
||||
|
||||
export function getFileTypeFromExtensionForLivePhotoClustering(
|
||||
filename: string,
|
||||
) {
|
||||
const extension = getFileExtension(filename)?.toLowerCase();
|
||||
if (IMAGE_EXTENSIONS.includes(extension)) {
|
||||
return FILE_TYPE.IMAGE;
|
||||
} else if (VIDEO_EXTENSIONS.includes(extension)) {
|
||||
return FILE_TYPE.VIDEO;
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
import { Collection } from "types/collection";
|
||||
import { EnteFile } from "types/file";
|
||||
import { MagicMetadataCore, VISIBILITY_STATE } from "types/magicMetadata";
|
||||
import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker";
|
||||
|
||||
export function isArchivedFile(item: EnteFile): boolean {
|
||||
if (!item || !item.magicMetadata || !item.magicMetadata.data) {
|
||||
return false;
|
||||
}
|
||||
return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED;
|
||||
}
|
||||
|
||||
export function isArchivedCollection(item: Collection): boolean {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.magicMetadata && item.magicMetadata.data) {
|
||||
return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED;
|
||||
}
|
||||
|
||||
if (item.sharedMagicMetadata && item.sharedMagicMetadata.data) {
|
||||
return (
|
||||
item.sharedMagicMetadata.data.visibility ===
|
||||
VISIBILITY_STATE.ARCHIVED
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isPinnedCollection(item: Collection) {
|
||||
if (
|
||||
!item ||
|
||||
!item.magicMetadata ||
|
||||
!item.magicMetadata.data ||
|
||||
typeof item.magicMetadata.data === "string" ||
|
||||
typeof item.magicMetadata.data.order === "undefined"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return item.magicMetadata.data.order !== 0;
|
||||
}
|
||||
|
||||
export async function updateMagicMetadata<T>(
|
||||
magicMetadataUpdates: T,
|
||||
originalMagicMetadata?: MagicMetadataCore<T>,
|
||||
decryptionKey?: string,
|
||||
): Promise<MagicMetadataCore<T>> {
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
|
||||
if (!originalMagicMetadata) {
|
||||
originalMagicMetadata = getNewMagicMetadata<T>();
|
||||
}
|
||||
|
||||
if (typeof originalMagicMetadata?.data === "string") {
|
||||
originalMagicMetadata.data = await cryptoWorker.decryptMetadata(
|
||||
originalMagicMetadata.data,
|
||||
originalMagicMetadata.header,
|
||||
decryptionKey,
|
||||
);
|
||||
}
|
||||
// copies the existing magic metadata properties of the files and updates the visibility value
|
||||
// The expected behavior while updating magic metadata is to let the existing property as it is and update/add the property you want
|
||||
const magicMetadataProps: T = {
|
||||
...originalMagicMetadata.data,
|
||||
...magicMetadataUpdates,
|
||||
};
|
||||
|
||||
const nonEmptyMagicMetadataProps =
|
||||
getNonEmptyMagicMetadataProps(magicMetadataProps);
|
||||
|
||||
const magicMetadata = {
|
||||
...originalMagicMetadata,
|
||||
data: nonEmptyMagicMetadataProps,
|
||||
count: Object.keys(nonEmptyMagicMetadataProps).length,
|
||||
};
|
||||
|
||||
return magicMetadata;
|
||||
}
|
||||
|
||||
export const getNewMagicMetadata = <T>(): MagicMetadataCore<T> => {
|
||||
return {
|
||||
version: 1,
|
||||
data: null,
|
||||
header: null,
|
||||
count: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const getNonEmptyMagicMetadataProps = <T>(magicMetadataProps: T): T => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(magicMetadataProps).filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
([_, v]) => v !== null && v !== undefined,
|
||||
),
|
||||
) as T;
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
import * as Comlink from "comlink";
|
||||
import { convertHEIC } from "services/wasmHeicConverter/wasmHEICConverterClient";
|
||||
|
||||
export class DedicatedConvertWorker {
|
||||
async convertHEIC(fileBlob: Blob, format: string) {
|
||||
return convertHEIC(fileBlob, format);
|
||||
}
|
||||
}
|
||||
|
||||
Comlink.expose(DedicatedConvertWorker, self);
|
Loading…
Add table
Reference in a new issue