Add object properties sidebar. Closes #68

This commit is contained in:
Tracy Rust 2024-01-27 16:39:39 -05:00
commit 3bba33e788
12 changed files with 233 additions and 65 deletions

View File

@ -20,6 +20,7 @@ import {localNodeConfig} from "./localnodeconfig"
import './components/navbar.css';
import { EulaScreen } from './components/eula';
import {ObjectPropertiesSidebar} from "./components/objectpropertiessidebar";
//I gave up on trying to make rollup work correctly, just put electron-main dependencies here:
@ -154,7 +155,6 @@ function App()
<Notifications appContext={appContext} />
</div>
</div>
<Overlays appContext={appContext} />

View File

@ -0,0 +1,11 @@
.object-properties-sidebar {
background-color: #260C3C;
width: 300px;
padding: 20px;
overflow: hidden;
}
.object-properties-sidebar > p, h1, h2 {
color: white;
word-wrap: break-word;
}

View File

@ -0,0 +1,72 @@
import React, {useEffect, useState} from 'react';
import {StatorContext} from "../statorobject";
import {useWatchSidebarStator} from "./sidebarstator";
import {getBinderListStator} from "./openbinders";
import {BinderContext} from "./bindercontext";
import "./objectpropertiessidebar.css"
import {getMetaApi} from "../ipcbindings";
interface ObjectPropertiesSidebarProps {
appContext: StatorContext;
hide: boolean;
objId: string;
binderContext: BinderContext;
imageResolution: string | undefined;
}
function ObjectPropertiesSidebar(props: ObjectPropertiesSidebarProps) {
const [sizeInBytes, setSizeInBytes] = useState<number>();
const [mimeString, setMimeString] = useState("...");
const [mainMimeType, setMainMimeType] = useState("");
useEffect(() => {
if(!props.objId) return;
(async () => {
const path = await props.binderContext.db.getObjectPath(props.objId);
if(path == "") return;
const stats = await getMetaApi().readFileStats(path.path);
if(!stats) return;
setSizeInBytes(Number(stats.size));
const mime = await props.binderContext.db.getMimeGenIfNotExists(props.objId);
setMimeString(mime);
const splits = mime.split("/");
if (splits.length > 1) {
setMainMimeType(splits[0]);
}
})();
}, [props.objId]);
const sizeString = (() => {
const sizeInKilobytes = sizeInBytes / 1024;
const sizeInMegabytes = sizeInKilobytes / 1024;
const sizeInGigabytes = sizeInMegabytes / 1024;
if(sizeInKilobytes < 1)
return sizeInBytes.toFixed(2) + " bytes";
if(sizeInMegabytes < 1)
return sizeInKilobytes.toFixed(2) + " kilobytes";
if(sizeInGigabytes < 1)
return sizeInMegabytes.toFixed(2) + " megabytes";
return String(sizeInGigabytes) + " gigabytes";
})();
if(props.hide) return <></>
else return <div className={"object-properties-sidebar"}>
<h1>Properties</h1>
<p>File Size: {sizeString}</p>
<p>Type: {mimeString}</p>
{mainMimeType == "image" ? <p>Resolution: {props.imageResolution}</p> : ""}
</div>
}
export {ObjectPropertiesSidebar}

View File

@ -1,9 +1,15 @@
.object-viewer-and-properties-container {
display: flex;
width: 100%;
height: 100%;
}
.object-viewer
{
flex: 1;
display: flex;
align-items: center;
height: 100%;
width: 100%;
color: white;
position:relative;
overflow: hidden;
@ -31,13 +37,16 @@
background-color: aliceblue;
}
.object-viewer-controls-left-right-base-style {
height: 40%;
width: 90px;
padding: 20px;
}
.object-viewer-side-control-overlay
{
position:absolute;
display: flex;
height: 40%;
width: 90px;
padding: 20px;
opacity: 0;
}
@ -70,6 +79,15 @@
border-radius: 10px 0 0 10px;
}
.object-viewer-side-control-overlay-top-right
{
top: 0;
right: 0;
border-radius: 0 0 0 10px;
height: 3%;
padding: 20px;
}
/*so... If we don't do this... At least in chromium...

View File

@ -4,6 +4,8 @@ import { StatorContext } from '../statorobject';
import "./objectviewer.css"
import { getObjectViewerStator } from './objectviewercontroller';
import { getBinderListStator } from './openbinders';
import {ObjectPropertiesSidebar} from "./objectpropertiessidebar";
import {getSidebarStator, useWatchSidebarStator} from "./sidebarstator";
export interface ObjectViewerProps {
theBinderId: BinderConnectionId;
@ -40,10 +42,12 @@ function VideoPlayerHead(props: ObjectViewerProps)
interface ViewerImplProps extends ObjectViewerProps {
objId: string;
resolutionPropogator: (res: string) => void;
}
function ImageViewer(props: ViewerImplProps) {
const imageRef = useRef<HTMLImageElement>(null);
const context = getBinderListStator(props.appContext).getBinder(props.theBinderId).context;
const [imageUrl, setImageUrl] = useState<string>();
@ -66,12 +70,18 @@ function ImageViewer(props: ViewerImplProps) {
controller.startOrContinueSlideshow();
}
*/
const width = imageRef.current.naturalWidth
const height = imageRef.current.naturalHeight;
props.resolutionPropogator("" + width + "x" + height + "px");
}
const wrappedURI = imageUrl ? "file:///" + imageUrl : undefined;
return (
<img
ref={imageRef}
className="object-viewer-content-area"
src={wrappedURI}
onLoad={handleLoad}
@ -117,20 +127,25 @@ function PlainTextViewer(props: ViewerImplProps)
function ObjectViewer(props: ObjectViewerProps)
{
const myRef = useRef<HTMLDivElement>(null)
const myRef = useRef<HTMLDivElement>(null);
//const myRef = useRef(null);
const stator = getObjectViewerStator(props.appContext);
const context = getBinderListStator(props.appContext).getBinder(props.theBinderId).context;
const [mime, setMime] = useState<string>();
const [imageResolution, setImageResolution] = useState<string>("");
const [objId, setObjId] = useState<string>();
const [doRender, setDoRender] = useState<boolean>(false);
//const objectOpenStator = getObjectViewerOpenStator(props.appContext);
const sidebarStator = useWatchSidebarStator(props.appContext);
useEffect(() => {
setImageResolution("");//reset on object change
if (mime) {
return;
}
(async () => {
if (!objId) {
return;
@ -151,6 +166,7 @@ function ObjectViewer(props: ObjectViewerProps)
}, [objId]);
useEffect(() => {
const currentOpenObjectCont = context.currentOpenObject;
@ -281,7 +297,7 @@ function ObjectViewer(props: ObjectViewerProps)
if (mainMimeType == "image") {
myJsx = <ImageViewer {...props} objId={objId} />
myJsx = <ImageViewer {...props} objId={objId} resolutionPropogator={setImageResolution}/>
} else if (mainMimeType == "application") {
let val = secondMimeType;
if (secondMimeType == "octet-stream") {
@ -306,30 +322,56 @@ function ObjectViewer(props: ObjectViewerProps)
e.stopPropagation();
};
const propertiesToggle = (e: React.MouseEvent) => {
e.stopPropagation();
sidebarStator.toggleObjectProperties();
}
return (
<div className={`object-viewer ${checkboardBack ? 'object-viewer-checkerboard' : ''}`}
onClick={clickHandler}
onKeyDown={downHandler}
tabIndex={1}
<div className={"object-viewer-and-properties-container"}
style={style}
ref={myRef}
>
<div className={"object-viewer-container"}>
{myJsx}
<div className={`object-viewer ${checkboardBack ? 'object-viewer-checkerboard' : ''}`}
onClick={clickHandler}
onKeyDown={downHandler}
tabIndex={1}
ref={myRef}
>
<div className={"object-viewer-container"}>
{myJsx}
</div>
<img className={'object-viewer-side-control-overlay ' +
'object-viewer-controls-left-right-base-style ' +
'object-viewer-side-control-overlay-left'}
src={'static/rewind-button.svg'}
draggable={false}
onClick={backClickHandler}
/>
<img className={'object-viewer-side-control-overlay ' +
'object-viewer-controls-left-right-base-style ' +
'object-viewer-side-control-overlay-right'}
src={'static/fast-forward-button.svg'}
draggable={false}
onClick={forwardClickHandler}
/>
<img className={'object-viewer-side-control-overlay object-viewer-side-control-overlay-top-right'}
src={'static/kebaab.svg'}
draggable={false}
onClick={propertiesToggle}
/>
</div>
<img className={'object-viewer-side-control-overlay object-viewer-side-control-overlay-left'}
src={'static/rewind-button.svg'}
draggable={false}
onClick={backClickHandler}
/>
<img className={'object-viewer-side-control-overlay object-viewer-side-control-overlay-right'}
src={'static/fast-forward-button.svg'}
draggable={false}
onClick={forwardClickHandler}
/>
<ObjectPropertiesSidebar appContext={props.appContext}
hide={sidebarStator.objectPropertiesHidden}
objId={objId}
imageResolution={imageResolution}
binderContext={context}/>
</div>
);
)
;
}
export {ObjectViewer}

View File

@ -0,0 +1,31 @@
import {StatorContext, StatorObjectBase} from "../statorobject";
class SidebarStator extends StatorObjectBase {
tagSidebarHidden = false;
objectPropertiesHidden = true;
toggleTagSidebar() {
this.tagSidebarHidden = !this.tagSidebarHidden;
this.publishChanges();
}
toggleObjectProperties() {
this.objectPropertiesHidden = !this.objectPropertiesHidden;
this.publishChanges();
}
}
const SidebarStatorKey = "SidebarStatorKeyKey";
function useWatchSidebarStator(appContext: StatorContext)
{
return appContext.useWatchStatorLegacy(SidebarStatorKey, new SidebarStator(appContext)) as SidebarStator;
}
function getSidebarStator(appContext: StatorContext)
{
return appContext.getStatorLegacy(SidebarStatorKey, new SidebarStator(appContext)) as SidebarStator;
}
export { useWatchSidebarStator, getSidebarStator }

View File

@ -10,39 +10,7 @@ import { BinderContextPair, getBinderListStator, useWatchBinderListStator } from
import { reserved } from '../tagdatabase'
import { BinderContext, ContextStatus } from './bindercontext'
import { useWatchObservableValue } from '../observermanager'
class TagSidebarStator extends StatorObjectBase
{
hidden = false;
setHiddenness(h: boolean)
{
if(h == this.hidden)
return;
this.hidden = h;
this.publishChanges();
}
toggle()
{
this.hidden = !this.hidden;
this.publishChanges();
}
}
const TagSidebarStatorKey = "TagBarSideBarStatorKeyKey";
function useWatchTagSidebarStator(appContext: StatorContext)
{
return appContext.useWatchStatorLegacy(TagSidebarStatorKey, new TagSidebarStator(appContext)) as TagSidebarStator;
}
function getTagSidebarStator(appContext: StatorContext)
{
return appContext.getStatorLegacy(TagSidebarStatorKey, new TagSidebarStator(appContext)) as TagSidebarStator;
}
import { useWatchSidebarStator} from "./sidebarstator";
function Header(props: {context: BinderContext})
{
@ -152,7 +120,7 @@ function TagSidebar(props: TagSidebarProps)
{
//Fairly innefficient to re-render this errytime a selection changes...
const binderListStator = useWatchBinderListStator(props.appContext);
const tagSidebarStator = useWatchTagSidebarStator(props.appContext); //This is just for the show/hide button thing
const tagSidebarStator = useWatchSidebarStator(props.appContext); //This is just for the show/hide button thing
if(binderListStator.getOpenBinderList().length == 0)
{
@ -169,7 +137,7 @@ function TagSidebar(props: TagSidebarProps)
/>);
});
const style = tagSidebarStator.hidden ? {display: "none"} : {};
const style = tagSidebarStator.tagSidebarHidden ? {display: "none"} : {};
return <div style={style}>{tagSidebars}</div>;
}
@ -181,15 +149,15 @@ interface SidebarViewToggleButtonProps
function SidebarViewToggleButton(props: SidebarViewToggleButtonProps)
{
const sidebarStator = useWatchTagSidebarStator(props.appContext);//Cause this is for global stuff like show/hide
const sidebarStator = useWatchSidebarStator(props.appContext);//Cause this is for global stuff like show/hide
const handleClick = (e: any) =>
{
sidebarStator.toggle();
sidebarStator.toggleTagSidebar();
};
const imgSrc = sidebarStator.hidden ? "static/show-tag-sidebar.svg"
const imgSrc = sidebarStator.tagSidebarHidden ? "static/show-tag-sidebar.svg"
: "static/hide-tag-sidebar.svg";
return (<img

View File

@ -99,11 +99,14 @@ function setupCommonIPC(authKey, win) {
ipcMain.handle("readFileModTime", async (event, path) => {
return pathUtils.readFileModTime(path);
});
ipcMain.handle("readFileStats", async (event, path) => {
return pathUtils.readFileStats(path);
});
ipcMain.handle("listPaths", async (event, path) => {
return pathUtils.listPaths(path);
});
ipcMain.handle("copyPath", async (event, from, to) => {
pathUtils.copyPath(from, to);
await pathUtils.copyPath(from, to);
});
ipcMain.handle("renamePath", async (event, path, newPath) => {
return pathUtils.renamePath(path, newPath);

View File

@ -41,6 +41,7 @@ interface MetaApi {
readMime: (path: string) => Promise<string>;
createDirectories: (path: string) => Promise<any>;
readFileModTime: (path: string) => Promise<string>;
readFileStats: (path: string) => Promise<any>; //todo make typed right
listPaths: (path: string) => Promise<DirectoryListing | undefined>;
copyPath: (from: string, to: string) => Promise<void>;
renamePath: (path: string, newPath: string) => Promise<any>;

View File

@ -237,6 +237,18 @@ async function readFileModTime(path) {
});
}
async function readFileStats(path) {
return new Promise(async (resolve, reject) => {
try {
const stats = await fsPromises.stat(path, {bigint: true});
resolve(stats);
} catch(e) {
reject();
}
});
}
async function listPaths(path) {
return new Promise(async (resolve, reject) => {
let dir;
@ -370,6 +382,7 @@ export {
readMime,
createDirectories,
readFileModTime,
readFileStats,
listPaths,
copyPath,
renamePath,

View File

@ -83,6 +83,10 @@ contextBridge.exposeInMainWorld('metaApi',
return ipcRenderer.invoke('readFileModTime', path);
},
readFileStats: async (path) => {
return ipcRenderer.invoke('readFileStats', path);
},
listPaths: async (path) => {
return ipcRenderer.invoke('listPaths', path);
},

View File

@ -56,6 +56,11 @@ const b2bSumToObjectId = "b2bSumToObjectId";
const tagsTable = "tags";
const objectsTable = "objects";
const mimesTableV1 = "mimesV1";
//For historical reasons, the above are stored at the root but future stuff
// should be stored in the object properties table
const objectPropertiesV1 = "objectPropertiesV1";
const objectPropertyImageWidth = "imageWidth";
const objectPropertyImageHeight = "imageHeight";
const tagChildSortListTable = "tagChildSortList";