Added media viewer
This commit is contained in:
parent
a4054f134a
commit
fcce477d15
13 changed files with 174 additions and 26 deletions
25
package-lock.json
generated
25
package-lock.json
generated
|
|
@ -25,6 +25,7 @@
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"gh-pages": "^5.0.0"
|
"gh-pages": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -638,9 +639,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
"version": "7.21.11",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz",
|
||||||
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
|
"integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-annotate-as-pure": "^7.18.6",
|
||||||
|
"@babel/helper-create-class-features-plugin": "^7.21.0",
|
||||||
|
"@babel/helper-plugin-utils": "^7.20.2",
|
||||||
|
"@babel/plugin-syntax-private-property-in-object": "^7.14.5"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1898,6 +1906,17 @@
|
||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||||
|
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
||||||
|
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/preset-env/node_modules/semver": {
|
"node_modules/@babel/preset-env/node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"gh-pages": "^5.0.0"
|
"gh-pages": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,16 @@ import { useVirtualRoot } from "../../../hooks/virtual-drive/VirtualRootContext.
|
||||||
import styles from "./FileExplorer.module.css";
|
import styles from "./FileExplorer.module.css";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faArrowUp, faCaretLeft, faCaretRight, faCog, faDesktop, faFile, faFileLines, faFolder, faHouse, faImage, faSearch } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowUp, faCaretLeft, faCaretRight, faCog, faDesktop, faFile, faFileLines, faFolder, faHouse, faImage, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { VirtualFile } from "../../../features/virtual-drive/virtual-file.js";
|
||||||
|
import { useWindowsManager } from "../../../hooks/windows/WindowsManagerContext.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {VirtualFile} props.file
|
||||||
|
*/
|
||||||
function FilePreview({ file }) {
|
function FilePreview({ file }) {
|
||||||
let preview;
|
let preview = null;
|
||||||
|
|
||||||
console.log(file);
|
|
||||||
|
|
||||||
switch (file.extension) {
|
switch (file.extension) {
|
||||||
case "png":
|
case "png":
|
||||||
|
|
@ -25,6 +30,7 @@ export function FileExplorer() {
|
||||||
const virtualRoot = useVirtualRoot();
|
const virtualRoot = useVirtualRoot();
|
||||||
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate("~"));
|
const [currentDirectory, setCurrentDirectory] = useState(virtualRoot.navigate("~"));
|
||||||
const [path, setPath] = useState(currentDirectory.path);
|
const [path, setPath] = useState(currentDirectory.path);
|
||||||
|
const windowsManager = useWindowsManager();
|
||||||
|
|
||||||
const changeDirectory = (path, absolute = false) => {
|
const changeDirectory = (path, absolute = false) => {
|
||||||
const directory = absolute ? virtualRoot.navigate(path) : currentDirectory.navigate(path);
|
const directory = absolute ? virtualRoot.navigate(path) : currentDirectory.navigate(path);
|
||||||
|
|
@ -96,13 +102,18 @@ export function FileExplorer() {
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.Main}>
|
<div className={styles.Main}>
|
||||||
{currentDirectory.files.map((file, index) =>
|
{currentDirectory.files.map((file, index) =>
|
||||||
<button key={index} className={styles["File-button"]}>
|
<button key={index} className={styles["File-button"]} onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
windowsManager.openFile(file);
|
||||||
|
}}>
|
||||||
<FilePreview file={file}/>
|
<FilePreview file={file}/>
|
||||||
<p>{file.id}</p>
|
<p>{file.id}</p>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{currentDirectory.subFolders.map(({ name }, index) =>
|
{currentDirectory.subFolders.map(({ name }, index) =>
|
||||||
<button key={index} className={styles["Folder-button"]} onClick={() => { changeDirectory(name) }}>
|
<button key={index} className={styles["Folder-button"]} onClick={() => {
|
||||||
|
changeDirectory(name);
|
||||||
|
}}>
|
||||||
<FontAwesomeIcon icon={faFolder}/>
|
<FontAwesomeIcon icon={faFolder}/>
|
||||||
<p>{name}</p>
|
<p>{name}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
26
src/components/applications/media-viewer/MediaViewer.jsx
Normal file
26
src/components/applications/media-viewer/MediaViewer.jsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { VirtualFile } from "../../../features/virtual-drive/virtual-file.js"
|
||||||
|
import styles from "./MediaViewer.module.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {VirtualFile} props.file
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function MediaViewer({ file }) {
|
||||||
|
if (file == null)
|
||||||
|
return (<p>No file to render.</p>);
|
||||||
|
|
||||||
|
if (!["png"].includes(file.extension))
|
||||||
|
return (<p>Invalid file format.</p>);
|
||||||
|
|
||||||
|
if (file.source == null)
|
||||||
|
return (<p>File failed to load.</p>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.Container}>
|
||||||
|
<img src={file.source} alt={file.id}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
.Container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Container img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,24 @@ import { Command } from "../../../features/applications/terminal/commands.js";
|
||||||
const USERNAME = "user";
|
const USERNAME = "user";
|
||||||
const HOSTNAME = "prozilla-os";
|
const HOSTNAME = "prozilla-os";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {String} props.text
|
||||||
|
*/
|
||||||
function OutputLine({ text }) {
|
function OutputLine({ text }) {
|
||||||
return (
|
return (
|
||||||
<p className={styles.Output}>{text}</p>
|
<p className={styles.Output}>{text}</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {String} props.value
|
||||||
|
* @param {String} props.prefix
|
||||||
|
* @param {Function} props.onChange
|
||||||
|
* @param {Function} props.onKeyUp
|
||||||
|
* @param {Function} props.onKeyDown
|
||||||
|
*/
|
||||||
function InputLine({ value, prefix, onChange, onKeyUp, onKeyDown }) {
|
function InputLine({ value, prefix, onChange, onKeyUp, onKeyDown }) {
|
||||||
return (
|
return (
|
||||||
<span className={styles.Input}>
|
<span className={styles.Input}>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,13 @@ import ApplicationsManager from "../../features/applications/applications.js";
|
||||||
import { useWindows } from "../../hooks/windows/WindowsContext.js";
|
import { useWindows } from "../../hooks/windows/WindowsContext.js";
|
||||||
import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js";
|
import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js";
|
||||||
import { ReactSVG } from "react-svg";
|
import { ReactSVG } from "react-svg";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import Application from "../../features/applications/application.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Application} props.app
|
||||||
|
*/
|
||||||
function AppButton({ app }) {
|
function AppButton({ app }) {
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
const windows = useWindows();
|
const windows = useWindows();
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,21 @@ import { ReactSVG } from "react-svg";
|
||||||
import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js";
|
import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js";
|
||||||
import Draggable from "react-draggable";
|
import Draggable from "react-draggable";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import Application from "../../features/applications/application.js";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import Vector2 from "../../features/math/vector2.js";
|
||||||
|
|
||||||
export function Window({ id, app, size, position, focused = false, onInteract }) {
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {String} props.id
|
||||||
|
* @param {Application} props.app
|
||||||
|
* @param {Vector2} props.size
|
||||||
|
* @param {Vector2} props.position
|
||||||
|
* @param {boolean} props.focused
|
||||||
|
* @param {Function} props.onInteract
|
||||||
|
*/
|
||||||
|
export function Window({ id, app, size, position, focused = false, onInteract, options }) {
|
||||||
const windowsManager = useWindowsManager();
|
const windowsManager = useWindowsManager();
|
||||||
const nodeRef = useRef(null);
|
const nodeRef = useRef(null);
|
||||||
const [maximized, setMaximized] = useState(false);
|
const [maximized, setMaximized] = useState(false);
|
||||||
|
|
@ -72,7 +85,7 @@ export function Window({ id, app, size, position, focused = false, onInteract })
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["Window-content"]}>
|
<div className={styles["Window-content"]}>
|
||||||
{app.windowContent}
|
<app.WindowContent {...options}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function WindowsView() {
|
||||||
return (<div>
|
return (<div>
|
||||||
{windows.sort((windowA, windowB) =>
|
{windows.sort((windowA, windowB) =>
|
||||||
windowA.lastInteraction - windowB.lastInteraction
|
windowA.lastInteraction - windowB.lastInteraction
|
||||||
).map(({ id, app, size, position }, index) =>
|
).map(({ id, app, size, position, options }, index) =>
|
||||||
<Window
|
<Window
|
||||||
onInteract={() => { windowsManager.focus(windows[index]); }}
|
onInteract={() => { windowsManager.focus(windows[index]); }}
|
||||||
id={id}
|
id={id}
|
||||||
|
|
@ -19,6 +19,7 @@ export function WindowsView() {
|
||||||
app={app}
|
app={app}
|
||||||
size={size}
|
size={size}
|
||||||
position={position}
|
position={position}
|
||||||
|
options={options}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>);
|
</div>);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,18 @@ export default class Application {
|
||||||
* @param {String} id
|
* @param {String} id
|
||||||
* @param {React.ReactElement} windowContent
|
* @param {React.ReactElement} windowContent
|
||||||
*/
|
*/
|
||||||
constructor(name, id, windowContent) {
|
constructor(name, id, windowContent, windowOptions) {
|
||||||
Object.assign(this, { name, id, windowContent });
|
Object.assign(this, { name, id, windowContent, windowOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowContent = (props) => {
|
||||||
|
props = {...props, ...this.windowOptions};
|
||||||
|
|
||||||
|
if (this.windowContent == null) {
|
||||||
|
console.warn(`App (${this.id}) is missing the windowContent property.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <this.windowContent {...props}/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,21 +1,26 @@
|
||||||
/* eslint-disable eqeqeq */
|
/* eslint-disable eqeqeq */
|
||||||
import { FileExplorer } from "../../components/applications/file-explorer/FileExplorer.jsx";
|
import { FileExplorer } from "../../components/applications/file-explorer/FileExplorer.jsx";
|
||||||
|
import { MediaViewer } from "../../components/applications/media-viewer/MediaViewer.jsx";
|
||||||
import { WebView } from "../../components/applications/templates/WebView.jsx";
|
import { WebView } from "../../components/applications/templates/WebView.jsx";
|
||||||
import { Terminal } from "../../components/applications/terminal/Terminal.jsx";
|
import { Terminal } from "../../components/applications/terminal/Terminal.jsx";
|
||||||
import Application from "./application.js";
|
import Application from "./application.js";
|
||||||
|
|
||||||
export default class ApplicationsManager {
|
export default class ApplicationsManager {
|
||||||
static APPLICATIONS = [
|
static APPLICATIONS = [
|
||||||
new Application("Terminal", "terminal", <Terminal/>),
|
new Application("Terminal", "terminal", Terminal),
|
||||||
// new Application("Browser", "browser"),
|
// new Application("Browser", "browser"),
|
||||||
new Application("Code Editor", "code-editor"),
|
new Application("Code Editor", "code-editor"),
|
||||||
new Application("File Explorer", "file-explorer", <FileExplorer/>),
|
new Application("File Explorer", "file-explorer", FileExplorer),
|
||||||
new Application("Media Viewer", "media-viewer"),
|
new Application("Media Viewer", "media-viewer", MediaViewer),
|
||||||
new Application("Wordle", "wordle", <WebView source="https://prozilla.dev/wordle"/>),
|
new Application("Wordle", "wordle", WebView, { source: "https://prozilla.dev/wordle" }),
|
||||||
new Application("Balls", "balls", <WebView source="https://prozilla.dev/ball-maze"/>),
|
new Application("Balls", "balls", WebView, { source: "https://prozilla.dev/ball-maze" }),
|
||||||
new Application("Minesweeper", "minesweeper", <WebView source="https://prozilla.dev/minesweeper"/>),
|
new Application("Minesweeper", "minesweeper", WebView, { source: "https://prozilla.dev/minesweeper" }),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} id
|
||||||
|
* @returns {Application}
|
||||||
|
*/
|
||||||
static getApplication(id) {
|
static getApplication(id) {
|
||||||
let application = null;
|
let application = null;
|
||||||
|
|
||||||
|
|
@ -28,4 +33,21 @@ export default class ApplicationsManager {
|
||||||
|
|
||||||
return application;
|
return application;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} fileExtension
|
||||||
|
* @returns {Application}
|
||||||
|
*/
|
||||||
|
static getFileApplication(fileExtension) {
|
||||||
|
let app = null;
|
||||||
|
|
||||||
|
// eslint-disable-next-line default-case
|
||||||
|
switch (fileExtension) {
|
||||||
|
case "png":
|
||||||
|
app = this.getApplication("media-viewer");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
import ApplicationsManager from "../applications/applications.js";
|
import ApplicationsManager from "../applications/applications.js";
|
||||||
import { randomRange } from "../math/random.js";
|
import { randomRange } from "../math/random.js";
|
||||||
import Vector2 from "../math/vector2.js";
|
import Vector2 from "../math/vector2.js";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { VirtualFile } from "../virtual-drive/virtual-file.js";
|
||||||
|
|
||||||
export default class WindowsManager {
|
export default class WindowsManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.windows = {};
|
this.windows = {};
|
||||||
this.updateWindows = () => {};
|
this.updateWindows = () => {};
|
||||||
console.log("Windows manager init");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open(appId) {
|
open(appId, options) {
|
||||||
const app = ApplicationsManager.getApplication(appId);
|
const app = ApplicationsManager.getApplication(appId);
|
||||||
const size = new Vector2(700, 400);
|
const size = new Vector2(700, 400);
|
||||||
const position = new Vector2(randomRange(50, 600), randomRange(50, 450));
|
const position = new Vector2(randomRange(50, 600), randomRange(50, 450));
|
||||||
|
|
@ -19,13 +20,16 @@ export default class WindowsManager {
|
||||||
id++;
|
id++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
id = id.toString();
|
||||||
|
|
||||||
console.log(`Opening window ${id}:${app.id}`);
|
console.log(`Opening window ${id}:${app.id}`);
|
||||||
|
|
||||||
this.windows[id.toString()] = {
|
this.windows[id] = {
|
||||||
id,
|
id,
|
||||||
app,
|
app,
|
||||||
size,
|
size,
|
||||||
position,
|
position,
|
||||||
|
options,
|
||||||
lastInteraction: new Date().valueOf()
|
lastInteraction: new Date().valueOf()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -33,6 +37,14 @@ export default class WindowsManager {
|
||||||
// console.log(this);
|
// console.log(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {VirtualFile} file
|
||||||
|
*/
|
||||||
|
openFile(file) {
|
||||||
|
const app = ApplicationsManager.getFileApplication(file.extension);
|
||||||
|
this.open(app.id, { file });
|
||||||
|
}
|
||||||
|
|
||||||
close(windowId) {
|
close(windowId) {
|
||||||
windowId = windowId.toString();
|
windowId = windowId.toString();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,13 @@ function initVirtualRoot(virtualRoot) {
|
||||||
folder.setAlias("~")
|
folder.setAlias("~")
|
||||||
.createFolder("Images", (folder) => {
|
.createFolder("Images", (folder) => {
|
||||||
folder.createFile("Wallpaper_1", "png", (file) => {
|
folder.createFile("Wallpaper_1", "png", (file) => {
|
||||||
file.setSource("/public/media/wallpapers/wallpaper-1.png")
|
file.setSource("/media/wallpapers/wallpaper1.png")
|
||||||
}).createFile("Wallpaper_2", "png", (file) => {
|
}).createFile("Wallpaper_2", "png", (file) => {
|
||||||
file.setSource("/public/media/wallpapers/wallpaper-2.png")
|
file.setSource("/media/wallpapers/wallpaper2.png")
|
||||||
}).createFile("Wallpaper_3", "png", (file) => {
|
}).createFile("Wallpaper_3", "png", (file) => {
|
||||||
file.setSource("/public/media/wallpapers/wallpaper-3.png")
|
file.setSource("/media/wallpapers/wallpaper3.png")
|
||||||
}).createFile("Wallpaper_4", "png", (file) => {
|
}).createFile("Wallpaper_4", "png", (file) => {
|
||||||
file.setSource("/public/media/wallpapers/wallpaper-4.png")
|
file.setSource("/media/wallpapers/wallpaper4.png")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.createFolder("Documents")
|
.createFolder("Documents")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue