Files
daedalOS/components/system/Files/FileEntry/functions.ts

370 lines
10 KiB
TypeScript
Raw Normal View History

2021-07-03 21:46:40 -07:00
import type { FSModule } from "browserfs/dist/node/core/FS";
import { monacoExtensions } from "components/apps/MonacoEditor/config";
2021-11-27 21:52:53 -08:00
import { isYouTubeUrl } from "components/apps/VideoPlayer/useVideoPlayer";
2021-08-28 22:21:00 -07:00
import type { ExtensionType } from "components/system/Files/FileEntry/extensions";
2021-07-03 21:41:57 -07:00
import extensions from "components/system/Files/FileEntry/extensions";
2021-07-03 21:46:40 -07:00
import type { FileInfo } from "components/system/Files/FileEntry/useFileInfo";
import type { FileStat } from "components/system/Files/FileManager/functions";
2021-07-10 22:43:05 -07:00
import processDirectory from "contexts/process/directory";
2021-07-03 21:46:40 -07:00
import ini from "ini";
2021-09-25 21:23:24 -07:00
import { extname, join } from "path";
2021-10-30 22:04:01 -07:00
import index from "public/.index/fs.9p.json";
2021-07-03 23:08:32 -07:00
import {
BASE_2D_CONTEXT_OPTIONS,
2021-07-31 21:34:00 -07:00
EMPTY_BUFFER,
2021-11-20 23:15:26 -08:00
FOLDER_ICON,
2021-07-03 23:08:32 -07:00
IMAGE_FILE_EXTENSIONS,
MP3_MIME_TYPE,
2021-11-20 23:15:26 -08:00
NEW_FOLDER_ICON,
2021-09-25 22:51:56 -07:00
ONE_TIME_PASSIVE_EVENT,
PREVIEW_FRAME_SECOND,
2021-07-03 23:08:32 -07:00
SHORTCUT_EXTENSION,
SHORTCUT_ICON,
2021-07-17 22:46:03 -07:00
SYSTEM_FILES,
2021-09-25 21:44:16 -07:00
SYSTEM_PATHS,
2021-11-20 23:15:26 -08:00
UNKNOWN_ICON,
2021-09-25 22:51:56 -07:00
VIDEO_FILE_EXTENSIONS,
2021-07-03 23:08:32 -07:00
} from "utils/constants";
2021-07-03 21:46:40 -07:00
import { bufferToUrl } from "utils/functions";
2021-08-21 22:31:01 -07:00
type InternetShortcut = {
InternetShortcut: {
BaseURL: string;
2021-11-06 22:26:44 -07:00
Comment: string;
2021-08-21 22:31:01 -07:00
IconFile: string;
2021-10-02 23:02:44 -07:00
Type: string;
2021-08-21 22:31:01 -07:00
URL: string;
};
};
2021-09-25 21:44:16 -07:00
type ShellClassInfo = {
ShellClassInfo: {
IconFile: string;
};
};
2021-10-30 22:04:01 -07:00
type FS9P = [string, number, number, number, number, number, FS9P[] | string];
const IDX_MTIME = 2;
const IDX_TARGET = 6;
2021-11-27 23:57:03 -08:00
const get9pModifiedTime = (path: string): number => {
2021-10-30 22:04:01 -07:00
let fsPath = index.fsroot as FS9P[];
let mTime = 0;
path
.split("/")
.filter(Boolean)
.forEach((pathPart) => {
const pathBranch = fsPath.find(([name]) => name === pathPart);
if (pathBranch) {
const isBranch = Array.isArray(pathBranch[IDX_TARGET]);
if (!isBranch) mTime = pathBranch[IDX_MTIME];
fsPath = isBranch ? (pathBranch[IDX_TARGET] as FS9P[]) : [];
}
});
return mTime;
};
export const getModifiedTime = (path: string, stats: FileStat): number => {
const { atimeMs, ctimeMs, mtimeMs } = stats;
return atimeMs === ctimeMs && ctimeMs === mtimeMs
? get9pModifiedTime(path) || mtimeMs
: mtimeMs;
};
export const getIconFromIni = (
fs: FSModule,
directory: string
): Promise<string> =>
2021-11-20 21:37:26 -08:00
new Promise((resolve) => {
fs.readFile(
join(directory, "desktop.ini"),
(error, contents = EMPTY_BUFFER) => {
if (!error) {
const {
ShellClassInfo: { IconFile = "" },
} = ini.parse(contents.toString()) as ShellClassInfo;
if (IconFile) resolve(IconFile);
}
}
2021-11-20 21:37:26 -08:00
);
});
2021-09-11 23:08:40 -07:00
const getDefaultFileViewer = (extension: string): string => {
if (monacoExtensions.has(extension)) return "MonacoEditor";
if (IMAGE_FILE_EXTENSIONS.has(extension)) return "Photos";
2021-09-25 22:56:09 -07:00
if (VIDEO_FILE_EXTENSIONS.has(extension)) return "VideoPlayer";
2021-09-11 23:08:40 -07:00
return "";
};
2021-09-11 21:33:28 -07:00
export const getIconByFileExtension = (extension: string): string => {
2021-08-28 22:21:00 -07:00
const { icon: extensionIcon = "", process: [defaultProcess = ""] = [] } =
extension in extensions ? extensions[extension as ExtensionType] : {};
2021-07-10 22:43:05 -07:00
2021-09-18 21:42:41 -07:00
if (extensionIcon) return `/System/Icons/${extensionIcon}.png`;
2021-07-10 22:43:05 -07:00
2021-09-04 21:55:55 -07:00
return (
processDirectory[defaultProcess || getDefaultFileViewer(extension)]?.icon ||
UNKNOWN_ICON
2021-09-04 21:55:55 -07:00
);
2021-07-10 22:43:05 -07:00
};
2021-06-26 22:01:45 -07:00
2021-09-11 22:11:58 -07:00
export const getProcessByFileExtension = (extension: string): string => {
2021-08-28 22:21:00 -07:00
const [defaultProcess = ""] =
extension in extensions
? extensions[extension as ExtensionType].process
: [getDefaultFileViewer(extension)];
2021-06-26 22:01:45 -07:00
return defaultProcess;
2021-04-04 00:14:27 -07:00
};
2021-07-03 21:46:40 -07:00
2021-10-02 23:02:44 -07:00
export const getShortcutInfo = (contents: Buffer): FileInfo => {
2021-07-03 21:52:14 -07:00
const {
2021-10-02 23:02:44 -07:00
InternetShortcut: {
BaseURL: pid = "",
2021-11-06 22:26:44 -07:00
Comment: comment = "",
2021-10-02 23:02:44 -07:00
IconFile: icon = "",
Type: type = "",
URL: url = "",
},
2021-08-21 22:31:01 -07:00
} = ini.parse(contents.toString()) as InternetShortcut;
2021-07-03 21:52:14 -07:00
2021-09-25 21:13:00 -07:00
if (!icon && pid) {
2021-11-06 22:26:44 -07:00
return { comment, icon: processDirectory[pid]?.icon, pid, type, url };
2021-09-25 21:13:00 -07:00
}
2021-11-06 22:26:44 -07:00
return { comment, icon, pid, type, url };
2021-07-03 21:52:14 -07:00
};
2021-07-03 21:46:40 -07:00
export const getInfoWithoutExtension = (
2021-09-25 21:44:16 -07:00
fs: FSModule,
2021-07-03 21:46:40 -07:00
path: string,
2021-09-25 21:44:16 -07:00
isDirectory: boolean,
useNewFolderIcon: boolean,
2021-09-25 21:44:16 -07:00
callback: (value: FileInfo) => void
): void => {
if (isDirectory) {
const setFolderInfo = (icon: string, getIcon?: () => void): void =>
callback({ getIcon, icon, pid: "FileExplorer", url: path });
2021-09-25 21:44:16 -07:00
setFolderInfo(useNewFolderIcon ? NEW_FOLDER_ICON : FOLDER_ICON, () =>
getIconFromIni(fs, path).then(setFolderInfo)
);
2021-09-25 21:44:16 -07:00
} else {
callback({ icon: UNKNOWN_ICON, pid: "", url: "" });
2021-09-25 21:44:16 -07:00
}
};
2021-07-03 21:46:40 -07:00
export const getInfoWithExtension = (
fs: FSModule,
path: string,
extension: string,
2021-09-25 21:23:24 -07:00
callback: (value: FileInfo) => void
2021-07-03 21:46:40 -07:00
): void => {
2021-09-25 23:14:40 -07:00
const subIcons: string[] = [];
const getInfoByFileExtension = (icon?: string, getIcon?: () => void): void =>
2021-07-03 21:46:40 -07:00
callback({
getIcon,
2021-07-03 21:46:40 -07:00
icon: icon || getIconByFileExtension(extension),
pid: getProcessByFileExtension(extension),
2021-09-25 23:14:40 -07:00
subIcons,
2021-07-03 21:46:40 -07:00
url: path,
});
if (extension === SHORTCUT_EXTENSION) {
2021-07-31 21:20:31 -07:00
fs.readFile(path, (error, contents = EMPTY_BUFFER) => {
subIcons.push(SHORTCUT_ICON);
2021-09-25 23:14:40 -07:00
2021-07-03 21:46:40 -07:00
if (error) {
getInfoByFileExtension();
} else {
2021-11-06 22:26:44 -07:00
const { comment, icon, pid, url } = getShortcutInfo(contents);
2021-09-25 21:23:24 -07:00
const urlExt = extname(url);
if (pid === "FileExplorer") {
const getIcon = (): void => {
getIconFromIni(fs, url).then((iniIcon) =>
2021-11-20 23:29:58 -08:00
callback({ comment, icon: iniIcon, pid, subIcons, url })
);
};
2021-11-06 22:26:44 -07:00
callback({ comment, getIcon, icon, pid, subIcons, url });
} else if (
2021-09-25 22:56:09 -07:00
IMAGE_FILE_EXTENSIONS.has(urlExt) ||
VIDEO_FILE_EXTENSIONS.has(urlExt) ||
urlExt === ".mp3"
) {
getInfoWithExtension(fs, url, urlExt, (fileInfo) => {
2021-11-27 23:07:37 -08:00
const { icon: urlIcon = icon, getIcon } = fileInfo;
2021-11-27 23:07:37 -08:00
callback({ comment, getIcon, icon: urlIcon, pid, subIcons, url });
2021-09-25 21:23:24 -07:00
});
2021-11-27 21:52:53 -08:00
} else if (isYouTubeUrl(url)) {
callback({
comment,
icon: `https://img.youtube.com/vi${new URL(url).pathname}/1.jpg`,
pid,
subIcons: [processDirectory["VideoPlayer"].icon],
url,
});
} else {
2021-11-06 22:26:44 -07:00
callback({ comment, icon, pid, subIcons, url });
2021-09-25 21:23:24 -07:00
}
2021-07-03 21:46:40 -07:00
}
});
} else if (IMAGE_FILE_EXTENSIONS.has(extension)) {
getInfoByFileExtension("/System/Icons/photo.png", () =>
fs.readFile(path, (error, contents = EMPTY_BUFFER) => {
if (!error) getInfoByFileExtension(bufferToUrl(contents));
})
);
2021-09-25 22:51:56 -07:00
} else if (VIDEO_FILE_EXTENSIONS.has(extension)) {
2021-11-13 22:26:22 -08:00
getInfoByFileExtension(processDirectory["VideoPlayer"].icon, () =>
fs.readFile(path, (error, contents = EMPTY_BUFFER) => {
if (!error) {
const video = document.createElement("video");
video.currentTime = PREVIEW_FRAME_SECOND;
video.addEventListener(
"loadeddata",
() => {
const canvas = document.createElement("canvas");
canvas.height = video.videoHeight;
canvas.width = video.videoWidth;
canvas
.getContext("2d", BASE_2D_CONTEXT_OPTIONS)
?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
canvas.toBlob((blob) => {
if (blob instanceof Blob) {
2021-11-20 22:32:19 -08:00
subIcons.push(processDirectory["VideoPlayer"].icon);
2021-11-13 22:26:22 -08:00
getInfoByFileExtension(URL.createObjectURL(blob));
}
});
},
ONE_TIME_PASSIVE_EVENT
);
video.src = bufferToUrl(contents);
video.load();
}
})
);
2021-07-03 21:46:40 -07:00
} else if (extension === ".mp3") {
2021-10-09 23:24:09 -07:00
getInfoByFileExtension(
`/System/Icons/${extensions[".mp3"].icon as string}.png`,
() =>
fs.readFile(path, (error, contents = EMPTY_BUFFER) => {
if (!error) {
import("music-metadata-browser").then(
({ parseBuffer, selectCover }) =>
parseBuffer(
contents,
{
mimeType: MP3_MIME_TYPE,
size: contents.length,
},
{ skipPostHeaders: true }
).then(({ common: { picture } = {} }) => {
const { data: coverPicture } = selectCover(picture) || {};
if (coverPicture) {
getInfoByFileExtension(bufferToUrl(coverPicture));
}
})
);
}
})
2021-10-09 23:24:09 -07:00
);
2021-07-03 21:46:40 -07:00
} else {
getInfoByFileExtension();
}
};
2021-07-17 22:46:03 -07:00
export const filterSystemFiles =
(directory: string) =>
(file: string): boolean =>
2021-09-25 21:44:16 -07:00
!SYSTEM_PATHS.has(join(directory, file)) && !SYSTEM_FILES.has(file);
2021-10-02 22:41:53 -07:00
type WrapData = {
lines: string[];
width: number;
};
const canvasContexts: Record<string, CanvasRenderingContext2D> = {};
const measureText = (
text: string,
fontSize: string,
fontFamily: string
2021-11-06 21:41:33 -07:00
): number => {
2021-10-02 22:41:53 -07:00
const font = `${fontSize} ${fontFamily}`;
if (!canvasContexts[font]) {
const canvas = document.createElement("canvas");
const context = canvas.getContext(
"2d",
BASE_2D_CONTEXT_OPTIONS
) as CanvasRenderingContext2D;
context.font = font;
canvasContexts[font] = context;
}
2021-11-06 21:41:33 -07:00
const { actualBoundingBoxLeft, actualBoundingBoxRight } =
canvasContexts[font].measureText(text);
2021-10-02 22:41:53 -07:00
2021-11-06 21:41:33 -07:00
return Math.abs(actualBoundingBoxLeft) + Math.abs(actualBoundingBoxRight);
2021-10-02 22:41:53 -07:00
};
export const getTextWrapData = (
text: string,
fontSize: string,
fontFamily: string,
maxWidth?: number
): WrapData => {
const lines = [""];
2021-11-06 21:41:33 -07:00
const totalWidth = measureText(text, fontSize, fontFamily);
2021-10-02 22:41:53 -07:00
if (!maxWidth) return { lines: [text], width: totalWidth };
if (totalWidth > maxWidth) {
2021-11-27 21:12:39 -08:00
const words = text.split(" ");
2021-10-02 22:41:53 -07:00
[...text].forEach((character) => {
2021-11-27 21:12:39 -08:00
const lineIndex = lines.length - 1;
const lineText = `${lines[lineIndex]}${character}`;
2021-11-06 21:41:33 -07:00
const lineWidth = measureText(lineText, fontSize, fontFamily);
2021-10-02 22:41:53 -07:00
if (lineWidth > maxWidth) {
2021-11-27 21:12:39 -08:00
const spacesInLine = lineText.split(" ").length - 1;
const lineWithWords = words.splice(0, spacesInLine).join(" ");
2021-11-27 21:17:44 -08:00
if (
lines.length === 1 &&
spacesInLine > 0 &&
lines[0] !== lineWithWords
) {
2021-11-27 21:12:39 -08:00
lines[0] = lineText.slice(0, lineWithWords.length);
lines.push(lineText.slice(lineWithWords.length));
} else {
lines.push(character);
}
2021-10-02 22:41:53 -07:00
} else {
2021-11-27 21:12:39 -08:00
lines[lineIndex] = lineText;
2021-10-02 22:41:53 -07:00
}
});
}
return {
lines,
width: Math.min(maxWidth, totalWidth),
};
};