Initial commit, closes #60

This commit is contained in:
Tracy Rust 2024-01-23 19:56:26 -05:00
commit 465b79e153
130 changed files with 28217 additions and 0 deletions

95
.gitignore vendored Normal file
View File

@ -0,0 +1,95 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Vite
.vite/
# Electron-Forge
out/
# Binder custom artefacts
buildInfo.json

8
AppRun Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
#Without this it crashes on thumbnail generation with
# "GSlice: assertion failed", probably due to an old version
# of glib that electron uses
export G_SLICE=always-malloc
HERE="$(dirname "$(readlink -f "${0}")")"
exec "${HERE}/usr/bin/Binder" "$@"

6
Binder.desktop Normal file
View File

@ -0,0 +1,6 @@
[Desktop Entry]
Name=Binder
Exec=Binder
Icon=binder
Type=Application
Categories=FileManager;

28
LICENSE.txt Normal file
View File

@ -0,0 +1,28 @@
Enesda Copycenter License v1
0) This software is licensed to you on an "as-is" basis. All warranties, express or implied, including merchantability, non-infringement, fitness for purpose, and warranty of title, are disclaimed.
1) This software is free to modify or use for any purpose not restricted by active addendums appended to the bottom of this license.
2) You are free to distribute this software, where distribution for the purposes of this license means intentionally providing access in any way to this software.
3) Addendums may be appended to this license text to restrict the use of the software, but they must have an explicit expiration date (and not simply a length of time or a way of calculating the date) of not more than 7 years after intentional distribution. Addendums may not restrict the possession or distribution of this software in source form, or the addition of further addendums.
4) Addendums may be removed or ignored once they have expired or if they are in violation of this license sans addendums. The owner of an addendum may also remove it from any modified version of this software at any time.
5) In order to distribute modified forms of this software, the modifications must be licensed under the terms of this license, including active addendums.
6) If you modify this software and distribute it in non-source form, you must also provide the modified source (if any) either alongside it, in an easy to find location, or upon request, and it must be free of charge or at cost to distribute. The source for modified software must be in its original, non-obfuscated form.
7) This software cannot be made to run on hardware which is configured to prevent its owners from using it to run their own modified versions of this software.
8) This license does not grant the right to use the software source itself to train AI, this does not apply to the use of the software itself in the development and training of AI.
You may add addendums below the line, separated from each other by three hyphens.
------
Enesda's addendum
Expiration date: February 2027
Without an additional license, you may use or modify this software for the purposes of evaluation with intent to purchase, or audit for security purposes. For other uses, such as personal or commercial, you will need to obtain an appropriate license from Enesda or authorized distributor.
---

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# Binder is eventually open source software
For details see the license file.
# Caution
This code isn't super clean, the focus has been on getting things done instead of pretty architecture so there are definitely some rough edges and embarrassing design decisions but they remain because they're bug-free and I have a mountain to move with a spoon already.
Also a lot of comments are "stream of conciousness" style and I honestly don't know what they meant, either :)
# See anything you like?
Feel free to open an issue to discuss potentially open-sourcing utility code you may find useful.

151
buildlinuxappimage.js Normal file
View File

@ -0,0 +1,151 @@
const fs = require('node:fs');
const process = require("node:process");
const {execSync, exec} = require('node:child_process')
const binderPortable = "out/Binder-linux-x64";
const excludeList = `"libatk-1.0.so.0
libatk-bridge-2.0.so.0,
libatspi.so.0,
libavahi-client.so.3,
libavahi-common.so.3,
libblkid.so.1,
libbsd.so.0,
libcairo-gobject.so.2,
libcairo.so.2,
libcups.so.2,
libdatrie.so.1,
libdbus-1.so.3,
libepoxy.so.0,
libffi.so.6,
libgcrypt.so.20,
libgdk-3.so.0,
libgdk_pixbuf-2.0.so.0,
libgio-2.0.so.0,
libglib-2.0.so.0,
libgmodule-2.0.so.0,
libgnutls.so.30,
libgobject-2.0.so.0,
libgraphite2.so.3,
libgssapi_krb5.so.2,
libgtk-3.so.0,
libhogweed.so.4,
libidn2.so.0,
libk5crypto.so.3,
libkeyutils.so.1,
libkrb5.so.3,
libkrb5support.so.0,
liblz4.so.1,
liblzma.so.5,
libmount.so.1,
libnettle.so.6,
libnspr4.so,
libnss3.so,
libnssutil3.so,
libpango-1.0.so.0,
libpangocairo-1.0.so.0,
libpangoft2-1.0.so.0,
libpcre.so.3,
libpixman-1.so.0,
libplc4.so,
libplds4.so,
libpng16.so.16,
libselinux.so.1,
libsmime3.so,
libsystemd.so.0,
libtasn1.so.6,
libunistring.so.2,
libwayland-client.so.0,
libwayland-cursor.so.0,
libwayland-egl.so.1,
libwayland-server.so.0,
libXau.so.6,
libxcb-render.so.0,
libxcb-shm.so.0,
libXcomposite.so.1,
libXcursor.so.1,
libXdamage.so.1,
libXdmcp.so.6,
libXext.so.6,
libXfixes.so.3,
libXinerama.so.1,
libXi.so.6"`
function addIconToAppDir(resolution)
{
var theDir = "out/AppDir/usr/share/icons/hicolor/" + resolution + "x" + resolution + "/apps";
var theDestPath = theDir + "/binder.png"
var srcPath = "../icons/icon" + resolution + ".png";
fs.mkdirSync(theDir, {recursive: true});
fs.copyFileSync(srcPath, theDestPath);
}
function constructAppImage(versionString)
{
try
{
fs.rmSync("out/AppDir", {recursive: true, force: true});
}
catch
{
//pass
}
fs.mkdirSync("out/AppDir/usr/bin", {recursive: true});
//this is a fucking mess wtf
fs.renameSync(binderPortable, "out/AppDir/usr/bin");
//fs.mkdirSync("out/AppDir/usr/lib", {recursive: true});
fs.mkdirSync("out/AppDir/usr/share/applications", {recursive: true});
fs.copyFileSync("AppRun", "out/AppDir/AppRun");
//Appimagetool seems to be fine with just the appimage at the AppDir level but
// I don't know the specifics and it looks like linuxdeployqt leaves it in bother
// places so lets just do that to be safe
fs.copyFileSync("Binder.desktop", "out/AppDir/usr/share/applications/Binder.desktop");
fs.copyFileSync("Binder.desktop", "out/AppDir/Binder.desktop");
fs.copyFileSync("Binder.desktop", "out/AppDir/Binder.desktop");
//Also it wants a specific icon
fs.copyFileSync( "../icons/icon256.png", "out/AppDir/binder.png");
addIconToAppDir(16);
addIconToAppDir(24);
addIconToAppDir(32);
addIconToAppDir(48);
addIconToAppDir(64);
addIconToAppDir(96);
addIconToAppDir(128);
addIconToAppDir(256);
fs.mkdirSync("out/AppDir/usr/share/icons/hicolor/scalable", {recursive: true});
fs.copyFileSync("../icons/icon.svg", "out/AppDir/usr/share/icons/hicolor/scalable/binder.svg");
//Do this or deal with a weird character in the output
versionString = versionString.trim();
console.log("version tag: " + versionString);
const newEnv = {...process.env, VERSION: versionString, ARCH: "x86_64"};
console.log("Using environment variables: " + JSON.stringify(newEnv));
process.chdir('out');
execSync("appimagetool AppDir", {env: newEnv});
}
async function doTheDo()
{
console.log("Henlo wurl :3c");
//console.log("Package ver: " + process.env.npm_package_version);
constructAppImage(process.env.npm_package_version);
}
doTheDo();

135
forge.config.js Normal file
View File

@ -0,0 +1,135 @@
const { genBuildInfo } = require("./genbuildinfo.js");
const nodePath = require('node:path');
/*
const nodePath = {join: (base, end) => {
return `${base}/${end}`;
}}
*/
function ignoreFiles(path) {
if (!path) return true; //wtf why does it ever empty??
const val = JSON.stringify(path);
throw new Error(val);
const allowedDirectories = [
".vite",
"icons",
"node_modules",
];
//block takes precedence over pass
const disallowedDirectories = [
"out",
"node_modules/.bin",
"node_modules/electron",
"node_modules/electron-prebuilt",
"node_modules/electron-prebuilt-compile",
"node_modules/electron-packager",
".git",
];
//these take precedence over all else
const explicitlyAllowedFiles = [
"src/thumbnailworkercode.mjs",
"package.json",
];
for (const dirEnd of explicitlyAllowedFiles) {
const absolute = nodePath.join(__dirname, dirEnd);
if (absolute === path) {
throw new Error("passing explicit: " + path);
return false;
}
}
let inAllowed = false;
for (const dirEnd of allowedDirectories) {
const absolute = nodePath.join(__dirname, dirEnd);
if (path.startsWith(absolute)) {
inAllowed = true;
break;
}
}
for (const dirEnd of disallowedDirectories) {
const absolute = nodePath.join(__dirname, dirEnd);
if (path.startsWith(absolute)) {
inAllowed = false;
break;
}
}
if (inAllowed) {
throw new Error("disallowing: " + path);
} else {
throw new Error("allowing: " + path);
}
return !inAllowed;
}
module.exports = {
packagerConfig: {
asar: false, //having trouble getting workers to load modules...
appCopyright: "Copyright (c) 2024 Enesda.",
icon: 'icons/icon',
//please forgive me for I have sinned
//Actually no this is bullshit which does it dump in (potentially sensitive) files in
// by default until asked not to on a case-by-case basis?
//The predicate version didn't do anything but pass in undefined paths...
ignore: /^(?!\/\.vite|\/icons|\/package\.json|\/buildInfo.json|\/LICENSE.txt|\/node_modules).+$/,
},
hooks: {
generateAssets: genBuildInfo
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
authors: "Enesda.",
description: 'Binder, a tag based organizer.',
iconUrl: nodePath.join(nodePath.dirname(__dirname), 'icons', 'icon.ico'),
setupIcon: 'icons/setupIcon.ico'
}
},
{
name: '@electron-forge/maker-zip',
platforms: ['linux'],
},
],
plugins: [
{
name: '@electron-forge/plugin-vite',
config: {
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
// If you are familiar with Vite configuration, it will look really familiar.
build: [
{
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
entry: 'src/main.js',
config: 'vite.main.config.mjs',
},
{
entry: 'src/preload.js',
config: 'vite.preload.config.mjs',
},
],
renderer: [
{
name: 'main_window',
config: 'vite.renderer.config.mjs',
},
],
},
},
],
};

24
genbuildinfo.js Normal file
View File

@ -0,0 +1,24 @@
const {execSync} = require('node:child_process');
const fs = require('node:fs');
const {version} = require("./package.json");
async function genArtefact() {
return {
"branch": '' + execSync("git symbolic-ref --short HEAD"),
"appTitle": "Binder",
"buildTimestamp": Date.now(),
"version": version,
"CommitSHA": '' + execSync("git rev-parse HEAD"),
}
}
async function genBuildInfo() {
const buildArtefact = await genArtefact();
fs.writeFileSync("buildInfo.json", JSON.stringify(buildArtefact));
}
module.exports = {
genBuildInfo
}

BIN
icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
icons/setupIcon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Binder</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

9931
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "binder",
"productName": "Binder",
"version": "0.1.13",
"description": "Binder, a tag based organizer.",
"main": ".vite/build/main.js",
"scripts": {
"start": "G_SLICE=always-malloc electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"make-appimage": "node buildlinuxappimage.js",
"publish": "electron-forge publish",
"lint": "echo \"No linting configured\"",
"test-gen-build-info": "node test/manual/testgenbuildinfo.js"
},
"keywords": [],
"author": {
"name": "Tracy Rust",
"email": "tracy@enesda.com"
},
"license": "Enesda Copycenter v1",
"devDependencies": {
"@electron-forge/cli": "^7.2.0",
"@electron-forge/maker-deb": "^7.2.0",
"@electron-forge/maker-squirrel": "^7.2.0",
"@electron-forge/maker-zip": "^7.2.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.2.0",
"@electron-forge/plugin-vite": "^7.2.0",
"@types/jest": "^29.5.11",
"@types/react": "^18.2.18",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.4",
"electron": "^28.1.4",
"electron-forge-maker-nsis": "^24.6.3"
},
"dependencies": {
"@noble/hashes": "^1.3.3",
"electron-squirrel-startup": "^1.0.0",
"proper-lockfile": "^4.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sharp": "^0.32.6",
"uuid": "^9.0.0",
"wasmagic": "^0.0.32"
}
}

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="cancel-button.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="6.0325"
inkscape:cx="46.964219"
inkscape:cy="52.943877"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<rect
y="187.79733"
x="195.00795"
height="2.0003684"
width="30.005524"
id="rect6171"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.16736799"
transform="rotate(44.999999)" />
<rect
transform="matrix(0.70253853,-0.71164571,0.77153858,0.63618254,0,0)"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1680948"
id="rect15"
width="29.997458"
height="2.0183222"
x="-223.86432"
y="208.61888" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50mm"
height="50mm"
viewBox="0 0 50 50"
version="1.1"
id="svg53"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="checkerboard.svg">
<defs
id="defs47" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.959798"
inkscape:cx="75.523552"
inkscape:cy="93.326853"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata50">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-247)">
<rect
style="fill:#b3b3b3;stroke-width:0.26458332"
id="rect59"
width="50"
height="50"
x="0"
y="247" />
<rect
style="fill:#4d4d4d;stroke-width:0.26458332"
id="rect55"
width="25"
height="25"
x="0"
y="272" />
<rect
y="247"
x="25"
height="25"
width="25"
id="rect57"
style="fill:#4d4d4d;stroke-width:0.26458332" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="child-tag-attention-tag-tree-arrow-black.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect672"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1 @ F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#260c3c"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="3.0894365"
inkscape:cx="50.494645"
inkscape:cy="57.615685"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#260c3c" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<path
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.287226px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 20.974963,268.09172 8.953994,26.94234 a 1.5031539,1.5031539 135.00311 0 1 -1.900659,1.90045 L 1.0919429,287.97926 a 1.595188,1.595188 90.695257 0 1 0.036881,-3.03922 l 10.0039918,-3.05841 a 5.6413451,5.6413451 134.99711 0 0 3.745718,-3.74609 l 3.058212,-10.00696 a 1.5946104,1.5946104 179.30496 0 1 3.038217,-0.0369 z"
id="path1203"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:path-effect="#path-effect672"
inkscape:original-d="M 19.39807,263.34689 30.875093,297.88096 -3.6527154,286.40185 14.00174,281.00455 Z" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="child-tag-attention-tag-tree-arrow-some.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect672"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1 @ F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#260c3c"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="5.2181833"
inkscape:cx="44.459917"
inkscape:cy="64.965138"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#260c3c" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<path
style="fill:#af8ff4;fill-opacity:1;stroke:none;stroke-width:0.287226px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 20.974963,268.09172 8.953994,26.94234 a 1.5031539,1.5031539 135.00311 0 1 -1.900659,1.90045 L 1.0919429,287.97926 a 1.595188,1.595188 90.695257 0 1 0.036881,-3.03922 l 10.0039918,-3.05841 a 5.6413451,5.6413451 134.99711 0 0 3.745718,-3.74609 l 3.058212,-10.00696 a 1.5946104,1.5946104 179.30496 0 1 3.038217,-0.0369 z"
id="path1203"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:path-effect="#path-effect672"
inkscape:original-d="M 19.39807,263.34689 30.875093,297.88096 -3.6527154,286.40185 14.00174,281.00455 Z" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="child-tag-attention-tag-tree-arrow.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect672"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1 @ F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#260c3c"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="3.0894365"
inkscape:cx="-32.368362"
inkscape:cy="57.615685"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#260c3c" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.287226px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 20.974963,268.09172 8.953994,26.94234 a 1.5031539,1.5031539 135.00311 0 1 -1.900659,1.90045 L 1.0919429,287.97926 a 1.595188,1.595188 90.695257 0 1 0.036881,-3.03922 l 10.0039918,-3.05841 a 5.6413451,5.6413451 134.99711 0 0 3.745718,-3.74609 l 3.058212,-10.00696 a 1.5946104,1.5946104 179.30496 0 1 3.038217,-0.0369 z"
id="path1203"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:path-effect="#path-effect672"
inkscape:original-d="M 19.39807,263.34689 30.875093,297.88096 -3.6527154,286.40185 14.00174,281.00455 Z" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="clear-input-button.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.7540625"
inkscape:cx="165.57105"
inkscape:cy="65.352432"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<g
id="g3229"
transform="matrix(0.93956,0,0,0.93956032,2.3626435,17.085205)">
<rect
transform="rotate(44.999999)"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.11806578"
id="rect6171"
width="21.166685"
height="1.4111123"
x="202.06351"
y="185.39378" />
<g
id="g3224">
<rect
transform="matrix(0.70253853,-0.71164571,0.77153858,0.63618254,0,0)"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.11857849"
id="rect15"
width="21.160994"
height="1.4237775"
x="-217.00284"
y="211.58026" />
</g>
</g>
<path
style="fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:1.1093235;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 29.445338,272.55466 H 12.300341 L 0.87034466,282 12.300341,291.44534 h 17.144997 z"
id="path2678"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="close-button.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="6.0324999"
inkscape:cx="80.290496"
inkscape:cy="47.59209"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<g
id="g6175"
style="fill:#000000"
transform="matrix(0.83333333,-0.83333333,0.83333333,0.83333333,-232.5,59.500001)">
<rect
y="279"
x="4.4408921e-16"
height="6"
width="30"
id="rect6169"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.2898365"
ry="3" />
<rect
transform="rotate(90)"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.2898365"
id="rect6171"
width="30"
height="6"
x="267"
y="-18"
ry="3" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="close-button.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="6.0324999"
inkscape:cx="80.290496"
inkscape:cy="47.59209"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<g
id="g6175"
style="fill:#ffffff"
transform="matrix(0.83333333,-0.83333333,0.83333333,0.83333333,-232.5,59.500001)">
<rect
y="279"
x="4.4408921e-16"
height="6"
width="30"
id="rect6169"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.2898365"
ry="3" />
<rect
transform="rotate(90)"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.2898365"
id="rect6171"
width="30"
height="6"
x="267"
y="-18"
ry="3" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="closed-tag-tree-arrow-black.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect672"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1 @ F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#260c3c"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.77235913"
inkscape:cx="-122.99978"
inkscape:cy="12.947345"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#260c3c" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<path
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.301057px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 2.3093538,267.16898 26.9474022,13.50203 a 1.503154,1.503154 90.000007 0 1 0,2.6878 L 2.3093543,296.86083 a 1.5948989,1.5948989 45.695155 0 1 -2.12262541,-2.17476 l 5.32979531,-10.0224 a 5.6413441,5.6413441 90.000007 0 0 7e-7,-5.29752 L 0.18672782,269.34374 a 1.5948989,1.5948989 134.30486 0 1 2.12262598,-2.17476 z"
id="path1203"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:path-effect="#path-effect672"
inkscape:original-d="M -2.1609004,264.92915 31.938909,282.01491 -2.1609004,299.10066 6.9251018,282.01491 Z" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="closed-tag-tree-arrow.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect672"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1 @ F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#260c3c"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.77235913"
inkscape:cx="42.726238"
inkscape:cy="12.947345"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#260c3c" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<path
style="fill:#77499f;fill-opacity:1;stroke:none;stroke-width:0.301057px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 2.3093538,267.16898 26.9474022,13.50203 a 1.503154,1.503154 90.000007 0 1 0,2.6878 L 2.3093543,296.86083 a 1.5948989,1.5948989 45.695155 0 1 -2.12262541,-2.17476 l 5.32979531,-10.0224 a 5.6413441,5.6413441 90.000007 0 0 7e-7,-5.29752 L 0.18672782,269.34374 a 1.5948989,1.5948989 134.30486 0 1 2.12262598,-2.17476 z"
id="path1203"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:path-effect="#path-effect672"
inkscape:original-d="M -2.1609004,264.92915 31.938909,282.01491 -2.1609004,299.10066 6.9251018,282.01491 Z" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="fast-forward-button.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect532"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1 @ F,0,0,1,0,3,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="6.178873"
inkscape:cx="60.366996"
inkscape:cy="51.142012"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#301749" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<path
style="fill:#ffffff;stroke:none;stroke-width:0.230245px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 1.2501756,275.70331 21.5452084,8.09338 a 0.75129316,0.75129316 90 0 1 0,1.40662 l -21.5452084,8.09338 a 0.89321676,0.89321676 45.345544 0 1 -1.14642092,-1.16033 l 1.73740222,-4.4609 a 8.0123873,8.0123873 90.752687 0 0 0.073815,-5.61856 l -1.86042651,-5.17483 a 0.92491885,0.92491885 135.40714 0 1 1.19563061,-1.17876 z"
id="path10"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:path-effect="#path-effect532"
inkscape:original-d="M -0.62208443,275 24.667644,284.5 -0.62208443,294 2.9299156,284.88 Z" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0.228452"
id="rect1880"
width="4"
height="19"
x="25.934584"
y="275"
ry="0.77077162" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="hamburger.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="4.3691229"
inkscape:cx="55.652941"
inkscape:cy="72.842748"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.25008985"
id="rect1197"
width="30"
height="4"
x="0"
y="293" />
<rect
y="280"
x="0"
height="4"
width="30"
id="rect1199"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.25008985" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.25008985"
id="rect1201"
width="30"
height="4"
x="0"
y="267" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="56mm"
height="32mm"
viewBox="0 0 56 32"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="hide-tag-sidebar.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 999.99997 : 0"
inkscape:vp_z="29.999999 : 12.5 : 1"
inkscape:persp3d-origin="14.999999 : 8.3333331 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="4.2656217"
inkscape:cx="84.137532"
inkscape:cy="66.883955"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1051"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-265)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-265)">
<rect
y="269"
x="4"
height="24"
width="48.02565"
id="rect1123"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.47469535" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-265)" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

96
public/static/kebaab.svg Normal file
View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="56mm"
height="32mm"
viewBox="0 0 56 32"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="kebaab.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 999.99997 : 0"
inkscape:vp_z="29.999999 : 12.5 : 1"
inkscape:persp3d-origin="14.999999 : 8.3333331 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="6.0325"
inkscape:cx="109.54192"
inkscape:cy="59.574627"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-265)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-265)">
<circle
style="fill:#ffffff;stroke-width:0.26458332"
id="path63"
cx="28"
cy="281"
r="5" />
<circle
r="5"
cy="281"
cx="11.5"
id="circle65"
style="fill:#ffffff;stroke-width:0.26458332" />
<circle
r="5"
cy="281"
cx="44.5"
id="circle67"
style="fill:#ffffff;stroke-width:0.26458332" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-265)" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="left-arrow.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="6.1788729"
inkscape:cx="36.57625"
inkscape:cy="43.69729"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#301749" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<path
style="fill:#ffffff;stroke:none;stroke-width:0.2990084px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 30,272 0,284.5 30,297 19.166806,285 Z"
id="path10"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="mini-close-button.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="6.0325"
inkscape:cx="77.217016"
inkscape:cy="52.943877"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<rect
y="187.47168"
x="190.12334"
height="2.6516504"
width="39.774757"
id="rect6171"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.22185987"
transform="rotate(44.999999)" />
<rect
transform="matrix(0.70253853,-0.71164571,0.77153858,0.63618254,0,0)"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.22282331"
id="rect15"
width="39.764065"
height="2.6754498"
x="-228.74762"
y="208.29031" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="minus-button-black.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="4.369123"
inkscape:cx="3.4995989"
inkscape:cy="67.097095"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.1739019"
id="rect6169"
width="18"
height="3.5999999"
x="6"
y="280.20001" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="minus-button.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="4.369123"
inkscape:cx="3.4995989"
inkscape:cy="67.097095"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.1739019"
id="rect6169"
width="18"
height="3.5999999"
x="6"
y="280.20001" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="open-tag-tree-arrow-black.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect672"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1 @ F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#260c3c"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="3.0894365"
inkscape:cx="9.0631414"
inkscape:cy="57.615685"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#260c3c" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<path
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.301057px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 29.889201,269.28098 -13.502037,26.94741 a 1.503154,1.503154 6.5138369e-6 0 1 -2.687792,0 L 0.19734415,269.28098 a 1.5948992,1.5948992 135.69515 0 1 2.17476745,-2.12262 l 10.0224004,5.32979 a 5.6413455,5.6413455 6.7423343e-6 0 0 5.297513,0 l 10.022409,-5.32979 a 1.5948992,1.5948992 44.304862 0 1 2.174767,2.12262 z"
id="path1203"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:path-effect="#path-effect672"
inkscape:original-d="m 32.129028,264.81073 -17.08576,34.09981 -17.0857503,-34.09981 17.0857503,9.086 z" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="open-tag-tree-arrow.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect672"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1 @ F,0,0,1,0,5,0,1 @ F,0,0,1,0,3,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#260c3c"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="3.0894365"
inkscape:cx="9.0631414"
inkscape:cy="57.615685"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#260c3c" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<path
style="fill:#b488f2;fill-opacity:1;stroke:none;stroke-width:0.301057px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 29.889201,269.28098 -13.502037,26.94741 a 1.503154,1.503154 6.5138369e-6 0 1 -2.687792,0 L 0.19734415,269.28098 a 1.5948992,1.5948992 135.69515 0 1 2.17476745,-2.12262 l 10.0224004,5.32979 a 5.6413455,5.6413455 6.7423343e-6 0 0 5.297513,0 l 10.022409,-5.32979 a 1.5948992,1.5948992 44.304862 0 1 2.174767,2.12262 z"
id="path1203"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:path-effect="#path-effect672"
inkscape:original-d="m 32.129028,264.81073 -17.08576,34.09981 -17.0857503,-34.09981 17.0857503,9.086 z" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="pause-button.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="5.6568543"
inkscape:cx="46.212259"
inkscape:cy="47.971796"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0.22845162"
id="rect1880"
width="4"
height="19"
x="-12"
y="275"
transform="scale(-1,1)" />
<rect
transform="scale(-1,1)"
y="275"
x="-22"
height="19"
width="4"
id="rect1920"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.22845162" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="play-button.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="5.6568543"
inkscape:cx="14.834396"
inkscape:cy="47.971796"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.31089458px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 6.7593067,273.3162 v 22.3676 L 23.240693,284.5 Z"
id="path1941"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="play-slideshow-button.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="47.700733"
inkscape:cy="22.882606"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.13181372px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 11.389751,277.75956 v 9.17784 l 7.220497,-4.58892 z"
id="path1941"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<rect
style="fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5697"
width="18.806969"
height="14.833746"
x="5.5965157"
y="274.59265" />
<path
style="fill:none;stroke:#f9f9f9;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 9.1843678,292.01338 -3.7885454,3.41437"
id="path5699"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#ffffff;stroke-width:1.99334741;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 14.996672,292.16728 0.0067,3.94189"
id="path5701"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path5705"
d="m 20.940093,292.07952 3.788546,3.41437"
style="fill:none;stroke:#f9f9f9;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="plus-button-black.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="3.01625"
inkscape:cx="-51.046"
inkscape:cy="31.827076"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<g
id="g6175"
style="fill:#000000"
transform="matrix(0.6,0,0,0.6,6,112.8)">
<rect
y="279"
x="4.4408921e-16"
height="6"
width="30"
id="rect6169"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.2898365" />
<rect
transform="rotate(90)"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.2898365"
id="rect6171"
width="30"
height="6"
x="267"
y="-18" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="plus-button.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="3.01625"
inkscape:cx="-51.046"
inkscape:cy="31.827076"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<g
id="g6175"
style="fill:#ffffff"
transform="matrix(0.6,0,0,0.6,6,112.8)">
<rect
y="279"
x="4.4408921e-16"
height="6"
width="30"
id="rect6169"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.2898365" />
<rect
transform="rotate(90)"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.2898365"
id="rect6171"
width="30"
height="6"
x="267"
y="-18" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="related-protected.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="30 : 12.5 : 1"
inkscape:persp3d-origin="15 : 8.3333334 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="4.369123"
inkscape:cx="50.353355"
inkscape:cy="61.110662"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#301749" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-267)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-267)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.173902"
id="rect6169"
width="18"
height="3.5999999"
x="13.63025"
y="262.27499" />
<circle
style="fill:#ffffff;stroke-width:0.351532;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:8;fill-opacity:1"
id="path344"
cx="15"
cy="282"
r="9.9737244" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-267)" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="rewind-button.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect532"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1 @ F,0,0,1,0,3,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="6.178873"
inkscape:cx="60.366996"
inkscape:cy="51.142012"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#301749" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<path
style="fill:#ffffff;stroke:none;stroke-width:0.230245px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 28.684408,275.70331 7.1392,283.79669 a 0.75129316,0.75129316 90 0 0 0,1.40662 l 21.545208,8.09338 a 0.89321676,0.89321676 134.65446 0 0 1.146421,-1.16033 l -1.737402,-4.4609 a 8.0123873,8.0123873 89.247313 0 1 -0.07381,-5.61856 l 1.860426,-5.17483 a 0.92491885,0.92491885 44.592856 0 0 -1.195631,-1.17876 z"
id="path10"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:path-effect="#path-effect532"
inkscape:original-d="m 30.556668,275 -25.289728,9.5 25.289728,9.5 -3.552,-9.12 z" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0.228452"
id="rect1880"
width="4"
height="19"
x="-4"
y="275"
transform="scale(-1,1)"
ry="0.77077162" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="right-arrow.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="6.1788729"
inkscape:cx="57.451211"
inkscape:cy="43.762759"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<path
style="fill:#ffffff;stroke:none;stroke-width:0.2990084px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 0,272 30,284.5 0,297 10.833194,285 Z"
id="path10"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="save-button.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="5.6568543"
inkscape:cx="46.212259"
inkscape:cy="47.971796"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<path
style="fill:#ffffff;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 7.9999998,275 v 19 L 22,285 Z"
id="path1941"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="56mm"
height="32mm"
viewBox="0 0 56 32"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="show-tag-sidebar.svg">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 12.5 : 1"
inkscape:vp_y="0 : 999.99997 : 0"
inkscape:vp_z="29.999999 : 12.5 : 1"
inkscape:persp3d-origin="14.999999 : 8.3333331 : 1"
id="perspective1181" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="4.2656217"
inkscape:cx="94.921423"
inkscape:cy="58.209956"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-265)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-265)">
<rect
y="269"
x="17"
height="24"
width="35"
id="rect1123"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.40524006" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0.20549424"
id="rect1125"
width="9"
height="24"
x="4"
y="269" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-265)" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="stop-button.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="5.6568543"
inkscape:cx="13.420182"
inkscape:cy="47.264689"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0.51083332"
id="rect1880"
width="20"
height="19"
x="-25"
y="275"
transform="scale(-1,1)" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30mm"
height="25mm"
viewBox="0 0 30 25"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="stop-slideshow-button.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#301749"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="1.4142136"
inkscape:cx="-177.4576"
inkscape:cy="78.271244"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
transform="translate(0,-272)" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-272)">
<rect
style="fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5697"
width="18.806969"
height="14.833746"
x="5.5965157"
y="274.59265" />
<path
style="fill:none;stroke:#f9f9f9;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 9.1843678,292.01338 -3.7885454,3.41437"
id="path5699"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#ffffff;stroke-width:1.99334741;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 14.996672,292.16728 0.0067,3.94189"
id="path5701"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path5705"
d="m 20.940093,292.07952 3.788546,3.41437"
style="fill:none;stroke:#f9f9f9;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5707"
width="8.7643232"
height="8.301302"
x="10.583333"
y="277.98306" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="display:inline"
transform="translate(0,-272)" />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,44 @@
import {parentPort} from 'node:worker_threads';
import sharp from 'sharp';
sharp.cache(false);
//Okay so it's not the "right" solution, because I really need to add
// a configuration to pack dependencies for the electron side of things...
// but I'm in a rush, and this works. It also works better than bindings, tbh,
// being it keeps trying to load the stubbed out debug module that isn't actually
// compiled...
//var nucleus = require('bindings')('binder_nucleus.node');
console.log("thumbnailer thread up and running");
if (!parentPort) {
throw new Error("parent port not defined in thumbnail worker");
}
///*
//parentPort.onmessage = (async (message) => {
parentPort.on('message', async (message) => {
//console.log("Oh what a wonderful day to be alive! Working on a new thumbnail :3cc");
const result = { succ: false }
const pair = message;
if (!("sourcePath" in pair) || !("destPath" in pair)) {
console.warn("Bad pair in thumbnail worker");
parentPort.postMessage(result);
return;
}
try {
await sharp(pair.sourcePath).resize({ width: 150 }).webp({ quality: 100 }).toFile(pair.destPath);
result.succ = true;
} catch (e) {
console.error("failed to generate thumbnail " + e);
}
parentPort.postMessage(result);
return;
});

237
src/App.css Normal file
View File

@ -0,0 +1,237 @@
body
{
user-select: none;
overflow: hidden;
background-color: #240b2e;
}
*:focus
{
outline: none;
}
::selection
{
background-color: rgb(101, 85, 173);
}
/*I don't want people selecting just anything
but I have no reason to prevent them from selecting actual paragraphs*/
p
{
user-select: text;
padding-right: 0px;
}
input
{
width: 75%;
height: 30px;
font-size: 20px;
margin-bottom: 10px;
}
button
{
border-width: 0px;
background-color: transparent;
}
::-webkit-scrollbar
{
width: 10px;
}
::-webkit-scrollbar-track
{
background-color: transparent;
}
::-webkit-scrollbar-thumb
{
background-color: #301749;
}
::-webkit-resizer
{
background-color: #45285f;
z-index: 20;
}
.app-root
{
display: grid;
grid-template-rows: auto; /*, content*/
height: 100vh;
max-height: 100vh;
background-color: #301749;
position: relative;
}
.second-level
{
flex: 1;
display: flex;
flex-direction: row;
width: 100%;
height: 100vh;
}
.left-sidebar
{
display: flex;
flex-direction: column;
flex-wrap: nowrap;
width:56px;
}
.user-sidebar
{
width: 300px;
height: 100%;
background-color: red;
}
.third-level
{
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.navi-bar
{
height: 35px;
background-color: green;
}
.toggle-tag-bar-button
{
background-color: orange;
}
.primary-content-area
{
display: flex;
flex-wrap: nowrap;
flex-direction: row;
overflow-y: auto; /*this makes the content area scrollable https://stackoverflow.com/a/35609992 don't ask me why*/
}
.open-binder-area
{
flex: 1;
display: flex;
flex-wrap: nowrap;
flex-direction: row;
overflow-y: auto;
}
.open-binder-area-hidden
{
display: none;
}
.open-binder-area
{
background-color: aquamarine;
}
.no-binders-container
{
display: flex;
flex-direction: column;
}
.no-binders-container-message
{
margin: 0px;
padding: 0px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: rgb(95, 63, 95);
background-color: #1E0E2E;
font-size: 50px;
}
.no-binders-container-message > button
{
margin-top: 20px;
padding: 10px;
color: white;
border-radius: 0px 10px 0px 10px;
background-color: #724596;
}
.no-binders-container-message > button:hover
{
background-color: #804ea8;
}
.no-binders-container-message > button:active
{
background-color: #6a3e8f;
}
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
button {
font-size: calc(10px + 2vmin);
}

222
src/App.tsx Normal file
View File

@ -0,0 +1,222 @@
import React, { useEffect, useState } from 'react'
import { BinderBar } from './components/binderbar';
import { Overlays } from './components/overlays';
import './App.css'
//import { NavBar } from './components/navbar'
import { useStatorContext } from './statorobject';
import { OpenBinders} from './components/openbinders'
import { ContextMenu } from './components/contextmenu';
import { TagSidebar, SidebarViewToggleButton} from './components/tagsidebar';
import { HamburgerMenu } from './components/hamburgermenu';
import { NagScreen } from './components/nagscreen';
import { getActivationStator } from './activation';
import { getUpdaterStator } from './updaterstator';
import { Notifications } from './components/notifications';
import {BuildInfo, getBuildInfo} from "./ipcbindings";
import {UploadController} from "./components/uploadcontroller";
import {localNodeConfig} from "./localnodeconfig"
import './components/navbar.css';
import { EulaScreen } from './components/eula';
//I gave up on trying to make rollup work correctly, just put electron-main dependencies here:
//import { v4 } from 'uuid';
//Layout is as such
//--------- notif area
//|########
//|########
//|########
//^ ^
//| L dynamicContentArea
//L mainsidebar
//Dynamic content area is broken up into two rows vertically
// navbar on top, content area on bottom.
function App()
{
const appContext = useStatorContext();
const [configLoaded, setConfigLoaded] = useState<boolean>(false);
const [eulaAgreed, setEulaAgreed] = useState<boolean>(false);
//activateWithKey("6AF6D30F-140B499E-A2E8EF92-D1A4D3E2");
/*
useEffect(() => {
return getRootStator(appContext).mount();
}, [1]);
*/
useEffect(() => {
getBuildInfo().then((val: BuildInfo) => {
document.title = val.appTitle;
});
}, [1]);
useEffect(() => {
if(localNodeConfig.checkLoadInitiated()) return;
(async () => {
await localNodeConfig.load();
setEulaAgreed(await localNodeConfig.checkEulaAgreement());
setConfigLoaded(true);
})();
}, []);
/*
//Initialize binder list
useEffect(() =>
{
allBindersEndpoint()
.then((freshBinders: Array<Binder>) =>
{
binderListStator.syncToBinderList(freshBinders);
//Normally it's the last in the list we want the first here
if (freshBinders.length)
binderListStator.setSelectedBinder(freshBinders[0].id);
})
.catch((msg: string) =>
{
console.log("Got error while trying to query binders: " + msg);
});
//No unregister, cause this is a lifetime object and should only run once
//except for vite, dumbass...
}, []);
*/
/*
useEffect(() => {
const id = setInterval(() => {
const binderListStator = getBinderListStator(appContext);
binderListStator.flushBinders();
}, 1000);
return () => {
clearInterval(id);
};
}, [appContext]);
*/
useEffect(() =>
{
//Very important this is done somewhere...
const activationStator = getActivationStator(appContext);
activationStator.initializeActivation();
//We want the updater to follow activation, cause otherwise
// it'll say something wrong like it's not valid for your key, so
// we need to authorize first and then the updater message will be gud.
const updateStator = getUpdaterStator(appContext);
activationStator.registerCallback((gen: number) =>
{
updateStator.checkForUpdates();
});
}, [appContext]);
const agreedammit = async (e) => {
e.preventDefault();
console.log("Agree! but skipped");
await localNodeConfig.setEulaAgreed();
setEulaAgreed(await localNodeConfig.checkEulaAgreement());
}
console.log("Eula agreed: " + eulaAgreed);
if(eulaAgreed && configLoaded) {
return (<div className="app-root">
<div className="second-level">
<div className="left-sidebar">
<HamburgerMenu
appContext={appContext}
buttonStyleClass="nav-button binder-bar-ham-burber-bubbon" />
<BinderBar appContext={appContext} />
<SidebarViewToggleButton appContext={appContext} />
</div>
<TagSidebar appContext={appContext} />
<div className="third-level">
<OpenBinders appContext={appContext} />
<Notifications appContext={appContext} />
</div>
</div>
<Overlays appContext={appContext} />
<NagScreen appContext={appContext} />
<UploadController appContext={appContext} />
<ContextMenu appContext={appContext} />
</div>);
} else if(configLoaded && !eulaAgreed) {
return (
<EulaScreen agreeCallback={agreedammit}/>
)
} else {
return (<div className="app-root"></div>);
}
}
export default App
/*
function App() {
const [count, setCount] = useState(.01)
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Hello Vite + React!</p>
<p>
<button type="button" onClick={() => setCount((count) => count * 2 )}>
count is: {count}
</button>
</p>
<p>
Edit <code>App.tsx</code> and save to test HMR updates.
</p>
<p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
{' | '}
<a
className="App-link"
href="https://vitejs.dev/guide/features.html"
target="_blank"
rel="noopener noreferrer"
>
Vite Docs
</a>
</p>
</header>
</div>
)
}
*/

215
src/activation.ts Normal file
View File

@ -0,0 +1,215 @@
import { StatorContext, StatorObjectBase } from "./statorobject";
import { getMetaApi, getServerEndpoint } from "./ipcbindings";
import { blake2b } from '@noble/hashes/blake2b';
import { bytesToHex } from '@noble/hashes/utils';
import { getNagStator } from "./components/nagscreen";
const ActivationFileName = "activation.json";
//numbers mirrored in activation.h on the C++ side
// and in the website's backend code
//Should there be a separate status for "pending"? As in, we're still *loading*
// whereas *pending" is for delays in talking to the online servers?
enum ActivationStatus
{
Unlicensed = 0,
Subscription = 1,
Perpetual = 2,
TamperTrip = 3,
Pending = 4,
Refunded = 5,
ChargedBacked = 6,
InvalidKey = 7,
ExpiredSub = 8,
ActivationError = 9,
}
async function getActivationFilePath() {
return await getMetaApi().joinPath(await getMetaApi().getConfigPath(), ActivationFileName);
}
class Activation extends StatorObjectBase
{
status = ActivationStatus.Pending;
licenseKey = "";
purchaserEmail = "sales@enesda.com";
updatesUntil: number = -3804840000000;
activationHash: string = "";
constructor(appContext: StatorContext) {
super(appContext);
}
async save() {
const data = {
status: this.status,
licenseKey: this.licenseKey,
purchaserEmail: this.purchaserEmail,
"updatesUntil": this.updatesUntil,
"activationHash": this.activationHash
};
const payload = JSON.stringify(data);
console.log("payload: " + payload);
const path = await getActivationFilePath();
const tempPath = path + ".temp";
await getMetaApi().writeFile(tempPath, payload, "utf8");
await getMetaApi().renamePath(tempPath, path);
}
calculateActivationHash() {
const secret = "uwu pwease dwont bweak mwe OWO I hwave bwiws two pway >.<";
//nodeId in here is kind of a bad idea... for one is it even loaded by this point? And for another...
// uh... you should be able to change it without borking everything lol
//It would also encourage people to reuse the machineconfig when they shouldn't because
// it's the only way to preserve an activation file...
//This is why you shouldn't second guess things you figured out already...
const str = `${this.status}${this.licenseKey}${this.purchaserEmail}${this.updatesUntil}${secret}`;
return `${bytesToHex(blake2b(str))}`;
}
async initializeActivation() {
const readed = await getMetaApi().readUtf8File(await getActivationFilePath());
if(readed.bytesRead) {
this.slurp(JSON.parse(readed.data), false);
const computedHash = this.calculateActivationHash();
if (computedHash != this.activationHash) {
console.log("computed hash: " + computedHash);
console.log("stored hash: " + this.activationHash);
console.log("tamper tripped! in Initialize!");
this.status = ActivationStatus.TamperTrip;
getNagStator(this.getContext()).forceShow();
}
//Retry on restart
if (this.status == ActivationStatus.Pending
|| this.status == ActivationStatus.ActivationError
|| this.status == ActivationStatus.Subscription) {
console.log("Phoning home to check license key...")
await this.activateWithKey(this.licenseKey);
}
} else {
//default values will suffice save for the status
this.status = ActivationStatus.Unlicensed;
}
this.publishChanges();
}
private reset()
{
this.status = ActivationStatus.Pending;
//this.licenseKey //we don't reset this! It's ANNOYING
this.purchaserEmail = "";
this.updatesUntil = 0;
}
shouldNag()
{
switch(this.status)
{
case ActivationStatus.Pending:
case ActivationStatus.Subscription:
case ActivationStatus.Perpetual:
case ActivationStatus.ActivationError: //This one is false cause it's an error on the backend.
return false;
case ActivationStatus.Unlicensed:
case ActivationStatus.TamperTrip:
case ActivationStatus.Refunded:
case ActivationStatus.ChargedBacked:
case ActivationStatus.InvalidKey:
case ActivationStatus.ExpiredSub:
return true;
}
}
private slurp(payload: any, rehash: boolean) {
try
{
this.status = Number(payload["status"]);
const kickedBackKEy = String(payload["licenseKey"]);
if (kickedBackKEy)//Cause the server doesn't send bad keys back and it's annoying
this.licenseKey = kickedBackKEy;
this.purchaserEmail = String(payload["purchaserEmail"]);
this.updatesUntil = Number(payload["updatesUntil"]);
if(rehash) {
this.activationHash = this.calculateActivationHash();
this.save().then();
} else {
this.activationHash = String(payload["activationHash"]);
}
}
catch(e: any)
{
console.warn("Failed to activate with payload");
console.log(e);
console.log(payload);
this.reset();
}
this.publishChanges();
}
async activateWithKey(key: string)
{
//If it a subscription that is already active, and the key is the same, then we're just
// double checking, otherwise we clear the activation status
if(this.status != ActivationStatus.Subscription || this.licenseKey != key) {
this.reset();
this.licenseKey = key;
if (!this.licenseKey)
this.status = ActivationStatus.Unlicensed;
await this.save();
this.publishChanges();
}
if(!this.licenseKey)
return;
const endpoint = (await getServerEndpoint()) + "/api/binder/v1/activate/" + key;
console.log("Phoning home: requesting activation information");
const request = new Request(endpoint, { method: "GET" });
const data = await(await fetch(request)).json();
console.log(data);
if (!data)
{
console.log("No response from activation servers");
//Not much we can do about that!
//But the thing is pending by default so that's fine w/e we don't wanna nag if we don't
// have our shit together
return;
}
this.slurp(data, true);
}
}
const ActivationStatorKey = "ActivationStator";
function useWatchActivationStator(appContext: StatorContext)
{
return appContext.useWatchStatorLegacy(ActivationStatorKey, new Activation(appContext)) as Activation;
}
function getActivationStator(appContext: StatorContext)
{
return appContext.getStatorLegacy(ActivationStatorKey, new Activation(appContext)) as Activation;
}
export { Activation, useWatchActivationStator, getActivationStator, ActivationStatus }

541
src/backend.ts Normal file
View File

@ -0,0 +1,541 @@
import { Activation } from './activation';
import { Binder } from './binder';
import {backendEntrypoint} from './ipcbindings'
type BinderConnectionId = string;
type TagId = string;
type ObjectId = string;
class BinderTag
{
name: string = "";
id: TagId = "";
children: Array<BinderTag> = [];
/*
clone() {
const result = new BinderTag();
result.name = this.name;
result.id = this.id;
for(const child of this.children)
result.children.push(child.clone());
return result;
}
*/
}
class IndexedObject {
id: ObjectId = "";
index: number = 0;
}
class BinderObject
{
id: ObjectId = "";
b2bsum: string = "";
index: number = 0;
mime: string = "";
objectOnDiskPath: string = "";
thumbnailOnDiskPath: string = "";
//Since object -> tag is a generally a smaller set
// than tag -> object, we store the relationship here, to avoid
// loading potentially hundreds of thousands of objects per tag.
//Each object has to be loaded, period, and it's tags are already loaded,
// so yeh.
tags = new Set<TagId>();
/*
clone() {
const result = new BinderObject();
result.id = this.id;
result.imageDisplayURI = this.imageDisplayURI;
result.videoDisplayURI = this.videoDisplayURI;
return result;
}
*/
}
// we can do caching in here
// so like, we can fetch object lists from the database, but then
// if we need them again just memoize, and if we need the intersection
// of two, again, just do that here.
//In fact maybe just don't even have the multi-tag select in the backend at all.
// the "objectsForTagsEndpoint" below accepts a list, but the backend need not.
//Uh oh, need a "binderId"... Which we don't have in here oopsies.
//const binderApiUrl = "http://localhost:18080/api/binder/";
interface CommonResponse
{
statusCode: number;
userErrorMessage: string;
}
interface ObjectsForTagResponse extends CommonResponse
{
objects: Array<BinderObject>;
}
async function commonFetch<Type extends CommonResponse>(endpoint: string, requestBody: any, abortSig?: AbortSignal): Promise<Type>
{
/*
const authKey = await getAuthKey();
const options: RequestInit = {
method: "POST",
body: requestBody,
headers: {"Content-Type": "application/json", "auth-key": authKey}
};
if(abortSig)
options.signal = abortSig;
const request = new Request(binderApiUrl + endpoint, options);
const response = await fetch(request);
*/
const startTagRequest = Date.now();
const response = await backendEntrypoint(endpoint, requestBody);
const tagRequestTime = Date.now() - startTagRequest;
console.log("request time (" + endpoint + "): " + tagRequestTime);
let result: Type; //LET. JASON. TYPE.
try
{
const startTagRequest = Date.now();
result = JSON.parse(response);
const tagRequestTime = Date.now() - startTagRequest;
console.log("parse time (" + endpoint + "): " + tagRequestTime);
}
catch
{
console.log("Response not json!");
console.log(response);
throw "failed to process request";
}
if(result.statusCode != 200)
{
console.log("Statuscode not 200! is: " + result.statusCode);
console.log("server message is: " + result.userErrorMessage);
throw result.userErrorMessage; //doesn't fetch throw?
}
return result;
}
interface ActivationStatusResponse extends CommonResponse
{
activation: any;
};
async function queryActivationStatus(): Promise<Activation>
{
const requestBody = ({});
const respbody = (await commonFetch<ActivationStatusResponse>("activationstatus", {})).activation;
return respbody;
}
async function saveActivation(payload: Activation)
{
await commonFetch("activate", (payload));
}
interface ConnectToBinderResponse extends CommonResponse
{
result: Binder;
}
async function createBinderEndpoint(name: string, directory: string): Promise<Binder>
{
const requestBody = ({
"clusterParentDirectory": directory,
"clusterName": name
});
return (await commonFetch<ConnectToBinderResponse>("createbindercluster", requestBody)).result;
}
async function disconnectFromBinderEndpoint(binderId: BinderConnectionId)
{
throw new Error("Don't use this funciton!!!");
const requestBody = ({
binderId: binderId
});
await commonFetch<CommonResponse>("disconnectfrombinder", requestBody);
return;
}
async function connectToExistingEndpoint(binderPath: string): Promise<Binder>
{
const requestBody = ({
"clusterPath": binderPath,
});
return (await commonFetch<ConnectToBinderResponse>("connecttoexisting", requestBody)).result;
}
interface AllBindersResponse extends CommonResponse
{
binders: Array<Binder>;
}
async function allBindersEndpoint(): Promise<Array<Binder>>
{
return (await commonFetch<AllBindersResponse>("allbinders", {})).binders;
}
interface AllTagsResponse extends CommonResponse
{
tags: Array<BinderTag>;
}
async function allTagsEndpoint(theBinderId: BinderConnectionId): Promise<Array<BinderTag>>
{
const requestBody = ({"binderId": theBinderId})
return (await commonFetch<AllTagsResponse>("alltags", requestBody)).tags
/*
if(theBinderId == ":3c")
{
return [
{ name: "whee", id: "1234", children: [] },
{ name: "hee", id: "234", children: [] },
{ name: "whee", id: "4", children: [{ name: "wheeeeeeeeekkkkkkkkkkkkkkkkk", id: "5", children: [{ name: ":3", id: "5555", children: [] }] }] },
{ name: "blee!!", id: "34", children: [] },
];
}
if(theBinderId == ":fjidfj3c")
{
return [
{ name: "OwO", id: "fjf38383390278j", children: [] },
{ name: "OwO 2", id: "fjf38383390278j88", children: [] },
{ name: "Owo", id: "fjf38383390278jjf", children: [] }
]
}
if(theBinderId == ":fjidfj3cjfuhudhf")
{
//This one uses the same ids as above to check against spooky action at a distance
return ([
{ name: "uwu", id: "fjf38383390278j", children: [] },
{ name: "uwu 2", id: "fjf38383390278j88", children: [] },
{ name: "uwu 3", id: "fjf38383390278jjf", children: [] }
]);
}
return [];
// */
}
async function objectsForTagEndpoint(theBinderId: BinderConnectionId,
tags: Array<TagId>,
sig: AbortSignal)
{
const requestBody = (
{
binderId: theBinderId,
tags: tags //the problem is the api wants a single tag but we have all these damn tags...
//The million dollar question is, do we do the intersection here or on the backend?
})
return (await commonFetch<ObjectsForTagResponse>("objectsontags", requestBody, sig)).objects;
/*
const result = new Array<BinderObject>();
const tagSet1 = new Set<TagId>(["1234"]);
const tagSet2 = new Set<TagId>(["1234"]);
const tagSet3 = new Set<TagId>(["1234"]);
//779 objects
let alternator: number = 0;
for(let i: number = 0; i != 790; ++i) {
let tagSet = new Set<TagId>();
if(alternator == 0) tagSet = new Set(tagSet1);
else if(alternator == 1) tagSet = new Set(tagSet2);
else if(alternator == 2) tagSet = new Set(tagSet3);
else throw Error("fuck it");
if(alternator == 2) {
alternator = 0;
} else { alternator = alternator + 1; }
result.push({id: i.toString(), imageDisplayURI: "", videoDisplayURI: "", tags: tagSet})
}
return result;
*/
}
//Uses common response
//Uses common response
async function internalModifyTagRelationship(endpoint: string,
theBinderId: BinderConnectionId,
theTag: TagId,
theObjects: Array<ObjectId>)
{
const requestBody = (
{
binderId: theBinderId,
tagId: theTag,
objects: theObjects,
}
);
return (await commonFetch<CommonResponse>(endpoint, requestBody));
}
async function addTagToObjects(theBinderId: BinderConnectionId,
theTag: TagId,
theObjects: Array<ObjectId>)
{
return (await internalModifyTagRelationship("addtagtoobjects",
theBinderId,
theTag,
theObjects));
}
//removetagfromobjects
async function removeTagFromObjects(theBinderId: BinderConnectionId,
theTag: TagId,
theObjects: Array<ObjectId>)
{
return (await internalModifyTagRelationship("removetagfromobjects",
theBinderId,
theTag,
theObjects));
}
interface NewTagResponse extends CommonResponse
{
tagId: TagId;
}
async function createTagEndpoint(theBinderId: BinderConnectionId,
theName: string,
tagParent: TagId): Promise<TagId>
{
const requestBody = (
{
binderId: theBinderId,
tagName: theName,
tagParentId: tagParent
});
return (await commonFetch<NewTagResponse>("createtag", requestBody)).tagId;
}
async function renameTagEndpoint(theBinderId: BinderConnectionId,
tagId: TagId,
theNewName: string)
{
const requestBody = (
{
binderId: theBinderId,
tagId: tagId,
newName: theNewName,
}
);
return (await commonFetch<CommonResponse>("settagname", requestBody));
}
//settagparent
async function setTagParent(theBinderId: BinderConnectionId,
tag: TagId,
parentToBe: TagId)
{
const requestBody = (
{
binderId: theBinderId,
tagId: tag,
newParentTagId: parentToBe
}
)
return (await commonFetch<CommonResponse>("settagparent", requestBody))
}
export enum Position
{
Before,
After,
}
async function repositionTag(theBinderId: BinderConnectionId,
tag: TagId,
tagOther: TagId,
thePosition: Position)
{
const posString = thePosition == Position.Before ? "before" : "after";
const requestBody = (
{
binderId: theBinderId,
tagId: tag,
otherTagId: tagOther,
position: posString,
}
)
return (await commonFetch<CommonResponse>("repositiontag", requestBody));
}
async function deleteTag(theBinderId: BinderConnectionId, tag: TagId)
{
const requestBody = (
{
binderId: theBinderId,
tagId: tag
}
);
return (await commonFetch<CommonResponse>("deletetag", requestBody));
}
interface CreateObjectResponse extends CommonResponse
{
skipped: Array<BinderObject>;
forcedDuplicates: Array<BinderObject>;
taggedDuplicates: Array<BinderObject>;
objects: Array<BinderObject>;
}
async function createObject(theBinderId: BinderConnectionId,
baseTag: TagId,
additionalTags: Array<TagId>,
theFiles: Array<string>)
{
const requestBody = (
{
binderId: theBinderId,
duplicateBehavior: "tagDuplicate",
baseTag: baseTag,
tags: additionalTags,
paths: theFiles
});
return await commonFetch<CreateObjectResponse>("addfiles", requestBody);
}
interface ExportObjectResponse extends CommonResponse
{
}
async function exportObject(theBinderId: BinderConnectionId,
dest: string,
objectsByB2Sum: Array<string>)
{
const requestBody = (
{
binderId: theBinderId,
dest: dest,
objectsB2: objectsByB2Sum,
});
return await commonFetch<ExportObjectResponse>("exportobjects", requestBody);
}
async function generateThumbnailForObject(theBinderId: BinderConnectionId,
b2sum: string)
{
const requestBody = (
{
binderId: theBinderId,
b2sum: b2sum,
});
return await commonFetch<CommonResponse>("generatestandardobjectthumbnail", requestBody);
}
async function syncCluster(theBinderId: BinderConnectionId)
{
const requestBody = (
{
binderId: theBinderId,
}
);
return await commonFetch<CommonResponse>("synccluster", requestBody);
}
function getThumbnailUri(obj: ObjectId) {
console.warn("REimpliment getThumbanilUrls, dubmadss");
return "";
//*if electron TODO
//return "file:///" + obj.thumbnailOnDiskPath;
//*else
//return binderApiUrl + "standardobjectthumbnail" + "/" + theBinderId + "/" + b2bsum;
}
function getObjectUri(obj: ObjectId) {
//*if electron TODO
console.warn("REimpliment getObjectURI, dubmadss");
return "";
//return "file:///" + obj.objectOnDiskPath;
//*else
// return binderApiUrl + "image" + "/" + theBinderId + "/" + b2bsum;
}
export {
Binder,
BinderTag,
BinderObject,
getObjectUri,
getThumbnailUri,
queryActivationStatus,
saveActivation,
createBinderEndpoint,
disconnectFromBinderEndpoint,
connectToExistingEndpoint,
allBindersEndpoint,
generateThumbnailForObject,
};
export type {
BinderConnectionId,
TagId,
ObjectId,
IndexedObject,
};

40
src/binder.ts Normal file
View File

@ -0,0 +1,40 @@
import { BinderConnectionId } from "./backend";
import { BinderContext } from "./components/bindercontext";
import { getMetaApi } from "./ipcbindings";
import { BinderIdFilename, localNodeConfig } from "./localnodeconfig";
import { v4 } from 'uuid';
class Binder
{
name: string = "";
id: BinderConnectionId = "";
exists: boolean = false;
clusterPath: string = ""; //deprecated, same as binderId now
context: BinderContext;
constructor(context: BinderContext) {
this.context = context;
}
static async createBinder(location: string, name: string) {
const path = await getMetaApi().joinPath(location, name);
if(await localNodeConfig.checkBinderExists(path)) {
return "Binder already exists!";
/*
console.warn("Binder already exists, skipping create for: " + path);
return;
*/
}
await getMetaApi().createDirectories(path);
const binderIdFilePath = await getMetaApi().joinPath(path, BinderIdFilename);
await getMetaApi().writeFile(binderIdFilePath, v4(), "utf8");
await localNodeConfig.registerCluster(path);
return "";
}
}
export {Binder}

29
src/buildInfo.js Normal file
View File

@ -0,0 +1,29 @@
import fs from 'node:fs';
import path from 'node:path';
import {app} from 'electron';
const devBuildInfo = {
"branch": "~DEVELOPMENT NOT FOR REDISTRIBUTION~",
"appTitle": "~BINDER - TEST - NOT FOR RELEASE~",
"buildTimestamp": -380484000000,
"version": "reee.eee.eee.eee",
}
function getBuildInfo(devMode = true) {
const buildInfoPath = path.join(app.getAppPath(), "buildInfo.json");
try {
const readData = fs.readFileSync(buildInfoPath, 'utf8');
const dynamicBuildInfo = JSON.parse(readData);
if (devMode) {
dynamicBuildInfo.appTitle += "-test";
}
return dynamicBuildInfo;
} catch(e) {
console.log("failed to load buildinfo " + e);
}
}
export {getBuildInfo}

View File

@ -0,0 +1,43 @@
.about-overlay-container
{
padding: 0px;
margin: 0px;
overflow-y: scroll;
}
.about-overlay-text-container
{
display: flex;
flex-direction: column;
user-select:text;
padding: 30px;
}
.about-overlay-text-container > p
{
margin: 0px;
color: white;
line-height: 1.5;
}
.about-overlay-text-container > h1
{
margin-top: 0px;
margin-left: auto;
margin-right: auto;
font-size: 70px;
font-family: Arial, Helvetica, sans-serif;
font-weight: 900;
align-items: center;
color: white;
}
.about-overlay-text-container > h2
{
margin-top: 15px;
margin-bottom: 10px;
font-size: 50px;
color: white;
font-family: Arial, Helvetica, sans-serif;
font-weight: 900;
}

View File

@ -0,0 +1,287 @@
import React, {useState, useEffect} from 'react';
import './aboutoverlay.css';
import { StatorContext } from '../statorobject';
import { GenericOverlayWrapper } from './genericoverlay';
import {handleGotoEnesdaPage, openUrl, getBuildInfo, BuildInfo} from "../ipcbindings";
import {readLicense} from './eula';
interface AboutOverlayProps
{
appContext: StatorContext;
}
interface DetailsOfContribProps
{
title: string;
link: string;
terms: string;
}
function DetailsOfContrib(props: DetailsOfContribProps)
{
const gotoThePlace = (e: any) =>
{
openUrl(props.link);
};
return (<>
<h2>{props.title}</h2>
<p><a href="#" onClick={gotoThePlace}>Project Homepage</a></p>
<br/>
<details>
<summary>License</summary>
<span>
{props.terms}
</span>
</details>
</>);
}
function AboutOverlayImpl(props: AboutOverlayProps)
{
const [buildInfo, setBuildInfo] = useState<BuildInfo>({buildTimestamp: -380484000000, branch: "...", appTitle: "Binder...", version: "...", CommitSHA: "7"});
const [binderLicenseText, setBinderLicenseText] = useState("Loading...");
const gotoEnesda = (e: any) =>
{
handleGotoEnesdaPage('binder');
};
useEffect(() => {
getBuildInfo().then((val: BuildInfo) => {
setBuildInfo(val);
});
readLicense().then((licenseText: string) => {
setBinderLicenseText(licenseText);
});
}, [1]);
const dateString = (() => {
if(!buildInfo) return "././.";
const dateObject = new Date(buildInfo.buildTimestamp);
return dateObject.toLocaleDateString() + " @ " + dateObject.toLocaleTimeString();
})();
return (
<div>
<h1>Binder</h1>
<p><a href={"#"} onClick={gotoEnesda}>Product Homepage</a></p>
<p>Version: {buildInfo.version}</p>
<p>Built: {dateString}</p>
<p>Commit SHA: {buildInfo.CommitSHA}</p>
<br />
<details>
<summary>License</summary>
<span> {"\n" + binderLicenseText} </span>
</details>
<br/>
<p>Binder uses the following technologies:</p>
<DetailsOfContrib title="React"
link={"https://reactjs.org/"}
terms={`
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`}/>
{
/*
<h2>PDF.js</h2>
*/
}
<DetailsOfContrib title={"Electron"} link={"https://www.electronjs.org/"} terms={`
Copyright (c) Electron contributors
Copyright (c) 2013-2020 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`}/>
<DetailsOfContrib title={"sharp"} link={"https://sharp.pixelplumbing.com/"} terms={`
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
`}/>
<DetailsOfContrib title={"Squirrel.Windows"} link={"https://github.com/Squirrel/Squirrel.Windows"} terms={`
Copyright (c) 2012 GitHub, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`}/>
{
/*
<DetailsOfContrib title={'RapidXml'} link={'https://rapidxml.sourceforge.net/'} terms={`
Copyright (c) 2006, 2007 Marcin Kalicinski
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`}/>
*/
}
<DetailsOfContrib title={"noble-hashes"} link={"https://github.com/paulmillr/noble-hashes"} terms={`
The MIT License (MIT)
Copyright (c) 2022 Paul Miller (https://paulmillr.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`}/>
<DetailsOfContrib title={'uuid'} link={"https://github.com/uuidjs/uuid"} terms={`
The MIT License (MIT)
Copyright (c) 2010-2020 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`}/>
<DetailsOfContrib title={"proper-lockfile"} link={"https://github.com/moxystudio/node-proper-lockfile"} terms={`
The MIT License (MIT)
Copyright (c) 2018 Made With MOXY Lda <hello@moxy.studio>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`}/>
<DetailsOfContrib title={"WASMagic"} link={"https://github.com/moshen/wasmagic"} terms={`
BSD-2-Clause
Copyright 2021 AUTHORS
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
`}/>
</div>);
}
function AboutOverlay(props: AboutOverlayProps)
{
return (<GenericOverlayWrapper
appContext={props.appContext}
startTab={0}
tabs={
[
{
title: "About",
nestedComponent: AboutOverlayImpl,
nestedProps: props
}
]
}
/>);
}
export { AboutOverlay }

View File

View File

@ -0,0 +1,124 @@
import React, {useState} from 'react';
import { StatorContext } from '../statorobject';
import { getOverlayStator } from './overlays';
import { getNagStator } from './nagscreen';
import { GenericOverlayWrapper } from './genericoverlay';
import { ActivationStatus, useWatchActivationStator } from '../activation';
interface ActivationOverlayProps
{
appContext: StatorContext;
}
function ActivationOverlayImpl(props: ActivationOverlayProps)
{
const activationStator = useWatchActivationStator(props.appContext);
const standinValue = "Enter License Key";
const storedKey = activationStator.licenseKey;
const initialKeyVal = storedKey? storedKey : standinValue;
const [compKey, setCompKey] = useState(initialKeyVal);
const onChangeKey = (e: any) =>
{
setCompKey(e.target.value);
};
const gotoPurchaseOptions = (e: any) =>
{
getOverlayStator(props.appContext).closeOverlays();
getNagStator(props.appContext).forceShow();
};
const handleSubmit = (e: any) =>
{
e.preventDefault();
activationStator.activateWithKey(compKey);
};
const handleClick = (e: any) =>
{
e.preventDefault();
if(compKey == standinValue)
{
setCompKey("");
}
};
const Purchase = () => { return <a href="#" onClick={gotoPurchaseOptions}>purchase a license</a>};
const statusMessage = (() =>
{
if(activationStator == undefined || activationStator.status == ActivationStatus.Pending)
{
return <p>Validation in progress, please check again in a bit.</p>
}
switch (activationStator.status)
{
case ActivationStatus.Unlicensed:
return (
<p>This copy of binder is unlicensed and for evaluation purposes only.
Please enter a license key below or explore <a href="#" onClick={gotoPurchaseOptions}>purchase options.</a></p>
);
case ActivationStatus.Subscription:
case ActivationStatus.Perpetual:
{
return (<div>
<h2>Activated!</h2>
<p>Email: {activationStator.purchaserEmail}</p>
{activationStator.status == ActivationStatus.Perpetual?
<p>Complimentary updates until: {(new Date(activationStator.updatesUntil)).toLocaleDateString()}</p>
:
<></>
}
</div>);
}
case ActivationStatus.TamperTrip:
return (<p>Activation tamper system was tripped. Either re-enter your key below or <Purchase/> to continue using Binder.</p>);
/* This is redundant, and for some reason that's a goddamn error to typescript so w/e.
case ActivationStatus.Pending:
return (<p>Activation is pending, please check back in a bit.</p>);
*/
case ActivationStatus.Refunded:
return (<p>Your purchase was refunded and Binder is now unlicensed. Please <Purchase/> to continue using Binder</p>);
case ActivationStatus.ChargedBacked:
return (<p>A charge back was issued against your purchase and Binder is now unlicensed. Please <Purchase/> to continue using Binder</p>);
case ActivationStatus.ActivationError:
return <p>There was an error activating, please try again later.</p>
case ActivationStatus.InvalidKey:
return <p>Your key is invalid. Try again or <a href="#" onClick={gotoPurchaseOptions}>explore purchase options.</a></p>
case ActivationStatus.ExpiredSub:
return <p>Your subscription has expired. <a href="#" onClick={gotoPurchaseOptions}>Explore purchase options.</a></p>
}
})();
return (
<div>
<h1>Activation</h1>
{statusMessage}
<form onSubmit={handleSubmit} spellCheck={false}>
<input type="text" onClick={handleClick} name={"licenseKey"} value={compKey} onChange={onChangeKey} autoComplete="off" />
<input type="submit" className={"generic-overlay-form-button"} value={"Activate"} disabled={false}/>
</form>
</div>);
}
function ActivationOverlay(props: ActivationOverlayProps)
{
return (<GenericOverlayWrapper
appContext={props.appContext}
startTab={0}
tabs={[{
title: "Activate", /* Unused cause one tab but good practice nonethelss */
nestedComponent: ActivationOverlayImpl,
nestedProps: props,
}]}
/>);
}
export { ActivationOverlay }

View File

@ -0,0 +1,67 @@
.binder-bar
{
flex: 1;
display: flex;
flex-direction: column;
background-color: #301749;
overflow-y: scroll;
overflow-x: hidden;
align-items: center;
border-width: 0px;
}
.binder-bar::-webkit-scrollbar
{
width: 0px;
}
.binder-bar-ham-burber-bubbon
{
width: 40px;
}
.binder-bar-button-container
{
width: 56px;
}
.binder-bar-button
{
margin-top: 8px;
margin-left: 8px;
margin-right: 8px;
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #EADE83;
}
.binder-bar-new-binder-button
{
display: flex;
justify-content: center;
border-radius: 20px;
background-color: #573575;
color: white;
}
.binder-bar-button-current
{
border-radius: 8px;
}
.binder-bar-button-container:hover .binder-bar-new-binder-button
{
background-color: #462a5f;
}
.binder-bar-button-container:active .binder-bar-new-binder-button
{
background-color: #351b50;
}
.binder-bar-new-binder-button-icon
{
width: 23px;
}

View File

@ -0,0 +1,112 @@
import React from 'react'
import { Binder } from '../backend'
import { StatorContext } from '../statorobject';
import { getBinderListStator, useWatchBinderListStator } from './openbinders';
import './binderbar.css'
import './hamburgermenu.css'
import './vibrantactionbutton.css'
import { getOverlayStator, OverlayStatorStates } from './overlays';
import { ContextMenuData, getContextMenuStator } from './contextmenu';
import { localNodeConfig } from '../localnodeconfig';
interface BinderBarProps
{
appContext: StatorContext;
};
interface BinderButtonProps
{
myBinder: Binder;
current: Boolean;
appContext: StatorContext;
};
function BinderButton(props: BinderButtonProps)
{
const contextMenuHandler = (e: React.MouseEvent) => {
const menu: ContextMenuData = {
spawnX: e.pageX,
spawnY: e.pageY,
contextMenuSections: [ {
contextMenuItems: [ {
title: "Disconnect from: " + props.myBinder.name,
keyBinding: "",
action: () => {
localNodeConfig.removecluster(props.myBinder.id);
//disconnectFromBinderEndpoint(props.myBinder.id);
//getBinderListStator(props.appContext).removeBinderById(props.myBinder.id);
}
},
]
}
]
}
e.preventDefault();
getContextMenuStator(props.appContext).spawn(menu);
};
const selectThisBinderHandler = (e: React.MouseEvent) =>
{
e.preventDefault();
getBinderListStator(props.appContext).setSelectedBinder(props.myBinder.id);
}
return (
<div className={"binder-bar-button-container"}
onContextMenu={contextMenuHandler}
onClick={selectThisBinderHandler}
>
<div
className={`binder-bar-button ${props.current ? "binder-bar-button-current" : ""}`}
>
</div>
</div>
);
}
function BinderBar(props: BinderBarProps)
{
//todo Need "collapse taglist" button at the bottom
// but only if binderlist.length != 0
const binderListStator = useWatchBinderListStator(props.appContext);
const binderButtons = binderListStator.getOpenBinderList().map((pair) => {
return (
<BinderButton
key={pair.binder.id}
myBinder={pair.binder}
current = {binderListStator.checkSelected(pair.binder.id)}
appContext={props.appContext}/>
);
})
const launchNewBinderOverlay = () =>
{
const overlayStator = getOverlayStator(props.appContext);
overlayStator.set(OverlayStatorStates.showNewBinder);
}
return (
<div className="binder-bar">
{binderButtons}
<div className={"binder-bar-button-container"}
onClick={launchNewBinderOverlay}
>
<div className="binder-bar-button binder-bar-new-binder-button">
<img className="binder-bar-new-binder-button-icon"
src={"static/plus-button.svg"}
draggable={false}
/>
</div>
</div>
</div>
);
}
export {BinderBar};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
.center-overlay
{
display: flex;
flex-direction: column;
position: fixed;
/*This is offset by an odd number instead of 250 cause I don't like the perfect alignment
I got with the second row of thumbnails
maybe make it perfectly half-way instead?*/
top: 15vh;
left: 50%;
width: 640px;
margin-left: calc(-640px / 2);
padding: 0px;
background-color: #3B2452;
border-radius: 5px;
max-height: 70vh;
overflow: hidden;
z-index: 100;
box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.75);
}

View File

@ -0,0 +1,51 @@
.menu
{
display: flex;
flex-direction: column;
position: fixed;
width: 250px;
z-index: 10;
background-color: white;
/*border-radius: 0px 10px 0px 10px;*/
font-size: 15px;
justify-items: left;
padding: 4px;
border-color: black;
border-width: 1px;
border-style:solid;
box-shadow: 0px 0px 20px black;
}
.menu-hidden
{
display: none;
}
.menu > hr
{
border: none;
border-top: 1px solid rgb(175, 175, 175);
width: 250px;
margin-top: 4px;
margin-bottom: 4px;
}
.menu-item
{
text-align: left;
font-size: 15px;
padding: 8px;
color: black;
}
.menu-item:hover
{
background-color: rgb(212, 212, 212);
}
.menu-item:active
{
background-color: rgb(187, 187, 187);
}

View File

@ -0,0 +1,175 @@
import React, { useLayoutEffect, useState} from 'react'
import './contextmenu.css'
import { useCloseableOverlay } from './useCloseableOverlay';
import { StatorObjectBase, StatorContext } from '../statorobject';
interface ContextMenuItem
{
title: string;
keyBinding: string;
action: () => void;
}
interface ContextMenuSection
{
contextMenuItems: Array<ContextMenuItem>;
}
export class ContextMenuData
{
spawnX: number = 0;
spawnY: number = 0;
contextMenuSections = new Array<ContextMenuSection>();
}
class ContextMenuStator extends StatorObjectBase
{
show = false;
private myContextMenuData = new ContextMenuData();
spawn(data: ContextMenuData)
{
//spawnContextMenu(data);
//return;
this.myContextMenuData = data;
this.show = true;
this.publishChanges();
}
close()
{
this.show = false;
this.publishChanges();
}
getData()
{
return this.myContextMenuData
}
}
const ContextMenuStatorKey = "ContextMenuStator";
function getContextMenuStator(context: StatorContext)
{
return context.getStatorLegacy(ContextMenuStatorKey, new ContextMenuStator(context)) as ContextMenuStator;
}
interface ContextMenuProps
{
appContext: StatorContext;
}
interface Dimens
{
width: number;
height: number;
}
function ContextMenu(props: ContextMenuProps)
{
const myStator = props.appContext.useWatchStatorLegacy(ContextMenuStatorKey, new ContextMenuStator(props.appContext));
const myData = myStator.getData();
//Make sure to display none until we get a ref to set the location correctly
const [myDimens, setMyDimens] = useState<Dimens>({width: 0, height: 0});
useLayoutEffect(() =>
{
if(!myRef.current)
{
return;
}
const width = myRef.current.getBoundingClientRect().width;
const height = myRef.current.getBoundingClientRect().height;
setMyDimens({
width: width,
height: height
});
}, [myData.spawnX, myData.spawnY, myData.contextMenuSections]);
const closeHandler = () =>
{
myStator.close();
}
const myRef = useCloseableOverlay(closeHandler);
if(!myStator.show)
return null;
//This gets a warning because of keys in a list but they're not possible to rearrange...
//It's bitching because of the <hr> tags...
//But no option to supress that warning... Thanks react
let idIndex = 0;
const content = myData.contextMenuSections.map((section: ContextMenuSection, index: number) =>
{
const result = section.contextMenuItems.map((item: ContextMenuItem) =>
{
const decoratedAction = () =>
{
closeHandler();
item.action();
}
return (
<button className="menu-item"
onClick={decoratedAction}
key={idIndex++}
>
{item.title}
</button>
);
});
const sectionBreak = <hr key={idIndex++}/>;
if(index != 0)
return [sectionBreak, ...result];
return result;
});
//console.log("Spawn x, y: " + props.spawnX + ", " + props.spawnY);
//Gotta do it this way cause the ref is invalid at first
let myStyle: React.CSSProperties = {};
const upperBoundX = myData.spawnX + myDimens.width;
if (upperBoundX > window.innerWidth)
myStyle.left = myData.spawnX - (upperBoundX - window.innerWidth);
else
myStyle.left = myData.spawnX;
//if(!myData.spawnX)
// console.log("NOT!");
const upperBoundY = myData.spawnY + myDimens.height;
//console.log("Spawny: " + myData.spawnY);
//console.log("Height: " + height);
//console.log("Uppder bound y: " + upperBoundY + ", window.innerHeight: " + window.innerHeight);
if (upperBoundY > window.innerHeight)
myStyle.top = myData.spawnY - (upperBoundY - window.innerHeight);
else
myStyle.top = myData.spawnY;
return (
<div className={`hamburger-menu menu`}
ref={myRef}
style={myStyle}
>
{content}
</div>
);
}
export {ContextMenu, getContextMenuStator };

37
src/components/eula.css Normal file
View File

@ -0,0 +1,37 @@
.eula-screen {
display: flex;
flex-direction: column;
height: 100vh;
}
.eulaText {
margin: 30px;
margin-bottom: 0px;
padding: 20px;
overflow-y: scroll;
overflow-x: hidden;
background-color: rgb(218, 186, 216);
}
.eulaText::-webkit-scrollbar-thumb {
background-color: rgb(138, 102, 135);
}
.eula-screen > form {
}
.eula-screen input {
margin: 30px;
}
.eulaText > p {
}
br {
margin: 0px;
padding: 0px;
height: 100px;
border: 0px;
background-color: orange;
}

77
src/components/eula.tsx Normal file
View File

@ -0,0 +1,77 @@
import React, {useState, useEffect} from 'react';
import {getMetaApi} from "../ipcbindings";
import './eula.css'
//The part before the dash is the license with it's version, after the dash
// is any revisions made to the changeover date, changeover license, etc.
interface EulaScreenProps {
agreeCallback: (e: any) => void;
}
async function takeLicenseChecksum() {
const lpath = await getMetaApi().getLicensePath();
return await getMetaApi().takeChecksumOfFile(lpath, "blake2b");
}
async function readLicense(): Promise<string> {
const lpath = await getMetaApi().getLicensePath();
return (await getMetaApi().readUtf8File(lpath)).data;
}
interface FormatIntoParagraphsProps {
text: string;
}
function FormatTextFileIntoParagraphsWithHeader(props: FormatIntoParagraphsProps) {
return (<>
{props.text.split('\n').map((paragraph: string, index: number) => {
if(paragraph == "") {
return;
} else if (index == 0) {
return <h1 key={index}>{paragraph}</h1>
} else {
return <p key={index}>{paragraph}</p>
}
})}
</>)
}
function EulaScreen(props: EulaScreenProps) {
const [loadingLicense, setLoadingLicense] = useState(true);
const [licenseText, setLicenseText] = useState<string>("");
useEffect(() => {
(async () => {
setLicenseText(await readLicense());
setLoadingLicense(false);
})();
}, [])
if(loadingLicense) {
return (
<div className="eula-screen">
<div className="eulaText">
<p>Loading license please wait...</p>
</div>
</div>
)
} else {
return (
<div className="eula-screen">
<div className="eulaText">
<FormatTextFileIntoParagraphsWithHeader text={licenseText}/>
</div>
<form onSubmit={props.agreeCallback} spellCheck={false}>
<input type="submit" className={"generic-overlay-form-button"} value={"Agree"} disabled={false} />
</form>
</div>
)
}
}
export {EulaScreen, takeLicenseChecksum, readLicense, FormatTextFileIntoParagraphsWithHeader}

View File

@ -0,0 +1,213 @@
.generic-overlay-container
{
display: flex;
flex-direction: column;
padding: 0px;
margin: 0px;
overflow: hidden;
}
.generic-overlay-tabbar
{
flex-shrink: 0;
display: flex;
background-color: #301749;
}
.generic-overlay-tab-button
{
font-size: 25px;
padding: 12px 22px;
color: white;
}
.generic-overlay-tab-button:hover
{
background-color: #563a73;
}
.generic-overlay-tab-button-current
{
background-color: #3B2452;
}
.generic-overlay-tab-button-current:hover
{
background-color: #3B2452;
}
.generic-overlay-content-container
{
overflow-y: scroll;
}
.generic-overlay-inner
{
user-select:text;
padding: 30px;
padding-right: calc(30px - 10px); /*subtract scrollbar*/
}
.generic-overlay-inner p, .generic-overlay-inner span
{
margin: 0px;
color: white;
line-height: 1.5;
padding-left: 15px;
padding-right: 15px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.generic-overlay-inner h1
{
user-select: none;
margin-top: 0px;
font-size: 70px;
font-family: Arial, Helvetica, sans-serif;
font-weight: 900;
align-items: center;
color: white;
text-align: center;
}
.generic-overlay-inner h2
{
padding-left: 15px;
padding-right: 15px;
margin-top: 15px;
margin-bottom: 10px;
font-size: 30px;
color: white;
font-family: Arial, Helvetica, sans-serif;
font-weight: 900;
}
.generic-overlay-inner a
{
color:rgb(106, 138, 228);
}
.generic-overlay-inner a:active
{
color:rgb(233, 99, 99);
}
.generic-overlay-inner form
{
}
.generic-overlay-inner input
{
}
.generic-horizontal-input-cont
{
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.generic-horizontal-input-button
{
width: unset;
color: black;
border-width: 3px;
border: none;
float: right;
background-color: #dc9aa5;
margin-right: 8px;
margin-top: 8px;
padding: 8px;
height: unset;
margin-bottom: 25px;
}
.generic-horizontal-input-button:hover
{
background-color: pink;
}
.generic-horizontal-input-button:active
{
background-color: white;
}
.generic-overlay-inner input[type=text]
{
margin: 8px;
padding: 8px;
height: unset;
margin-bottom: 25px;
color:rgb(0, 255, 0);
width: calc(100% - 38px); /*padding, border, and inherited margin*/
background-color: rgb(44, 1, 73);
border-width: 3px;
font-style: italic;
border-style: solid;
border-color: transparent;
border-radius: 3px;
}
.generic-overlay-inner input[type=text]:hover
{
color: pink;
border-color: pink;
}
.generic-overlay-inner input[type=text]:focus
{
font-style:normal;
color: white;
outline: none;
border-color: white;
background-color: #260C3C;
}
.generic-overlay-form-button
{
font-size: 16pt;
margin: 8px;
padding: 12px;
padding-left: 15px;
padding-right: 15px;
height: unset;
margin-bottom: 25px;
width: unset;
color: black;
border-width: 3px;
border: none;
float: right;
border-radius: 12px 0px 12px 0px;
background-color: #48e43d;
}
.generic-overlay-form-button:hover
{
background-color: #11ff00;
}
.generic-overlay-form-button:active
{
color: white;
background-color: #0bac00;
}
.generic-overlay-container details
{
padding-left: 16px;
padding-right: 16px;
}
.generic-overlay-container summary
{
color: white;
user-select: none;
}

View File

@ -0,0 +1,88 @@
import React, {useState} from 'react';
import { StatorContext } from '../statorobject';
import { getOverlayStator } from './overlays';
import { useCloseableOverlay } from './useCloseableOverlay';
import "./genericoverlay.css"
export interface GenericOverlayTab
{
title: string;
nestedComponent: any;
nestedProps: any;
}
export interface GenericOverlayProps
{
startTab: number;
tabs: Array<GenericOverlayTab>;
appContext: StatorContext;
}
interface TabButtonProps
{
text: String;
index: number;
isCurrent: boolean;
tabChanger: (i: number) => void;
}
function TabButton(props: TabButtonProps)
{
return(
<button className={`generic-overlay-tab-button ${props.isCurrent ? "generic-overlay-tab-button-current" : ""}`}
onClick={() => { props.tabChanger(props.index) }}>
{props.text}
</button>
)
}
function GenericOverlayWrapper(props: GenericOverlayProps)
{
const [currentTab, setCurrentTab] = useState(props.startTab);
const closeMeCallback = () =>
{
getOverlayStator(props.appContext).closeOverlays();
};
const myRef = useCloseableOverlay(closeMeCallback);
const tabsJsx = (() =>
{
if(props.tabs.length == 1)
return <></>;
const tabs = props.tabs.map((t: GenericOverlayTab, index: number) =>
{
return ( <TabButton
text={t.title}
index={index}
isCurrent={index == currentTab}
tabChanger={setCurrentTab}
key={t.title}
/>);
});
return <div className={"generic-overlay-tabbar"}>{tabs}</div>
})();
const Component = props.tabs[currentTab].nestedComponent;
const ChildProps = props.tabs[currentTab].nestedProps;
return (
<div ref={myRef} className="center-overlay">
<div className={"generic-overlay-container"}>
{tabsJsx}
<div className={"generic-overlay-content-container"}>
<div className={"generic-overlay-inner"}>
<Component {...ChildProps} />
</div>
</div>
</div>
</div>
);
}
export { GenericOverlayWrapper }

View File

@ -0,0 +1,3 @@
.hamburber
{
}

View File

@ -0,0 +1,145 @@
import React from 'react'
import { ContextMenuData, getContextMenuStator } from './contextmenu';
import "./hamburgermenu.css"
import { StatorContext } from '../statorobject';
import { getBinderListStator } from './openbinders';
import { getOverlayStator, OverlayStatorStates } from './overlays';
import {justDie} from '../ipcbindings';
interface HamburgerMenuProps
{
buttonStyleClass: string;
appContext: StatorContext;
}
function HamburgerMenu(props: HamburgerMenuProps)
{
const overlayStator = getOverlayStator(props.appContext);
const clickHandler = (e: React.MouseEvent) =>
{
const menuProps: ContextMenuData =
{
spawnX: 28,
spawnY: 16,
contextMenuSections:
[
{
contextMenuItems:
[
{
title: "Sync Selected Binder",
keyBinding: "F6",
action: () => {
const binder = getBinderListStator(props.appContext).getCurrentBinder();
binder?.context.db.syncCluster();
}
},
]
},
/*
{
contextMenuItems:
[
{
title: "Undo",
keyBinding: "Ctrl-Z",
action: () => { }
},
{
title: "Redo",
keyBinding: "Ctrl-Y",
action: () => { }
},
]
},
*/
{
contextMenuItems:
[
{
title: "Select All",
keyBinding: "Ctrl-A",
action: () =>
{
getBinderListStator(props.appContext).getCurrentBinder()?.context.selectAllObjects();
}
},
{
title: "Invert Selection",
keyBinding: "Ctrl-Alt-A",
action: () =>
{
getBinderListStator(props.appContext).getCurrentBinder()?.context.invertObjectSelection();
}
},
]
},
{
contextMenuItems:
[
{
title: "Export Selected Objects",
keyBinding: "Ctrl-S",
action: () =>
{
getBinderListStator(props.appContext).getCurrentBinder()?.context.exportSelected();
//getMapper(getBinderListStator(props.appContext).getSelectedBinder())?.exportSelection();
}
}
]
},
{
contextMenuItems:
[
{
title: "Activation",
keyBinding: "",
action: () => { getOverlayStator(props.appContext).set(OverlayStatorStates.showActivation); } // :)
},
{
title: "Updates",
keyBinding: "",
action: () => { getOverlayStator(props.appContext).set(OverlayStatorStates.showUpdater); }
}
]
},
{
contextMenuItems:
[
{
title: "About",
keyBinding: "",
action: () => { overlayStator.set(OverlayStatorStates.showAbout) }
},
]
},
{
contextMenuItems:
[
{
title: "Quit",
keyBinding: "",
action: () => { justDie() }
},
]
},
],
}
getContextMenuStator(props.appContext).spawn(menuProps);
};
return (
<img
className={`hamburber ${props.buttonStyleClass}`}
onClick={clickHandler}
src={"static/kebaab.svg"}
draggable={false}
/>
);
}
export { HamburgerMenu };

View File

@ -0,0 +1,61 @@
.nag-screen-background
{
z-index: 100;
background-color: rgb(64, 0, 64, 0.45);
backdrop-filter: blur(10px);
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
display: flex;
align-items: center;
justify-content: center;
}
.sales-thing
{
color:white;
background-color: #240b2e;
width: 740px;
padding: 20px 0px 20px 0px;
border-radius: 5px;
}
.sales-thing a
{
color: #367bcb;
}
.sales-thing a:active
{
color: #2e34a6;
}
.sales-thing::selection
{
background-color: unset;
}
.sales-thing > p
{
margin: 0px 20px 0px 20px;
}
.sales-thing-footer
{
text-align: center;
user-select:none;
}
.tier-frame
{
border-style: none;
width: 100%;
height: 350px;
overflow: hidden;
margin-top: 15px;
margin-bottom: 10px;
}

View File

@ -0,0 +1,226 @@
import React, { useState, useEffect } from "react";
import { ActivationStatus, getActivationStator} from '../activation';
import { StatorContext, StatorObjectBase} from "../statorobject";
import { getBinderVersion, getUpdaterStator } from "../updaterstator";
import './nagscreen.css';
import { getOverlayStator, OverlayStatorStates } from "./overlays";
import { getMetaApi } from "../ipcbindings";
/*
class Periodic
{
interval: number = 0;
actuator = (): boolean => { return false; }; //returns true on continue
}
*/
class NagStator extends StatorObjectBase
{
private neverNag = true;
private timerId: number = 0;
private interval = (1000 * 60 * 45); //Every forty five minutes
private showNaggy = false;
constructor(context: StatorContext)
{
super(context);
//init :3
this.triggerNag();
}
private stopTimer()
{
window.clearTimeout(this.timerId);
}
private startTheClock()
{
this.timerId = window.setTimeout(() => { this.triggerNag(); }, this.interval)
}
private pumpActivation()
{
this.neverNag = !getActivationStator(this.getContext()).shouldNag();
}
forceShow()
{
this.showNaggy = true;
this.publishChanges();
}
private triggerNag()
{
this.pumpActivation();
//If the activation page is up, don't nag, that's heckin' annoying. Just restart the clock
//It's okay if it pops up some variable time after it's closed, it doesn't matter, I just don't want it
// to pop up while someone is trying to enter their key
//Now it's just any overlay, cause... Idk... It's annoying. What are we nagging over, a help screen? About page?
// meh.
//EHHHH I mean... The only reason not to annoy is to annoy someone that already purchased, otherwise it's like... par for the course
///ehhhhh it's annoying turn it off if an overlay is open
if(this.neverNag || getOverlayStator(this.getContext()).myShowState != OverlayStatorStates.showNone)
{
this.startTheClock();
return;
}
this.forceShow();
}
shutUp()
{
this.showNaggy = false;
//Check activation status now tho?
//Nah, it's checked in triggerNag, and we wait to check periodically even if activated.
this.startTheClock();
this.publishChanges();
}
shouldShowNaggy(): boolean
{
return this.showNaggy;
}
};
const NagStatorKey = "NagStator";
function useWatchNagStator(appContext: StatorContext)
{
return appContext.useWatchStatorLegacy(NagStatorKey, new NagStator(appContext)) as NagStator;
}
function getNagStator(appContext: StatorContext)
{
return appContext.getStatorLegacy(NagStatorKey, new NagStator(appContext)) as NagStator;
}
interface NagScreenProps
{
appContext: StatorContext;
};
function NagScreen(props: NagScreenProps)
{
const [pricingUrl, setPricingUrl] = useState("");
const stator = useWatchNagStator(props.appContext);
useEffect(() => {
(async () => {
const baseUrl = await getMetaApi().getServerEndpoint();
const binderVer = await getBinderVersion();
const endpoint = baseUrl + "/api/binder/v1/pricing/" + binderVer;
setPricingUrl(endpoint);
})();
}, []);
const openActivationPage = () =>
{
stator.shutUp();
getOverlayStator(props.appContext).set(OverlayStatorStates.showActivation);
};
if(!stator.shouldShowNaggy())
return <></>;
const appContext = props.appContext;
const CloseButton = (props: { val: string }) =>
{
return (
<a href="#" onClick={() =>
{
stator.shutUp();
//Check for updates cause otherwise we blasted past them if we're unlicensed.
getUpdaterStator(appContext).checkForUpdates();
}}>
{props.val}
</a>);
}
//IF tamper trip... Just go straight to activation page?
let tamperTrip = false;
const licenseStatus = (() =>
{
const myActivation = getActivationStator(props.appContext);
if(!myActivation)
{
return (<p><CloseButton val="close"/></p>);
}
switch (myActivation.status)
{
case ActivationStatus.Unlicensed:
return (<p>This copy of Binder is unlicensed and for evaluation purposes only, please purchase a license if you are done evaluating or <CloseButton val="continue your evaluation." /></p>);
case ActivationStatus.Subscription:
case ActivationStatus.Perpetual:
return <p>This copy of Binder is already licensed, <CloseButton val={"close this."} /></p>
case ActivationStatus.TamperTrip:
tamperTrip = true;
return <p>Activation tamper detection system was tripped and you need to re-activate</p>
case ActivationStatus.Pending:
//This could happen if the thing is opened from the activation page, otherwise it should never
// automatically nag if pending (don't annoy someone unless you're ready for their attention)
return <p>Activation is pending, <CloseButton val={"click here to dismiss this dialog."} /></p>
case ActivationStatus.Refunded:
return <p>Your subscription was refunded and you will need a new license to continue using Binder.</p>;
case ActivationStatus.ChargedBacked:
return <p>A charge back was issued against your subscription and you will need a new license to continue using Binder.</p>;
case ActivationStatus.ActivationError:
return (<p>There was an error activating, please try again later. <CloseButton val={"Close"}/></p>)
case ActivationStatus.InvalidKey:
return (<p>
Your key is invalid. <a href="#" onClick={openActivationPage}>Resolve.</a>
</p>);
case ActivationStatus.ExpiredSub:
return <p>Your subscription has expired.</p>;
default:
return (<CloseButton val={"Close"} />);
}
})();
//Tamper trip status, unlicensed status, activated and no further action needed (cause we can get to this page
// from the activation page which is always available)
//Todo: onError for the iframe, the case being it's not online, just... idk render it without it?
return (
<div className={"nag-screen-background"}
draggable={false}>
<div className={"sales-thing"}>
{licenseStatus}
<iframe className={"tier-frame"} src={pricingUrl}></iframe>
{
tamperTrip ?
<div className="sales-thing-footer">
<h2><a href="#" onClick={openActivationPage}>Go to activation page to sort this out</a></h2>
</div>
:
(
<div className="sales-thing-footer">
<p>and/or</p>
<h2><a href="#" onClick={openActivationPage}>Enter License Key</a></h2>
</div>
)
}
</div>
</div>);
}
export {NagScreen, getNagStator}

113
src/components/navbar.css Normal file
View File

@ -0,0 +1,113 @@
.nav-bar
{
display:flex;
flex-wrap: nowrap;
align-items: center;
background-color: #301749;
height: 35px;
width: 100%;
}
.nav-button
{
height: 19px;
padding: 8px;
color: white;
}
.nav-button:hover
{
background-color: #422a5a;
}
.nav-button:active
{
background-color: #0d0813;
}
.back-button
{
background-image: "src/logo.svg";
}
.nav-left-section
{
height: 100%;
position: relative;
}
.nav-bar-progress-background
{
position: absolute;
width: 100%;
height: 100%;
background-color: rgb(163, 120, 163);
z-index: 1;
color: black;
}
.nav-bar-progress-message
{
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
z-index: 3;
}
.nav-bar-progress
{
position: absolute;
height: 100%;
background-color: #48e43d;
z-index: 2;
}
.nav-right-section
{
flex-grow: 1;
display: flex;
flex-direction: row;
justify-content: right;
}
.nav-bar-drag-area
{
min-width: 300px;
width: 100%;
-webkit-app-region: drag;
}
.close-binder-button
{
background-color: none;
}
.close-binder-button:hover
{
background-color: rgb(252, 14, 14);
}
.close-binder-button:active
{
background-color: rgb(173, 20, 20);
}
.nav-button-depressed
{
/*background-color: #23142a;*/
/*background-color: #1E0E2E;*/
/*background-color: #260C3C;*/
/*background-color: #291141;*/
/*background-color: #260C3C;*/
background-color: #191023;
background-color: #151023;
background-color: #0f0b1c;
background-color: #100817;
/*background-color: #2b1441;*/
}

176
src/components/navbar.tsx Normal file
View File

@ -0,0 +1,176 @@
import React from 'react'
import { StatorContext, StatorObjectBase } from '../statorobject';
import './navbar.css'
import { ViewerState, useWatchObjectViewerOpenStator, useWatchObjectViewerStator } from './objectviewercontroller';
import { useWatchBinderListStator } from './openbinders';
import { justDie } from '../ipcbindings';
class NavBarStator extends StatorObjectBase
{
}
const NavBarStatorKey = "NavBarStator";
function useWatchNavBarStator(appContext: StatorContext)
{
return appContext.useWatchStatorLegacy(NavBarStatorKey, new NavBarStator(appContext)) as NavBarStator;
}
function getNavBarStator(appContext: StatorContext)
{
return appContext.getStatorLegacy(NavBarStatorKey, new NavBarStator(appContext)) as NavBarStator;
}
interface ObjectViewerControlsProps
{
appContext: StatorContext;
}
function AudioVideoControls(props: ObjectViewerControlsProps)
{
const controller = useWatchObjectViewerStator(props.appContext);
if (controller.checkPlayingAudioVideo())
return <NavBarButton src={"static/pause-button.svg"} clickHandler={() => controller.pauseAudioVideo()}/>
else
return <NavBarButton src={"static/play-button.svg"} clickHandler={() => controller.playAudioVideo()}/>
}
function ObjectViewerControls(props: ObjectViewerControlsProps)
{
//The beautiful thing about the usewatch functions, is that
// they gracefully handle the context object being swapped out.
const controllerStator = useWatchObjectViewerStator(props.appContext);
const singleObjectViewOpenStator = useWatchObjectViewerOpenStator(props.appContext);
if(!singleObjectViewOpenStator.viewerOpen)
{
console.log("Skip render object viewer controls");
return null;
}
console.log("Render object viewer controls");
const msg = controllerStator.theViewerState == ViewerState.loadingWhilePlayingSlideshow ? "Loading next..." : "";
const slideshowToggle = (() =>
{
switch(controllerStator.theViewerState)
{
case ViewerState.loadingWhilePlayingSlideshow:
case ViewerState.playingSlideshow:
return <NavBarButton src={"static/stop-slideshow-button.svg"}
clickHandler={ () => controllerStator.stopSlideshow() }/>
default:
return <NavBarButton src={"static/play-slideshow-button.svg"}
clickHandler={ () => {controllerStator.startOrContinueSlideshow() }}/>
}
})();
return (<div>
<NavBarButton src={"static/cancel-button.svg"} clickHandler={() =>
{
//Kinda jank cause this should be an action basically that other areas can call..
singleObjectViewOpenStator.close();
controllerStator.stopSlideshow();
//REALLY JANK CAUSE THIS SHOULD BE AN ACTION BASICALLY THAT OTHER AREAS CAN CALL..
controllerStator.pauseAudioVideo();
}}/>
{slideshowToggle}
{controllerStator.checkShowAudioVideoControls() ? <AudioVideoControls {...props}/> : ""}
</div>);
}
interface NavBarButtonProps
{
src: string;
clickHandler: () => void;
}
function NavBarButton(props: NavBarButtonProps)
{
return <img className="forward-button nav-button"
src={props.src}
onClick={(e: any) => { props.clickHandler() }}
draggable={false}
/>
}
interface NavBarProps
{
appContext: StatorContext;
}
function NavBar(props: NavBarProps)
{
const stator = useWatchNavBarStator(props.appContext);
const binderListStator = useWatchBinderListStator(props.appContext);
console.log("Re-rendering navbar");
let progressBar = <div></div>;
/*
if(stator.showProgressBar)
{
//Multiply all the percentages together
let percentage = 1;
let message = "";
for(const progBar of stator.progressBars)
{
percentage = percentage * progBar.getPercentage();
if (message != "")
message = message + " and " + progBar.getMessage();
else
message = progBar.getMessage();
}
const coolKidsPercentage = 100 * percentage;
const progressBarStyle = { width: coolKidsPercentage + "%" }
progressBar = (
<div>
<div className="nav-bar-progress-background"></div>
<div className="nav-bar-progress-message"> {message} </div>
<div style={progressBarStyle} className='nav-bar-progress'></div>
</div>
);
}
*/
//TODO
const openBinderContext = undefined;// binderListStator.getCurrentBinderContext();
if(!openBinderContext)
console.warn("Bad binder context");
return (<div className="nav-bar">
<div className="nav-left-section">
{openBinderContext ? <ObjectViewerControls appContext={openBinderContext}/>: null}
{progressBar}
</div>
<div className="nav-right-section">
{/*
<img className="back-button nav-button"
src={"static/left-arrow.svg"}
onClick={prevHandler}/>
<img className="forward-button nav-button"
src={"static/right-arrow.svg"}
onClick={nextHandler}/>
*/
}
<div className={'nav-bar-drag-area'} />
<NavBarButton src={"static/close-button.svg"}
clickHandler={() => { justDie() }} />
</div>
</div>);
}
export {NavBar, getNavBarStator}

View File

@ -0,0 +1,10 @@
.new-binder-overlay-error
{
margin-bottom: 20px;
background-color: rgb(223, 29, 29);
width: calc(100% - 30px);
padding: 15px;
border-radius: 5px;
user-select: text;
color: white;
}

View File

@ -0,0 +1,230 @@
import React, {useState} from 'react';
import { Binder } from '../backend'
import { getOverlayStator } from './overlays';
import './newbinderoverlay.css'
import './centeroverlay.css'
import './vibrantactionbutton.css'
import { StatorContext } from '../statorobject';
import { GenericOverlayWrapper } from './genericoverlay';
import {openSelectDirectory} from "../ipcbindings";
import { localNodeConfig } from '../localnodeconfig';
enum Operation //And we can do shit like this cause it's not C++, lol
{
NewBinder,
ConnectToExisting,
};
interface NewBinderOverlayProps
{
appContext: StatorContext;
}
function CreateNewBinderContent(props: NewBinderOverlayProps)
{
const [binderNameField, setBinderNameField] = useState<string>("");
const [binderDirectoryField, setBinderDirectoryField] = useState<string>("");
const [errString, setErrString] = useState<string>("");
const binderNameChange = (e: any) =>
{
setBinderNameField(e.target.value);
};
const binderDirectoryChange = (e: any) =>
{
setBinderDirectoryField(e.target.value);
}
const handleSubmit = async (e: any) => {
e.preventDefault();
try {
const msg = await Binder.createBinder(binderDirectoryField, binderNameField);
setErrString(msg);
if (!msg) {
getOverlayStator(props.appContext).closeOverlays();
}
} catch(e) {
setErrString("There was an error");
console.warn(e);
}
/*
createBinderEndpoint(binderNameField,
binderDirectoryField).then( (binder: Binder) =>
{
getBinderListStator(props.appContext).addBinder(binder);
}).catch((msg: string) =>
{
setErrString(msg);
console.log(msg);
})
*/
}
const choose = (e: any) =>
{
openSelectDirectory().then((result: string[] | undefined) =>
{
if(!result || !result[0])
return;
setBinderDirectoryField(result[0]);
});
};
let errorContent = <div></div>;
if(errString)
{
errorContent = <div className="new-binder-overlay-error">{errString}</div>
}
return <div>
<form onSubmit={handleSubmit} spellCheck={false}>
{errorContent}
<p>What would you like to call your new binder? (Folder Name)</p>
<input
type="text"
name="binderName"
value={binderNameField}
onChange={binderNameChange}
autoComplete="off"/>
<p> Where do you want it? (Can be local, Dropbox, OneDrive, etc)</p>
<div className="generic-horizontal-input-cont">
<input
type="text"
name="binderDirectory"
value={binderDirectoryField}
onChange={binderDirectoryChange}
autoComplete="off"/>
<input className="generic-horizontal-input-button" type="button"
value={"Choose"} onClick={choose}/>
</div>
<input type="submit" className="generic-overlay-form-button" value={"Create"}/>
</form>
</div>;
}
function ConnectToExistingContent(props: NewBinderOverlayProps)
{
const [binderPath, setBinderPath] = useState<string>("");
const [errString, setErrString] = useState<string>("");
const binderPathChange = (e: any) =>
{
setBinderPath(e.target.value);
};
const handleSubmit = async (e: any) => {
e.preventDefault();
try {
const msg = await localNodeConfig.registerCluster(binderPath);
setErrString(msg);
if (!msg) {
getOverlayStator(props.appContext).closeOverlays();
}
} catch(e) {
setErrString("There was an error");
console.warn(e);
}
/*
connectToExistingEndpoint(binderPath).then((binder: Binder) =>
{
getBinderListStator(props.appContext).addBinder(binder);
getMapper(binder.id).refreshAndSync();
}).catch((msg: string) =>
{
setErrString(msg);
console.log(msg);
});
*/
}
const choose = (e: any) =>
{
openSelectDirectory().then((result: string[] | undefined) =>
{
if(!result || !result[0])
return;
setBinderPath(result[0]);
});
};
let errorContent = <div></div>;
if(errString != "")
{
errorContent = <div className="new-binder-overlay-error">{errString}</div>
}
return <div>
<form onSubmit={handleSubmit} spellCheck={false}>
{errorContent}
<p>Where is the binder?</p>
<div className="generic-horizontal-input-cont">
<input
type="text"
name="binderName"
value={binderPath}
onChange={binderPathChange}
autoComplete="off"/>
<input className="generic-horizontal-input-button" type="button"
value={"Choose"} onClick={choose}/>
</div>
<input type="submit" className={"generic-overlay-form-button"} value={"Connect"}/>
</form>
</div>;
}
function NewBinderOverlay(props: NewBinderOverlayProps): JSX.Element
{
const [currentOperation, setCurrentOperation] = useState<Operation>(Operation.NewBinder);
return (<GenericOverlayWrapper
appContext={props.appContext}
startTab={0} /*Start us out at new binder*/
tabs={[{
title: "New",
nestedComponent: CreateNewBinderContent,
nestedProps: props,
},
{
title: "Existing",
nestedComponent: ConnectToExistingContent,
nestedProps: props,
}
]}
/>);
}
export { NewBinderOverlay }

View File

@ -0,0 +1,5 @@
.notification-area
{
background-color: #48e43d;
display: none;
}

View File

@ -0,0 +1,34 @@
import './notifications.css';
import React, {} from 'react'
import { StatorContext, StatorObjectBase } from '../statorobject';
class NotificationStator extends StatorObjectBase
{
}
const NotificationStatorKey = "NotificationStator";
function useWatchActivationStator(appContext: StatorContext)
{
return appContext.useWatchStatorLegacy(NotificationStatorKey, new NotificationStator(appContext));
}
//Yes I notice the typo, no I don't want to change it.
function getNotificationStatur(appContext: StatorContext)
{
return appContext.getStatorLegacy(NotificationStatorKey, new NotificationStator(appContext));
}
interface NotificationsProps
{
appContext: StatorContext;
}
function Notifications(props: NotificationsProps)
{
return <></>;
return <div className="notification-area"><p>updates avail!</p></div>
}
export {Notifications, getNotificationStatur}

View File

@ -0,0 +1,25 @@
.object-gallery
{
/*
padding-left: 6px;
padding-right: 18px; /*scrollbar width + 2 for some reason + 6 to match over (selected border)*/
/*
padding-top: 14px;
padding-bottom: 24px;
*/
position: relative;
}
.page-message
{
padding: 40px;
font-size: 55pt;
color: rgb(95, 63, 95);
text-align: center;
vertical-align: middle;
}
.object-gallery-container
{
height: 100%;
}

View File

@ -0,0 +1,285 @@
import React, {useEffect, useState, useRef} from 'react';
import { ObjectThumbnail} from './thumbnails/objectthumbnail';
import { BinderConnectionId, IndexedObject} from '../backend';
import './objectgallery.css';
import { StatorContext } from '../statorobject';
//import { getNavBarStator } from './navbar'
import { getUploadControllerStator } from "./uploadcontroller";
import { getBinderListStator } from './openbinders';
import { fileTreeFromDataTransferList } from '../filetree';
import { useWatchObservableValue } from '../observermanager';
import { ContextStatus } from './bindercontext';
const overRenderFactor = 3;
const rerenderThresholdFactor = 1;
interface ObjectGalleryProps {
theBinderId: BinderConnectionId;
appContext: StatorContext;
tagSelectionCount: number;
viewAreaHeight: number;
nominalOffset: number;
}
function PageMessage(props: {message: string}) {
return (<div className="page-message">{props.message}</div>);
}
function ObjectGallery(props: ObjectGalleryProps): JSX.Element {
const myRef = useRef<HTMLDivElement>(null)
//WHEN YOU'RE DONE REWRITING DON'T FORGET TO DO THE THING!
// AND SET THE ROW WIDTH!!
const [loadState, setLoadState] = useState<number>(ContextStatus.LoadingTags);
const [objectList, setObjectList] = useState<Array<IndexedObject>>(new Array<IndexedObject>());
const [viewWidth, setViewWidth] = useState<number>(0);
const context = getBinderListStator(props.appContext).getBinder(props.theBinderId).context;
useEffect(() => {
const queueChangeObsId = context.objectQueue.register(() => {
const objList = context.objectQueue.get();
setObjectList(objList ? objList : new Array<IndexedObject>());
});
return function () {
context.objectQueue.unregister(queueChangeObsId);
};
}, []);
useWatchObservableValue(context.status, (stat: number | undefined) => {
if(!stat) {
setLoadState(0);
} else {
setLoadState(stat);
}
});
useEffect(() => {
if (!myRef?.current) {
return;
}
const resizeObserver = new ResizeObserver((e: Array<ResizeObserverEntry>) => {
if (myRef.current) {
if(context.currentOpenObject.get()) {
return;
}
setViewWidth(myRef.current.offsetWidth);
//props.doRender isn't updated yet when we need this
/*
const rowCount = style.gridTemplateColumns.split(" ").length;
context.setRowWidth(rowCount);
*/
}
});
resizeObserver.observe(myRef.current);
return function () {
//capture a damn copy of the lil fucker
const currentRef = myRef.current;
if (!currentRef) {
console.warn("could not unobserve resize, this is okay if this was a live reload");
return;
}
resizeObserver.unobserve(currentRef);
}
}, [myRef]);
const myDragOverHandler = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
}
/*
const myDragLeaveHandler = (e: React.DragEvent<HTMLDivElement>) =>
{
//console.log("leave");
if (!myRef)
return;
const current = myRef.current;
if (!current)
throw "dfjd";
const bounds = current.getBoundingClientRect();
bounds.x += 10;
bounds.width -= 20;
bounds.y += 10;
bounds.height -= 20;
console.log("page x: " + e.pageX);
console.log("bounds x + w: " + (bounds.x + bounds.width));
if(e.pageX < bounds.x || e.pageX >= bounds.x + bounds.width ||
e.pageY < bounds.y || e.pageY >= bounds.y + bounds.height)
{
e.preventDefault();
setInDragOver(false);
}
}
*/
const myDropHandler = async (e: React.DragEvent<HTMLDivElement>) => {
console.log("Drop");
if (!e) return;
if (!e.dataTransfer) return;
if (!e.dataTransfer.types.includes("Files")) return;
e.preventDefault();
//Kind of a crappy way to do this but w/e I'm lazy
// really need to add the binder context itself to the appcontext...
const binder = getBinderListStator(props.appContext).getCurrentBinder();
if (!binder) {
console.warn("Somehow the binder isn't ready for the drop");
return;
}
const myUploadController = getUploadControllerStator(props.appContext);
myUploadController.setPreparing();
const tree = await fileTreeFromDataTransferList(e.dataTransfer.items);
await binder.context.handleUpload(tree, myUploadController);
}
//const style = props.doRender ? {} : {display: "none"};
let style: React.CSSProperties | undefined;
let content;
let objectJSX = new Array<any>();
if (loadState == ContextStatus.LoadingTagSelection) {
content = (
<PageMessage message="Loading..." />
);
}
else if(loadState == ContextStatus.Searching) {
content = (
<PageMessage message="Searching..." />
);
}
else if(loadState != ContextStatus.Ready) {
content = (
<PageMessage message="..." />
);
}
else if (props.tagSelectionCount == 0 && !context.searchQueryString.get()) {
content = (
<PageMessage message="No tag selected" />
);
}
else if (((objectList.length == 0))) {
if (props.tagSelectionCount == 1) {
content = (
<PageMessage message="Drop files here to copy them to this tag" />
);
}
else {
let msg;
if(!context.searchQueryString.get()) {
msg = "No objects have all selected tags";
} else {
msg = "Found nothing matching your query";
}
content = (
<PageMessage message={msg}/>
);
}
}
else {
const overRenderSize = props.viewAreaHeight * overRenderFactor;
const overRenderPreceding = overRenderSize / 2;
const startBound = (() => {
let result = props.nominalOffset - overRenderPreceding;
return result < 0 ? 0 : result;
})();
//Outerbound is non-inclusive so add extra... Or not it really doesn't matter...
const outerBound = props.nominalOffset + props.viewAreaHeight + overRenderPreceding;
const itemBorderTotal = 6 * 2;
const itemSize = 150;
const gridSpacing = 24;
const itemMinSize = itemSize + itemBorderTotal;
const rowHeight = itemSize + itemBorderTotal + gridSpacing;
const colCount = Math.trunc((viewWidth - 24) / rowHeight);
const countHeight = objectList.length ? Math.ceil(objectList.length / colCount) * rowHeight : 0;
context.setRowWidth(colCount);
const leftPad = ((viewWidth - (colCount * itemMinSize)) / (colCount + 1));
//hmmm... lol this ain't it
let rowIndex = 0;
for(let y = 0; y < outerBound; y += rowHeight) {
//console.log("first loop");
if (y >= startBound) {
let nextX = leftPad;
for (let colI = 0; colI != colCount; ++colI) {
const objIndex = rowIndex + colI;
const obj = objectList.at(objIndex);
if(!obj) {
// console.log("skipping obj");
break;
}
// console.log("drawing object at: " + x + ", " + y);
objectJSX.push((<ObjectThumbnail key={obj.id}
obj={obj}
binderContext={context}
appContext={props.appContext}
theBinderId={props.theBinderId}
x={nextX}
y={y + gridSpacing/2} />));
nextX = nextX + itemMinSize + leftPad;
}
}
rowIndex += colCount;
}
style = {height: countHeight, display: "visible"};
}
return (
<div className='object-gallery-container'
onDragOver={myDragOverHandler}
onDrop={myDropHandler}
>
<div className="object-gallery"
ref={myRef}
style={style}
>
{objectJSX}
</div>
{content}
</div>);
}
export {
ObjectGallery,
overRenderFactor,
rerenderThresholdFactor
}

View File

@ -0,0 +1,114 @@
.object-viewer
{
display: flex;
align-items: center;
height: 100%;
width: 100%;
color: white;
position:relative;
overflow: hidden;
}
.object-viewer-container
{
position: absolute;
display: flex;
justify-content: center;
background-color: black;
width: 100%;
height: 100%;
}
.object-viewer-checkerboard
{
background-image: url("static/checkerboard.svg");
background-size: 20px;
}
.object-viewer-controls
{
position: absolute;
background-color: aliceblue;
}
.object-viewer-side-control-overlay
{
position:absolute;
display: flex;
height: 40%;
width: 90px;
padding: 20px;
opacity: 0;
}
.object-viewer-side-control-overlay:hover
{
opacity: 1;
/*how I managed to hit what is basically the same color (almoooost)
when mixed with black, by just... eyeballing it, not even
with that goal in mind...*/
/*background-color: #301749;*/
/*
background-color: rgba(90, 45, 133, 0.5); the eyeballed color */
background-color: rgba(96, 46, 146, 0.5);
}
.object-viewer-side-control-overlay:active
{
background-color: rgba(131, 39, 216, .5);
}
.object-viewer-side-control-overlay-left
{
left: 0;
border-radius: 0 10px 10px 0;
}
.object-viewer-side-control-overlay-right
{
right: 0;
border-radius: 10px 0 0 10px;
}
/*so... If we don't do this... At least in chromium...
* the view blows out based on the original size of the image
* but then it gets resized... So we have to pose limits
* to the size or it crushes the tag sidebar on large images*/
.object-viewer-content-area
{
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.object-viewer-pdf-frame
{
width: 100%;
border-width: 0px;
}
.object-viewer-plaintext
{
white-space: pre-wrap;
overflow-wrap: break-word;
margin: 0px;
color: black;
}
.object-viewer-plaintext::selection
{
color: white;
}
.object-viewer-plaintext-background
{
width: calc((100% - (90px * 2)) - 100px);
/*background-color: rgb(200, 178, 207);*/
background-color: white;
padding: 50px;
overflow-y: visible;
overflow-x: hidden;
user-select: text;
}

View File

@ -0,0 +1,336 @@
import React, {useState, useEffect, useRef} from 'react'
import { BinderConnectionId, getObjectUri} from "../backend";
import { StatorContext } from '../statorobject';
import "./objectviewer.css"
import { getObjectViewerStator } from './objectviewercontroller';
import { getBinderListStator } from './openbinders';
export interface ObjectViewerProps {
theBinderId: BinderConnectionId;
appContext: StatorContext;
}
function VideoPlayerHead(props: ObjectViewerProps)
{
/* cause it uses b2bsum...
const myRef = useRef<HTMLVideoElement>(null);
const stator = getObjectViewerStator(props.appContext);
//Seek should be a function here, that, after calling seek on the video, passing the current time
// back to the stator
useEffect(() =>
{
if (myRef && myRef.current)
{
stator.setAudioVideo(() => myRef.current?.play(),
() => myRef.current?.pause(),
(i: number) => myRef.current?.fastSeek(i),
myRef.current.duration)
}
},[myRef, myRef.current, props.obj.b2bsum]);
return <video
ref={myRef}
className={"object-viewer-content-area"}
src={getObjectUri(props.obj)}
/>//is it really that simple.
*/
}
interface ViewerImplProps extends ObjectViewerProps {
objId: string;
}
function ImageViewer(props: ViewerImplProps) {
const context = getBinderListStator(props.appContext).getBinder(props.theBinderId).context;
const [imageUrl, setImageUrl] = useState<string>();
useEffect(() => {
(async () => {
const optionalImageUrl = await context.db.getObjectPath(props.objId);
if (!optionalImageUrl) {
return;
}
setImageUrl(optionalImageUrl.path);
})();
});
const handleLoad = (e: React.SyntheticEvent) => {
/*
if (controller.theViewerState == ViewerState.loadingWhilePlayingSlideshow)
{
controller.startOrContinueSlideshow();
}
*/
}
const wrappedURI = imageUrl ? "file:///" + imageUrl : undefined;
return (
<img
className="object-viewer-content-area"
src={wrappedURI}
onLoad={handleLoad}
draggable={false}
/>
)
}
function PDFViewer(props: ViewerImplProps) {
const uri = getObjectUri(props.objId);
return <iframe id="pdf-js-viewer" className="object-viewer-pdf-frame" src={uri}></iframe>;
}
function PlainTextViewer(props: ViewerImplProps)
{
//literally copy and pasted this from objecthumbnail... It's basically the
// same component with different container rendering
const [text, setText] = useState<string>("");
const textUri = getObjectUri(props.objId);
useEffect(() =>
{
const request = new Request(textUri);
fetch(request).then((resp: Response) =>
{
resp.text().then((value: string) =>
{
setText(value);
});
});
}, [textUri]);
return (
<div className="object-viewer-plaintext-background">
<pre className="object-viewer-plaintext">{text}</pre>
</div>
)
}
function ObjectViewer(props: ObjectViewerProps)
{
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 [objId, setObjId] = useState<string>();
const [doRender, setDoRender] = useState<boolean>(false);
//const objectOpenStator = getObjectViewerOpenStator(props.appContext);
useEffect(() => {
if (mime) {
return;
}
(async () => {
if (!objId) {
return;
}
if (myRef.current && doRender && myRef.current != document.activeElement) {
//console.log("focusing object viewer");
myRef.current.focus();
}
const queriedMime = await context.db.getMimeGenIfNotExists(objId);
if (!queriedMime) {
return;
}
setMime(queriedMime);
})();
}, [objId]);
useEffect(() => {
const currentOpenObjectCont = context.currentOpenObject;
const sync = () => {
const newObjId = currentOpenObjectCont.get();
//clear mime cause it's outdated now, other effect hook
// will trigger on objId change to update it
setMime("");
if (!newObjId) {
setDoRender(false);
setObjId("");
} else {
setDoRender(true);
setObjId(newObjId.id);
}
}
const obsId = currentOpenObjectCont.register(sync);
return () => {
currentOpenObjectCont.unregister(obsId);
};
}, [myRef.current]);
/*
//Focus on render so that keyboard events are intuitive
useEffect(() =>
{
if(myRef == null)
return
if(myRef.current == null)
return;
//typescript going crazy on this w/e
myRef.current.focus();
});
*/
const clickHandler = (e: React.MouseEvent) => {
e.stopPropagation(); //Otherwise the tagpage whitespace handler is called and we get deselected but still visible
if(e.detail == 2) {
context.closeViewer();
}
}
//these really need to be unified... with the tagpage one... It's basically the same function
function downHandler(e: React.KeyboardEvent<HTMLDivElement>) {
//console.log("Now my turn!");
if(!doRender) //invisible so skip
return;
if(!e.repeat) //No repeats...
{
if(e.key == "ArrowLeft")
{
e.preventDefault();
e.stopPropagation();
context.selectPrevious();
}
if(e.key == "ArrowRight")
{
e.preventDefault();
e.stopPropagation();
context.selectNext();
}
if(e.key == "ArrowUp")
{
e.preventDefault();
e.stopPropagation();
context.selectPreviousRow();
}
if(e.key == "ArrowDown")
{
e.preventDefault();
e.stopPropagation();
context.selectNextRow();
}
if(e.key == " ")//Spacebar because whoever did this think's they're real clever
{
e.preventDefault()
e.stopPropagation();
if(!e.ctrlKey)
context.selectNext();
else
context.selectPrevious();
}
if(e.key == "Enter")
{
e.preventDefault();
e.stopPropagation();
context.closeViewer();
}
if(e.key == "Escape")
{
e.preventDefault();
e.stopPropagation();
context.closeViewer();
}
}
}
//const checkboardBack = mime == "image/svg+xml";
const checkboardBack = false;
const style = doRender ? { } : {display: "none"};
let myJsx = <></>;
if (mime && objId) //split up the thing
{
const splits = mime.split("/");
if (splits.length > 1) {
const mainMimeType = splits[0];
const secondMimeType = splits[1];
if (mainMimeType == "image") {
myJsx = <ImageViewer {...props} objId={objId} />
} else if (mainMimeType == "application") {
let val = secondMimeType;
if (secondMimeType == "octet-stream") {
val = "unknown";
}
myJsx = <p>{val}</p>
} else {
myJsx = <p>{mime}</p>
}
}
}
const backClickHandler = (e: React.MouseEvent) =>
{
context.selectPrevious();
e.stopPropagation();
};
const forwardClickHandler = (e: React.MouseEvent) =>
{
context.selectNext();
e.stopPropagation();
};
return (
<div className={`object-viewer ${checkboardBack ? 'object-viewer-checkerboard' : ''}`}
onClick={clickHandler}
onKeyDown={downHandler}
tabIndex={1}
style={style}
ref={myRef}
>
<div className={"object-viewer-container"}>
{myJsx}
</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}
/>
</div>
);
}
export {ObjectViewer}

View File

@ -0,0 +1,206 @@
import { StatorContext, StatorObjectBase } from "../statorobject";
//The idea is that the playbar thing resets the animation whenever
// it renders, and doesn't start until... Uh... mental model fuzzy
export enum ViewerState
{
chill,
playingSlideshow,
playingAudioVideo,
loadingWhilePlayingSlideshow,
}
export enum ObjectViewerContext
{
image,
audiovideo,
pdf,
text,
}
export class ObjectViewerStator extends StatorObjectBase
{
private viewerContext = ObjectViewerContext.image;
theViewerState: ViewerState = ViewerState.chill;
slideshowInterval = 3000; //3 seconds is gud :3
private timerId: any = 0;
loadPreviousImageCallback = () => { console.warn("Previous image callback not set!!"); };
loadNextImageCallback = () => { console.warn("Next image callback not setup!!!"); };
private pauseHook = () => {};
private playHook = () => { console.warn("Play hook not set") };
private seekHook = (i: number) => {};
private audioVideoLength = 0;
checkShowAudioVideoControls()
{
if (this.viewerContext != ObjectViewerContext.audiovideo)
return false;
if (this.theViewerState == ViewerState.loadingWhilePlayingSlideshow || this.theViewerState == ViewerState.playingSlideshow)
return false;
return true;
}
checkPlayingAudioVideo()
{
return this.theViewerState == ViewerState.playingAudioVideo;
}
setAudioVideo(play: () => void, pause: () => void, seek: (i: number) => void, length: number)
{
this.playHook = play;
this.pauseHook = pause;
this.seekHook = seek;
this.audioVideoLength = length;
this.viewerContext = ObjectViewerContext.audiovideo;
this.publishChanges();
}
private cancelTimer()
{
clearTimeout(this.timerId);
}
setViewerContext(c: ObjectViewerContext)
{
if(this.viewerContext == c)
return;
this.viewerContext = c;
this.publishChanges();
}
playAudioVideo()
{
this.cancelTimer();
this.theViewerState = ViewerState.playingAudioVideo;
this.playHook();
this.publishChanges();
}
pauseAudioVideo()
{
this.theViewerState = ViewerState.chill;
this.pauseHook();
this.publishChanges();
}
rewind()
{
this.pauseAudioVideo();
this.loadPreviousImageCallback();
}
fastForward()
{
this.pauseAudioVideo();
this.loadNextImageCallback();
}
setSlideshowInterval(i: number)
{
this.cancelTimer();
this.slideshowInterval = i;
this.publishChanges();
}
startOrContinueSlideshow()
{
if (this.theViewerState == ViewerState.playingAudioVideo)
this.pauseAudioVideo();
const callbacksies = () =>
{
console.log("next image plox, waiting for continuation from viewer");
this.theViewerState = ViewerState.loadingWhilePlayingSlideshow;
this.loadNextImageCallback();
this.publishChanges();
};
this.timerId = setTimeout(callbacksies, this.slideshowInterval);
this.theViewerState = ViewerState.playingSlideshow;
this.publishChanges();
}
stopSlideshow()
{
this.cancelTimer();
this.theViewerState = ViewerState.chill;
this.publishChanges();
}
}
const ObjectViewerStatorKey = "ObjectViewerStator";
function useWatchObjectViewerStator(appContext: StatorContext)
{
return appContext.useWatchStatorLegacy(ObjectViewerStatorKey, new ObjectViewerStator(appContext)) as ObjectViewerStator;
}
function getObjectViewerStator(appContext: StatorContext)
{
return appContext.getStatorLegacy(ObjectViewerStatorKey, new ObjectViewerStator(appContext)) as ObjectViewerStator;
}
export class ObjectViewerOpenStator extends StatorObjectBase
{
//openBinderObject: BinderObject | undefined;
viewerOpen = false;
close()
{
if(!this.viewerOpen)
return;
this.viewerOpen = false;
this.publishChanges();
}
/*
open(b: BinderObject)
{
if(this.openBinderObject == b && this.viewerOpen)
return;
this.openBinderObject = b;
this.viewerOpen = true;
this.publishChanges();
}
*/
open()
{
if(this.viewerOpen)
{
console.warn("Viewer already open");
return;
}
this.viewerOpen = true;
this.publishChanges();
}
};
const ObjectViewerOpenStatorKey = "ObjectViewerOpenStator";
function useWatchObjectViewerOpenStator(appContext: StatorContext)
{
return appContext.useWatchStatorLegacy(ObjectViewerOpenStatorKey, new ObjectViewerOpenStator(appContext)) as ObjectViewerOpenStator;
}
function getObjectViewerOpenStator(appContext: StatorContext)
{
return appContext.getStatorLegacy(ObjectViewerOpenStatorKey, new ObjectViewerOpenStator(appContext)) as ObjectViewerOpenStator;
}
export {useWatchObjectViewerStator,
getObjectViewerStator,
useWatchObjectViewerOpenStator,
getObjectViewerOpenStator,
}

View File

@ -0,0 +1,358 @@
import React from 'react'
import { StatorObjectBase, StatorContext } from '../statorobject'
import { BinderConnectionId, Binder } from '../backend';
import { TagPage } from "./tagPage";
import { getOverlayStator, OverlayStatorStates } from './overlays';
import { BinderContext } from './bindercontext';
import { localNodeConfig } from '../localnodeconfig';
class BinderContextPair {
binder: Binder;
narrowedContext: StatorContext;
constructor(b: Binder, cont: StatorContext)
{
this.binder = b;
this.narrowedContext = cont;
}
}
//This is hardly incremental, but it wasn't before either, so fuck it
class BinderListStator extends StatorObjectBase {
private openBinderList = new Array<BinderContextPair>();
private selectedBinderId: BinderConnectionId = "";
private initialized = false;
constructor(appContext: StatorContext) {
super(appContext);
const initialBinderList = localNodeConfig.connectedBinders.get();
///*
(async () => {
if (initialBinderList) {
await this.syncToBinderList(initialBinderList);
if (initialBinderList.length)
this.setSelectedBinder(initialBinderList[0]);
}
})();
//*/
localNodeConfig.connectedBinders.register(async () => {
const updatedBinderIdList = localNodeConfig.connectedBinders.get();
await this.syncToBinderList(updatedBinderIdList);
if(!this.initialized && updatedBinderIdList) {
if (updatedBinderIdList.length) {
this.setSelectedBinder(updatedBinderIdList[0]);
this.initialized = true;
}
}
});
}
/*
flushBinders() {
for(const pair of this.openBinderList) {
pair.binder.context.db.flushIfReady();
}
}
*/
private findPairById(binderId: BinderConnectionId): BinderContextPair | undefined
{
/*
for(const pair of this.openBinderList) {
if(pair.binder.id == binderId) {
return pair;
}
}
return undefined;
*/
return this.openBinderList.find((val: BinderContextPair) => { return val.binder.id == binderId; });
}
getBinder(binderId: BinderConnectionId): Binder {
const pair = this.findPairById(binderId);
if(!pair)
throw new Error("Can't get binder that doesn't exist");
return pair.binder;
}
checkSelected(bId: BinderConnectionId) {
return bId == this.selectedBinderId;
}
getCurrentBinder(): Binder | undefined {
return this.getBinder(this.selectedBinderId);
}
private async addBinderNonPublishing(bId: BinderConnectionId) {
// console.log("Loading binder");
const b = await localNodeConfig.loadBinder(bId);
this.openBinderList.push(new BinderContextPair(b, this.getContext()));
this.selectedBinderId = bId;
}
setSelectedBinder(bId: BinderConnectionId) {
const b = this.findPairById(bId);
if(!b)
{
console.warn("Could not find binder by id!!");
return;
}
this.selectedBinderId = bId;
this.publishChanges();
}
async syncToBinderList(binders: Array<BinderConnectionId> | undefined) {
if(!binders) {
return;
}
//console.log("Binder list to sync to: " + binders.length);
const currentIdSet = new Set<BinderConnectionId>();
let changed = false;
for(const bId of binders) {
currentIdSet.add(bId);
if (this.findPairById(bId)) {
//console.log("Skipping");
continue;
}
await this.addBinderNonPublishing(bId);
changed = true;
}
this.openBinderList.forEach((pair: BinderContextPair, index: number) => {
if(!currentIdSet.has(pair.binder.id)) {
this.openBinderList.splice(index, 1);
changed = true;
if(this.openBinderList.length && this.selectedBinderId == pair.binder.id) {
//select adjacent, preferably earlier index so we're stable
let newIndex = index;
if(newIndex + 1 > this.openBinderList.length) {
newIndex = this.openBinderList.length - 1;
}
this.setSelectedBinder(this.openBinderList[newIndex].binder.id);
}
}
});
if(changed) {
this.publishChanges();
}
}
/*
removeBinderById(id: BinderConnectionId)
{
//This is contrived cause I want to get the binder next to the
// one that was closed
let newOpenBindersList = new Array<BinderContextPair>();
let skippedOldBinder = false;
let replacedCurrent = false;
let lastBinderIdAdded: BinderConnectionId = "";
for (const oldB of this.openBinderList)
{
if (oldB.binder.id != id)
{
newOpenBindersList.push(oldB);
lastBinderIdAdded = oldB.binder.id;
}
else
{
skippedOldBinder = true;
}
if (skippedOldBinder && !replacedCurrent && lastBinderIdAdded)
{
this.selectedBinderId = lastBinderIdAdded;
replacedCurrent = true;
}
}
this.openBinderList = newOpenBindersList;
this.publishChanges();
}
*/
getOpenBinderList() {
return [...this.openBinderList];
}
}
const binderListStatorKey = "BinderListStator";
function useWatchBinderListStator(appContext: StatorContext)
{
return appContext.useWatchStator(BinderListStator);
// return appContext.useWatchStatorLegacy(binderListStatorKey, new BinderListStator(appContext)) as BinderListStator;
}
function getBinderListStator(appContext: StatorContext)
{
return appContext.getStator(BinderListStator);
//return appContext.getStatorLegacy(binderListStatorKey, new BinderListStator(appContext)) as BinderListStator;
}
interface NoBinderAction
{
actionTitle: string;
action: () => void;
}
interface NoBinderProps
{
doRender: boolean;
message: string;
actions: Array<NoBinderAction>;
}
function BinderErrorPage(props: NoBinderProps)
{
const style = props.doRender ? {} : {display: "none"};
const actionJsx = props.actions.map((val: NoBinderAction) =>
{
return <button key={val.actionTitle} onClick={val.action}>{val.actionTitle}</button>
});
return (<div style={style} className="open-binder-area no-binders-container">
<div className="no-binders-container-message">
{props.message}
{actionJsx}
</div>
</div>)
}
interface OpenBindersProps {
appContext: StatorContext;
}
function OpenBinders(props: OpenBindersProps): JSX.Element | null
{
const binderListStator = useWatchBinderListStator(props.appContext);
function refresh()
{
console.log("reimpliment meee")
}
const result = binderListStator.getOpenBinderList().map((pair: BinderContextPair) => {
if (pair.binder.id == "")
return "";
if(!pair.binder.exists)
{
return (<BinderErrorPage key={pair.binder.id.toString()}
doRender={binderListStator.checkSelected(pair.binder.id)}
message={"Could not find this binder, has it moved?"}
actions={[
{
actionTitle: "Update Location",
action: () => {console.log("oi! update the location yourself asshole"); refresh()}
},
{
actionTitle: "Disconnect",
action: () => {
localNodeConfig.removecluster(pair.binder.id);
}
},
{
actionTitle: "Refresh",
action: () => {refresh()}
},
]}/>
);
}
return(<OpenBinder key={pair.binder.id}
myBinderId={pair.binder.id}
binderContext={pair.binder.context} //really wanna merge this into app context so bad...
binderSpecificAppContext={pair.narrowedContext}
/>);
});
/*
const noBinderJsx = openBinderList.length != 0 ? '' : (
<BinderErrorPage selected={true}
message="Create or connect to a binder to get started"
actions={[
{
actionTitle: "Add Binder",
action: () => { overlayStator.set(OverlayStatorStates.showNewBinder) }
},
]}/>)
*/
if(result.length == 0)
{
const newBinderAction: NoBinderAction =
{
action: () => { getOverlayStator(props.appContext).set(OverlayStatorStates.showNewBinder) },
actionTitle: "Create/Connect",
};
return <BinderErrorPage doRender={true} message={"No binders"} actions={[newBinderAction]}/>
}
return (<>{result}</>);
}
interface OpenBinderProps
{
binderContext: BinderContext;
myBinderId: BinderConnectionId;
binderSpecificAppContext: StatorContext;
}
function OpenBinder(props: OpenBinderProps)
{
const binderListStator = useWatchBinderListStator(props.binderSpecificAppContext);
const selected = binderListStator.checkSelected(props.myBinderId);
//console.log("Open binder re-render");
return (
<div className={`open-binder-area ${selected ? "" : "open-binder-area-hidden"}`}>
<TagPage
binderContext={props.binderContext}
binderId={props.myBinderId}
doRender={true}
appContext={props.binderSpecificAppContext}
/>
</div>
);
}
//TODO: context pair is a leaky abstraction... TagSidebar needs it, just to read
// the binder list...
export { OpenBinders, getBinderListStator, useWatchBinderListStator, BinderContextPair }

View File

@ -0,0 +1,74 @@
import React from 'react'
import { StatorObjectBase, StatorContext } from '../statorobject';
import { NewBinderOverlay } from "./newbinderoverlay";
import { AboutOverlay } from './aboutoverlay';
import { ActivationOverlay } from './activationoverlay';
import { UpdaterOverlay } from './updateroverlay';
//Todo okay so this is janky
// I never intended this to be the central point to install new overlays...
// This process needs to be done functionally.
// (cept, you still need to be able to refer to them programatically... idek)
export enum OverlayStatorStates
{
showNone,
showNewBinder,
showActivation,
showAbout,
showUpdater,
}
class OverlayStator extends StatorObjectBase
{
myShowState = OverlayStatorStates.showNone;
closeOverlays()
{
this.myShowState = OverlayStatorStates.showNone;
this.publishChanges();
}
set(state: OverlayStatorStates)
{
this.myShowState = state;
this.publishChanges();
}
}
const overlayStatorKey = "OverlayStator";
function useWatchOverlayStator(appContext: StatorContext)
{
return appContext.useWatchStatorLegacy(overlayStatorKey, new OverlayStator(appContext))
}
function getOverlayStator(appContext: StatorContext)
{
return appContext.getStatorLegacy(overlayStatorKey, new OverlayStator(appContext)) as OverlayStator;
}
interface OverlayProps
{
appContext: StatorContext;
}
function Overlays(props: OverlayProps)
{
const myStator = useWatchOverlayStator(props.appContext);
switch(myStator.myShowState)
{
case OverlayStatorStates.showAbout:
return (<AboutOverlay appContext={props.appContext}/>);
case OverlayStatorStates.showNewBinder:
return (<NewBinderOverlay appContext={props.appContext}/>);
case OverlayStatorStates.showActivation:
return (<ActivationOverlay appContext={props.appContext}/>);
case OverlayStatorStates.showUpdater:
return (<UpdaterOverlay appContext={props.appContext}/>);
case OverlayStatorStates.showNone: return null;
}
}
export { Overlays, getOverlayStator }

View File

@ -0,0 +1,16 @@
.subdued-action-button
{
color: white;
border-width: 0px;
background-color: #301749;
}
.subdued-action-button:hover
{
background-color: #63378d;
}
.subdued-action-button:active
{
background-color: #322146;
}

158
src/components/tagPage.css Normal file
View File

@ -0,0 +1,158 @@
.tag-page-container
{
width: 100%;
display: flex;
flex-direction: column;
}
.tag-page
{
flex: 1;
display: flex;
flex-direction: column;
width: 100%;
overflow-x: hidden;
}
.tag-content-view
{
flex: 1;
display: flex;
flex-direction: column;
overflow-y: scroll;
/*I learned the hard way that for some reason, if this is higher up? Yeah, cleaner css
* but scrolling becomes jerky, and cpu spikes bad. Why? Idk, something related to compositing
* transparency, I suppose*/
background-color: #1E0E2E;
/*because sometimes the grid is scrollable by like 2px and idk why*/
}
.tag-content-view-header
{
flex-shrink: 0;
margin: 24px;
margin-bottom: 10px;
padding: 10px;
}
.tag-page-search-bar {
}
.tag-page-search-bar > h3 {
font-size: 30px;
font-weight:400;
color: rgb(94, 62, 102);
}
.tag-page-search-bar-input {
/*
font-size: 30px;
padding: 10px;
border-radius: 0px;
border: 0px;
width: 80%;
margin-left: 10%;
margin-right: 10%;
background-color: rgb(22, 2, 36);
color: white;
*/
margin: 8px;
padding: 8px;
height: unset;
margin-bottom: 25px;
color:rgb(0, 255, 0);
width: calc(100% - 38px); /*padding, border, and inherited margin*/
background-color: rgb(44, 1, 73);
background-color: rgb(22, 2, 36);
border-width: 3px;
font-style: italic;
border-style: solid;
border-color: transparent;
border-radius: 3px;
}
.tag-page-search-bar-input::placeholder {
color: rgb(243, 225, 255);
}
.tag-page-search-bar-input::selection {
}
.tag-page-search-bar-input:hover {
color: pink;
border-color: pink;
}
.tag-page-search-bar-input:focus {
font-style:normal;
color: white;
outline: none;
border-color: white;
background-color: #260C3C;
}
.object-gallery-status-bar
{
display: flex;
padding: 0px;
height: 35px;
width: 100%;
background-color: #301749;
align-items: center;
justify-content: right;
}
.object-gallery-status-bar > p
{
padding-right: 15px;
color: white;
margin: 0px;
}
.tag-content-view-header-field
{
align-items: center;
font-family: sans-serif;
width: 100%;
margin:2px;
padding: 7px;
min-height: 20px;
height: auto;
font-size: 25pt;
color: white;
border-style: none;
background-color: transparent;
overflow-wrap: anywhere;
resize: none;
}
.tag-content-view-header-field::selection
{
background-color: rgb(101, 85, 173);
}
.tag-content-view-header-field:focus
{
border-style: solid;
border-color: white;
margin: 0px;
border-width: 2px;
outline: none;
border-radius: 3px;
background-color: #322146;
}
.tag-list-item-new-tag-button
{
height: 32px;
font-size: 20px;
font-weight: bold;
text-align: center;
vertical-align: middle;
}

726
src/components/tagPage.tsx Normal file
View File

@ -0,0 +1,726 @@
import React, {useState, useEffect, useRef } from 'react'
import {ObjectGallery, rerenderThresholdFactor} from "./objectgallery";
import { BinderConnectionId, TagId, } from '../backend'
import './tagPage.css'
import {ObjectViewer} from "./objectviewer";
import { StatorContext } from '../statorobject';
import { parseTagName } from '../tagnamefunction';
import { BinderContext, ContextStatus } from './bindercontext';
import { getBinderListStator } from './openbinders';
import { useWatchObservableValue } from '../observermanager';
interface TagPageProps {
binderContext: BinderContext;
binderId: BinderConnectionId;
doRender: boolean;
appContext: StatorContext;
}
interface TagPageHeaderProps
{
context: BinderContext;
myTagId: TagId;
binderId: BinderConnectionId;
borkRef: React.MutableRefObject<HTMLDivElement | null>;
selectedTags: Set<TagId>;
}
enum TagPageLoadState {
loading,
loaded,
syncing,
};
interface TagPageSearchBarProps {
context: BinderContext;
query: string;
}
function TagPageSearchBar(props: TagPageSearchBarProps) {
const [value, setValue] = useState<string>(props.query);
const ref = useRef<HTMLInputElement>(null);
const inputHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}
const blurHandler = () => {
const currentSearchValue = props.context.searchQueryString.get();
if(!value) {
//We don't want to clear the search page like that
if(currentSearchValue) {
setValue(currentSearchValue);
}
return;
}
props.context.setSearchQuery(value);
}
const submit = (e: any) => {
e.preventDefault();
ref.current?.blur();
}
return (
<form onSubmit={submit}>
<div className='tag-page-search-bar generic-horizontal-input-cont'>
{
// <h3>Tag Search</h3>
}
<input className="tag-page-search-bar-input"
ref={ref}
onChange={inputHandler}
onBlur={blurHandler}
value={value}
placeholder={"Tag Search"}
></input>
<input value={"Search"} className='generic-horizontal-input-button' type="submit" />
</div>
</form>
);
}
function TagPageTitle(props: TagPageHeaderProps) {
const [myTagName, setMyTagName] = useState<string>("");
const [isBeingEdited, setIsBeingEdited] = useState<boolean>(false);
const editAreaRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const nameChangeObsId = props.context.db.registerTagNameChangeObserver(props.myTagId, (name: string) => {
setMyTagName(name);
});
setMyTagName(props.context.db.getTagName(props.myTagId));
return () => {
props.context.db.removeTagNameChangeObserver(props.myTagId, nameChangeObsId);
}
}, [props.myTagId]);
useEffect(() => {
}, []);
const submitNameChange = async (newName: string) => {
//This was to check to make sure I wasn't being bitten by ref comparison or something
//console.log("Name: " + myTagName + " == " + newName + "? ")
//console.log(myTagName == newName)
if(newName == myTagName)
return;
const succ = await props.context.db.renameTagEndpoint(props.myTagId, newName);
if(!succ && editAreaRef.current) {
editAreaRef.current.textContent = myTagName;
}
}
const titleFocusLossHandler = (e: React.FocusEvent) =>
{
const content = editAreaRef.current?.textContent;
//console.log(content);
submitNameChange("" + content);
window.getSelection()?.removeAllRanges();
setIsBeingEdited(false);
}
const titleEnterHandler = (e: React.KeyboardEvent) => {
if (e.key == "Enter")
{
e.preventDefault();
;
//trigger titlefocuslosshandler
if(editAreaRef == null)
return;
if(editAreaRef.current == null)
return;
editAreaRef.current.blur();
}
}
const renderedTagName = isBeingEdited ? myTagName : parseTagName(myTagName);
return (
<div
suppressContentEditableWarning={true}
className="tag-content-view-header-field"
ref={editAreaRef}
contentEditable={true}
onChange={(e: any) => {console.log(e.value)}}
onKeyDown={titleEnterHandler}
onBlur={titleFocusLossHandler}
onFocus={(e: any) => {setIsBeingEdited(true)}}
spellCheck={isBeingEdited}>
{renderedTagName}
</div>
)
}
//Todo filter from parseTagName when not editing.
function TagPageHeader(props: TagPageHeaderProps) {
const [searchQuery, setSearchQuery] = useState<string>("");
const [loadState, setLoadState] = useState<number>(ContextStatus.LoadingTags);
useWatchObservableValue(props.context.status, (stat: number | undefined) => {
if(!stat) {
setLoadState(0);
} else {
setLoadState(stat);
}
});
useWatchObservableValue(props.context.searchQueryString, (val: string | undefined) => {
if (val) {
setSearchQuery(val);
} else {
setSearchQuery("");
}
});
let jsx = <></>;
let style = {};
if(loadState == ContextStatus.Ready) {
if (searchQuery) {
jsx = <TagPageSearchBar context={props.context} query={searchQuery} />;
} else if (props.selectedTags.size == 1) {
jsx = <TagPageTitle {...props} />;
} else {
style = { display: "none" };
}
}
//and if selected tags is > 1 then.... Then we have a special search query page :3
return (<div style={style} ref={props.borkRef} className="tag-content-view-header">
{jsx}
</div>);
}
enum BottomBarState
{
GalleryView,
ObjectViewer,
}
interface ObjectGalleryStatusBarProps
{
binderContext: BinderContext;
}
function BottomBar(props: ObjectGalleryStatusBarProps) {
const [queueSize, setQueueSize] = useState<number>(0);
const [selectedCount, setSelectedCount] = useState<number>(0);
useEffect(() => {
const objectCounter = props.binderContext.objectQueue; //kinda hate tracking the whole queue...
const selectedCounter = props.binderContext.exactSelectedObjectCounter;
const syncQueueCount = () => {
const newQeueue = objectCounter.get();
if(!newQeueue) {
setQueueSize(0);
} else {
setQueueSize(newQeueue.length);
}
}
syncQueueCount();
const objectCountObsId = objectCounter.register(syncQueueCount);
const syncSelectedCounter = () => {
const count = selectedCounter.get();
if(!count) {
setSelectedCount(0);
} else {
setSelectedCount(count);
}
}
syncSelectedCounter();
const selectedCountObsId = selectedCounter.register(syncSelectedCounter);
return () => {
objectCounter.unregister(objectCountObsId);
selectedCounter.unregister(selectedCountObsId);
};
}, []);
if(!queueSize)
{
return (<div className={"object-gallery-status-bar"}>
<p> </p>
</div>);
}
else if(selectedCount)
{
const percent = Math.round(selectedCount / queueSize * 100);
return (<div className={"object-gallery-status-bar"}>
<p>{selectedCount}/{queueSize} objects selected ({percent}%)</p>
</div>);
}
return (<div className={"object-gallery-status-bar"}>
<p>{queueSize} objects</p>
</div>);
}
function TagPage(props: TagPageProps) {
const [inSearchContext, setInSearchContext] = useState<boolean>(false);
const [selectedTags, setSelectedTags] = useState<Set<TagId>>(new Set<TagId>());
const [potentiallyInvisibleHeaderHeight, setPotentiallyInvisibleHeaderHeight] = useState<number>(0);
const [viewAreaHeight, setViewAreaHeight] = useState<number>(0);
const [nominalScroll, setNominalScroll] = useState<number>(0);
const scrollableRef = useRef<HTMLDivElement>(null);
const myRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const galleryRef = useRef(null);
//These are separate cause I assumed the ObjectViewer is what captured
// the arrow keys but of course not. If we're re-rendering this errytime
// anyway this might as well be the same stator...
//const singleObjectStator = useWatchObjectViewerOpenStator(props.appContext);
const context = getBinderListStator(props.appContext).getBinder(props.binderId).context;
let galleryBaseOffset = 0;
if (selectedTags.size == 1 && !inSearchContext) {
galleryBaseOffset = potentiallyInvisibleHeaderHeight;
}
useEffect(() => {
const sync = () => {
if(context.searchQueryString.get()) {
setInSearchContext(true);
} else {
setInSearchContext(false);
}
};
sync();
const id = context.searchQueryString.register(sync);
return () => {
context.searchQueryString.unregister(id);
}
},[]);
useWatchObservableValue(context.currentOpenObject, (val: any) => {
if(!myRef.current) {
return;
}
if(val) {
//myRef.current.blur();
} else {
//console.log("focusing tag page");
myRef.current.focus();
}
});
useEffect(() => {
//We know the tag selection changes when the queue is different sooo
//It would be better to have a "global observer" on the observer class for
// the tagSelectionSentinel but we don't so w/e.
//What's nice here anyway is that we can also bind these changes in the objectGallery :3
//Which needs its own copy of the object list for some reason I forget
//Wait it's just for loading state? Why can't we do that here? It's relevant to
// the tag page...
const watcher = props.binderContext.tagSelectionChangeWatcher;
const tagSelectionChangeWatcher = watcher.register(() => {
//console.log("tag change");
setSelectedTags(props.binderContext.getSelectedTags());
if (scrollableRef.current) {
scrollableRef.current.scrollTop = 0;
}
});
return function () {
watcher.unregister(tagSelectionChangeWatcher);
};
}, []);
useEffect(() => {
if(!headerRef) {
return;
}
if (!headerRef.current)
return;
const resizeObserver = new ResizeObserver((e: Array<ResizeObserverEntry>) => {
if (headerRef.current) {
//console.log("resized header");
let height = 0;
height = headerRef.current.offsetHeight;
const compedStyle = window.getComputedStyle(headerRef.current)
height += parseInt(compedStyle.getPropertyValue('margin-top'));
height += parseInt(compedStyle.getPropertyValue('margin-bottom'));
setPotentiallyInvisibleHeaderHeight(height);
//props.doRender isn't updated yet when we need this
/*
const rowCount = style.gridTemplateColumns.split(" ").length;
context.setRowWidth(rowCount);
*/
}
});
resizeObserver.observe(headerRef.current);
return function () {
//capture a damn copy of the lil fucker
const currentRef = headerRef.current;
if (!currentRef) {
console.warn("could not unobserve resize, this is okay if this was a live reload");
return;
}
resizeObserver.unobserve(currentRef);
}
}, [headerRef]);
useEffect(() => {
if(!scrollableRef?.current) {
return;
}
const resizeObserver = new ResizeObserver((e: Array<ResizeObserverEntry>) => {
if (scrollableRef.current) {
if(context.currentOpenObject.get()) {
//Otherwise it resizes and forces a reload when you close it...
return;
}
const newHeight = scrollableRef.current.offsetHeight;
//console.log("new height: " + newHeight);
setViewAreaHeight(newHeight);
//props.doRender isn't updated yet when we need this
/*
const rowCount = style.gridTemplateColumns.split(" ").length;
context.setRowWidth(rowCount);
*/
}
});
resizeObserver.observe(scrollableRef.current);
return function () {
//capture a damn copy of the lil fucker
const currentRef = scrollableRef.current;
if (!currentRef) {
console.warn("could not unobserve resize, this is okay if this was a live reload");
return;
}
resizeObserver.unobserve(currentRef);
}
}, [scrollableRef.current, viewAreaHeight]);
/*
useEffect(() => {
if(!myRef.current) {
return;
}
myRef.current.addEventListener("scroll", (ev: Event) => {
});
return () => {
if(!myRef.current) {
return;
}
};
}, [myRef]);
*/
const handleScroll = (e: any) => {
//Alright so if it's within a given threshold it'll change the gallery scroll offset, to avoid
// doing ANYTHING that could trigger a re-render until absolutely necessary.
if (scrollableRef.current) {
let s = galleryBaseOffset;
const myThreshold = viewAreaHeight * rerenderThresholdFactor;
const adjustedCurrentScroll = scrollableRef.current.scrollTop - galleryBaseOffset;
if(Math.abs(nominalScroll - adjustedCurrentScroll) >= myThreshold) {
//console.log("Adjusting nominal scroll");
setNominalScroll(adjustedCurrentScroll);
}
/*
console.log("Gallery offset btw: " + galleryBaseOffset);
console.log("Scrolled to: " + scrollableRef.current.scrollTop);
*/
}
};
const selectWithOffset = (i: number) =>
{
/*
const mapper = getMapper(props.binderId)
if(!mapper) return;
const selected = mapper.getSelected();
//console.log(selected);
if(selected.length == 0)
{
singleObjectStator.close();
return;
}
let selectedSingle: BinderObject;
if(selected.length > 1)
{
if(i > 0) //Choose the object on the far right end
selectedSingle = selected[selected.length - 1];
else //choose the object on the far left end
selectedSingle = selected[0];
}
else
{
selectedSingle = selected[0];
}
const newSelection = mapper.handleSelectObjectByIndex(selectedSingle.index + i);
setSingleObject(newSelection.id);
*/
}
function downHandler(e: React.KeyboardEvent<HTMLDivElement>) {
//console.log("Keydown!");
if(!props.doRender) //invisible so skip
return;
//TODO still check if we have focus...
if (e.ctrlKey && e.key == "a")
{
if(e.shiftKey || e.altKey) //I prefer shift but chrome captures "ctrl shift a"
props.binderContext.invertObjectSelection();
else
props.binderContext.selectAllObjects();
e.preventDefault();
e.stopPropagation();
return;
}
//This is the only case where we need these keys
if(!props.binderContext.objectsSelectedOrNotSignal.get())
return;
if(!e.repeat) //No repeats...
{
if(e.key == "ArrowLeft")
{
e.preventDefault();
e.stopPropagation();
context.selectPrevious();
}
if(e.key == "ArrowRight")
{
e.preventDefault();
e.stopPropagation();
context.selectNext();
}
if(e.key == " ")
{
e.preventDefault();
e.stopPropagation();
if(!e.ctrlKey)
context.selectNext();
else
context.selectPrevious();
}
if(e.key == "ArrowUp")
{
e.preventDefault();
e.stopPropagation();
context.selectPreviousRow();
}
if(e.key == "ArrowDown")
{
e.preventDefault();
e.stopPropagation();
context.selectNextRow();
}
if(e.key == "Enter")
{
e.preventDefault();
e.stopPropagation();
if(!context.currentOpenObject.get()) {
context.openViewer(undefined); //should fallback on last select index
} else {
context.closeViewer();
}
}
if(e.key == "Escape" && context.currentOpenObject.get())
{
e.preventDefault();
e.stopPropagation();
}
}
}
//getRootStator(props.appContext).registerDownHandler(downHandler);
/*
useEffect(() =>
{
//console.log("Registering down handler");
document.addEventListener("keydown", downHandler)
return function ()
{
//console.log("unregistering down handler");
document.removeEventListener("keydown", downHandler)
}
}, [downHandler, props.doRender]) //Hate that this registers and unregisters every update...
*/
//Focus on render so that keyboard events are intuitive
useEffect(() =>
{
if(myRef == null)
return
if(myRef.current == null)
return;
//typescript going crazy on this w/e
//trus me bro
// @ts-ignore
myRef.current.focus();
});
const whitespaceClickHandler = (e: React.MouseEvent) => {
e.stopPropagation();
if (e.ctrlKey || e.shiftKey) return;
props.binderContext.clearObjectSelection();
};
let myTagId: string = "";
if (selectedTags.size == 1) {
selectedTags.forEach((val: string) => {
myTagId = val;
});
}
//const style = singleObjectStator.viewerOpen ? {display: "none"} : {};
//The separations of concerns here sucks
const mouseDownHandler = (e: any) => {
if(myRef.current) {
myRef.current.focus();
}
};
return (
<div className={"tag-page-container"}
>
<div className="tag-page"
tabIndex={1}
onKeyDown={downHandler}
onMouseDown={mouseDownHandler}
ref={myRef}>
<div
ref={scrollableRef}
onScroll={handleScroll}
className="tag-content-view"
onClick={whitespaceClickHandler}
>
<TagPageHeader
context={props.binderContext}
myTagId={myTagId}
binderId={props.binderId}
selectedTags={selectedTags}
key={myTagId}
borkRef={headerRef} />
<ObjectGallery
viewAreaHeight={viewAreaHeight}
nominalOffset={nominalScroll}
theBinderId={props.binderId}
tagSelectionCount={selectedTags.size}
appContext={props.appContext}
/>
</div>
<BottomBar binderContext={props.binderContext} />
</div>
<ObjectViewer
theBinderId={props.binderId}
appContext={props.appContext}
/>
</div>
);
}
export { TagPage, TagPageLoadState }

View File

@ -0,0 +1,152 @@
.tag-list-item-root-container
{
position: relative;
}
.tag-list-item-drop-zone-layover
{
z-index: 100;
height: 32px;
width: 100%;
background-color: white;
opacity: .5;
position: absolute;
}
.tag-list-item-drop-zone-layover-tween
{
height: 16px;
margin-top: 8px;
opacity: .8;
}
.tag-list-item
{
display: flex;
flex-wrap: nowrap;
position: relative; /*so tag modifiers can be absolute*/
/*idk why but I'm getting 3px underneath if I don't crop it off so WHATEVER I"M TIRED*/
height: 32px;
}
.tag-list-item-button
{
font-size: 15px;
color: white; /*font*/
white-space: nowrap;
border-radius: 0px;
border: 0px;
border-color: rgb(78, 78, 78);
height: 32px;
min-width: 30px;
}
.tag-list-item-button:hover
{
background-color: #45285f;
}
.tag-list-item-button:active
{
background-color: #1E0E2E;
}
.tag-list-item-body-button
{
width: 100%;
text-align: left;
background-color: unset;
text-overflow: ellipsis;
}
.tag-list-item-body-with-children {
padding-left: 4px;
}
.tag-list-item-current
{
/*margin-left: 5px; */
background-color: #48e43d;
color: black;
}
.tag-list-item-current:hover
{
background-color: #48e43d;
}
.tag-list-item-modifier-container
{
margin-left: auto;
display:flex;
flex-wrap: nowrap;
position: absolute;
right: 0px;
padding: 0px;
margin: 0px;
background-color: #260C3C;
}
.tag-list-item-child-tag-container
{
margin-left: 30px; /*so that it aligns with the parent text*/
}
.tag-list-item-hidden
{
display:none;
}
.tag-list-item-mini-button
{
height: 14px;
width: 20px;
padding-top: 9px;
padding-bottom: 9px;
padding-left: 0px;
padding-right: 0px;
margin: 0px;
}
.tag-list-item-mini-button-current-tag
{
background-color: #48e43d;
color: black;
}
.tag-list-item-mini-button-current-tag:hover
{
background-color: #52D249;
}
.tag-list-item-mini-button-current-tag:active
{
background-color: #4bb843;
}
.tag-list-mini-button-add
{
}
.tag-list-mini-button-remove
{
background-color: #67468a;
}
.tag-list-mini-button-protecc
{
background-color: #67468a;
}
.tag-list-mini-button-protecc:hover
{
background-color: #67468a;
}
.tag-list-mini-button-protecc:active
{
background-color: #67468a;
}

View File

@ -0,0 +1,804 @@
import React, {useState, useEffect, useRef} from 'react'
import {BinderConnectionId, TagId, Position} from '../backend'
import './taglistitem.css'
import { getContextMenuStator, ContextMenuData } from './contextmenu';
import { StatorContext } from '../statorobject';
import { parseTagName } from '../tagnamefunction';
import { BinderContext } from './bindercontext';
enum TagAction
{
None,
Add,
Remove,
AddRemove,
};
class TagListItemUpdatePayload
{
name: String = "";
selected: Boolean = false;
action = TagAction.None;
hideFromFilter: Boolean = false;
childHasSomethingToSay: Boolean = false;
childIsSelected: Boolean = false;
}
export interface TagListItemProps
{
binderContext: BinderContext;
theBinderId: BinderConnectionId;
tagId: TagId;
appContext: StatorContext;
}
interface TagMiniButtonProps
{
symbol: String;
doRender: Boolean;
currentTag: Boolean;
greenCauseChildSelection: Boolean;
callback: (e: any) => void;
}
function TagMiniButton(props: TagMiniButtonProps)
{
if(!props.doRender)
return null;
const imgSrc: string = (() =>
{
//literally just realized I don't have plus/minus symbols and if we're using
//imgs we.. Need that... shit
let result = "";
if(props.symbol == '+')
result = "static/plus-button";
else if(props.symbol == '-')
result = "static/minus-button";
else if (props.symbol == "▸")
result = "static/closed-tag-tree-arrow";
else if(props.symbol == '⤥')
result = "static/child-tag-attention-tag-tree-arrow";
else if (props.symbol == '▾')
result = "static/open-tag-tree-arrow";
else if (props.symbol == "0") {
result = "static/related-protected"
}
else
throw "Could not load svg for symbol: " + props.symbol;
if(props.symbol != "0") {
if (props.currentTag || (props.greenCauseChildSelection && props.symbol == '⤥'))
result += "-black";
}
return result + ".svg";
})();
const plusMinusStyling = (() => {
if(props.currentTag)
return "";
//God damn this is crufty
if(props.symbol == '+')
{
return "tag-list-mini-button-add";
}
else if(props.symbol == '⤥')
{
const result = "tag-list-mini-button-remove";
if(props.greenCauseChildSelection)
return result + " vibrant-action-button";
return result;
}
else if(props.symbol == '-')
{
return "tag-list-mini-button-remove";
}
else if(props.symbol == '0') {
return "tag-list-mini-button-protecc";
}
})();
return (
<div>
<img className={`tag-list-item-button
tag-list-item-mini-button ${plusMinusStyling}
${props.currentTag ? "tag-list-item-mini-button-current-tag" : ""}`}
onClick={props.callback}
src={imgSrc}
draggable={false}
/>
</div>
);
}
interface TagTreeListViewProps
{
binderContext: BinderContext;
parentTag: TagId;
theBinderId: BinderConnectionId;
appContext: StatorContext;
}
function InternalTagList(props: TagTreeListViewProps) {
const [tagList, setTagList] = useState(new Array<TagId>());
useEffect(() => {
const updateTagList = () => {
//Really there should be a "is reserved?" test and if it is just register with the appropriat ething...
//really really it should be a flag to test if it's limited or not...
if(!props.binderContext.db.checkLimitTagChildrenToObjectSelection(props.parentTag)) {
setTagList(props.binderContext.db.getSortedTags(props.parentTag));
} else { //reserved
//limit because we don't have lazy loading (yet, TODO)
const nList = new Array<TagId>();
const limit = 500;
let i = 0;
for(const tagId of props.binderContext.getNoisyChildrenOfParentsWhoAretired(props.parentTag)) {
if(i++ == limit) {
break;
}
nList.push(tagId);
}
setTagList(nList);
}
};
updateTagList(); //initialize it
//This might cause a double update, unfortunate but it is what it is...
//I can't think of a clean way of solving this problem that handles all the edge cases.
//At the very least it won't trigger a second render, so... yay react?
const sortTableObsId = props.binderContext.db.registerChildrenOrderChange(props.parentTag, () => {
//console.log("Updating taglistinternal from sort table");
updateTagList();
});
const childrenRelationShipObsId = props.binderContext.db.registerSpecificChildrenObserver(props.parentTag, () => {
//console.log("Updating taglistinternal from relationshipchange");
//const newList = props.binderContext.db.getSortedTags(props.parentTag);
//console.log("List size: " + newList.length + ", vals: " + newList);
updateTagList();
//updateTagList();
});
const parentsSomethingSayDeps = props.binderContext.getParentsWhoseChildrenHaveSomethingToSayAlwaysCounter();
const parentsCounterObsId = parentsSomethingSayDeps.register(props.parentTag, () => {
updateTagList();
});
return (() => {
props.binderContext.db.removeChildrenOrderChangeObserver(props.parentTag, sortTableObsId);
props.binderContext.db.removeSpecificChildrenObserver(props.parentTag, childrenRelationShipObsId);
parentsSomethingSayDeps.unregister(props.parentTag, parentsCounterObsId);
});
}, []);
//Okay sooooooo tagList should be a useState, and a use effect needs to register an observer
// or two on the TagDatabase for when the list changes :3
return (
<div>
{tagList.map((childId: TagId ) => {
return (
<TagListItem key={childId}
tagId={childId}
theBinderId={props.theBinderId}
appContext={props.appContext}
binderContext={props.binderContext}
/>
);
})}
</div>);
}
interface TagListProps {
binderContext: BinderContext;
theBinderId: BinderConnectionId;
parentTag: TagId;
appContext: StatorContext;
}
enum DropZoneState
{
Above,
Center,
Below,
}
interface TagItemDragData
{
dragType: string;
tagId: TagId;
}
interface DragTargetData
{
tagId: TagId;
openWithChildren: boolean;
}
const TagDropTargetDataType = "drag-target-data";
enum DragTargetState
{
None,
Above,
Center,
Below,
}
const TagItemDragDataType = "tagitemdragdata"; //Lowercased! Cause it will be lowercased if we don't do it!
function TagList(props: TagListProps) {
const [dragState, setDragState] = useState<DragTargetState>(DragTargetState.None)
const [layoverOffset, setLayoverOffset] = useState<number>(0);
const dropZoneContainerRef = useRef<HTMLDivElement>(null);
const tagDb = props.binderContext.db;
//ref={dropZoneContainerRef}
//onDragOver={myDragOverHandler}
//onDragEnter={myDragEnterHandler}
//onDrop={myOnDropHandler}
function myDragOverHandler(e: React.DragEvent)
{
if(!e.dataTransfer.types.includes(TagItemDragDataType))
return;
e.preventDefault();
if(!dropZoneContainerRef.current)
return;
const ourYoffset = dropZoneContainerRef.current.getBoundingClientRect().y;
const dragYoffset = e.pageY;
const relativeOffset = dragYoffset - ourYoffset;
//I feel like there's gotta be a cleaner way to express this but w/e
// Bascially we want to round down to the nearest 32nd
const layoverBaseOffset = Math.floor(relativeOffset / 32) * 32;
//console.log("my layout base offset: " + layoverBaseOffset);
//32 is the height of the item and is very carefully chosen because we can divide it evenly twice
// at the least
//Our regions are 8 + 16 + 8 (32)
// because we want the lower region to collapse with the upper of the next region
const ourModulus = relativeOffset % 32;
//console.log("Modulous: " + ourModulus);
if (ourModulus >= 24) //8 + 16 (extent of the upper and mid-region)
{
//console.log("below");
setLayoverOffset(layoverBaseOffset + 16);
setDragState(DragTargetState.Below);
}
else if (ourModulus >= 8) //8 is the extent of the upper region
{
//console.log("middle");
setLayoverOffset(layoverBaseOffset); //is just fine as it is I think it's pretty just the way it is
setDragState(DragTargetState.Center);
}
else if (ourModulus < 8)
{
setLayoverOffset(layoverBaseOffset - 16);
setDragState(DragTargetState.Above);
}
}
function myDragEnterHandler(e: React.DragEvent)
{
e.preventDefault();
}
function myDragLeaveHandler(e: React.DragEvent)
{
e.preventDefault()
}
function myOnDropHandler(e: React.DragEvent)
{
if(!e.dataTransfer.types.includes(TagItemDragDataType))
return;
e.preventDefault();
//Do this before trying anything funny cause otherwise we could be locked in a ghost
// drag layout situation
setDragState(DragTargetState.None);
try
{
const data = JSON.parse(e.dataTransfer.getData(TagItemDragDataType)) as TagItemDragData;
//console.log("Accepted!");
for (const val of document.elementsFromPoint(e.pageX, e.pageY)) //PAGE NOT RELATIVE OFFSET!!!
{
const rawDragTargetData = val.getAttribute(TagDropTargetDataType);
if(!rawDragTargetData)
continue; //find me one that isn't fucking null, then
const targetTag = JSON.parse(rawDragTargetData) as DragTargetData;
if (targetTag)
{
switch(dragState)
{
case DragTargetState.Above:
tagDb.repositionTag(data.tagId, targetTag.tagId, Position.Before);
break;
case DragTargetState.Center:
tagDb.setTagParent(data.tagId, targetTag.tagId);
break;
case DragTargetState.Below:
//If it's dropped below a tag that's open with children, then
// we want it to parent ABOVE the first item in the list.
// so, PREPEND NOT REPARENT, and the backend handles reparenting
if(targetTag.openWithChildren) {
//BUG: I think this just skips it rather than repositioning it if the parent is already the same...
//Testing it, the old version works the same way and I didn't seem to care idk TODO look into it later
tagDb.setTagParent(data.tagId, targetTag.tagId);
} else {
tagDb.repositionTag(data.tagId, targetTag.tagId, Position.After);
}
break;
}
break;
}
}
} catch(e: any) {
console.log("Failed to accept drop: " + e);
}
}
//We need the tag list to be top level cause while it's basically
// recursive it's managed by the root
//Uhhh?? how does this affect what we're doing now?
//I think this comment makes no sense now...
let layoverStyle = {display: "none", top: 0}
if(dragState != DragTargetState.None)
{
layoverStyle.display = "block";
layoverStyle.top = layoverOffset;
}
return (
<div className='tag-list-item-root-container'
ref={dropZoneContainerRef}
onDragOver={myDragOverHandler}
onDragEnter={myDragEnterHandler}
onDragLeave={myDragLeaveHandler}
onDrop={myOnDropHandler}
>
<InternalTagList
{...props}
/>
<div className={`tag-list-item-drop-zone-layover
${dragState != DragTargetState.Center ? "tag-list-item-drop-zone-layover-tween" : ""}`}
style={layoverStyle}>
</div>
</div>)
}
function TagListItem(props: TagListItemProps) {
const [isSelected, setIsSelected] = useState<Boolean>(false);
const [isHidden, setIsHidden] = useState<Boolean>(false);
const [showChildren, setShowChildren] = useState<Boolean>(false);
const [hasChildren, setHasChildren] = useState<Boolean>(false);
//TODO: The content of the tag item itself should probably be in a separate
// component so it doesn't force a re-render of the children whenever this stuff changes...
const [childrenHaveSomethingToSay, setChildrenHaveSomethingToSay] = useState<Boolean>(false);
const [childIsSelected, setChildIsSelected] = useState<Boolean>(false);
const [areObjectsSelected, setAreObjectSelected] = useState<boolean>(false);
//Use this to track the current tag action, but NOT whether it's none or not.
// we use areObjectsSelected to compute whether this is relevant so it needs to reset to
// TagAction.Add
const [explicitlySetTagAction, setExplicitlySetTagAction] = useState<TagAction>(TagAction.Add);
const [tagName, setTagName] = useState<String>("");
const tagListItemRef = useRef<HTMLDivElement>(null);
const dropZoneRef = useRef<HTMLDivElement> (null);
const tagDb = props.binderContext.db;
useEffect(() => {
if(!props.binderContext.db.checkLimitTagChildrenToObjectSelection(props.tagId)) {
setHasChildren(tagDb.checkTagHasChildren(props.tagId));
}
setIsSelected(props.binderContext.tagSelectionSentinel.get(props.tagId) != undefined);
const syncObjectSelectedOrNot = () => {
//So this should only fire on first selection, and it's suppose to pump the whole of all the
// tags, and then later the more relevant one fires. Or it's saying we're all done, so
// that's simpol as well :3
if (props.binderContext.objectsSelectedOrNotSignal.get()) {
setAreObjectSelected(true);
} else {
setAreObjectSelected(false);
}
}
const objectdSelectedOrNotId = props.binderContext.objectsSelectedOrNotSignal.register(() => {
syncObjectSelectedOrNot();
});
syncObjectSelectedOrNot();
const syncTagAction = () => {
const tagAction = props.binderContext.tagActionObservers.get(props.tagId);
if (tagAction != undefined) {
setExplicitlySetTagAction(tagAction);
} else {
setExplicitlySetTagAction(TagAction.Add); //reset to default
}
};
syncTagAction();
const tagActionObserverId = props.binderContext.tagActionObservers.register(props.tagId, () => {
syncTagAction();
});
const tagSelectionObserverId = props.binderContext.tagSelectionSentinel.register(props.tagId, () => {
//!!
setIsSelected(!!props.binderContext.tagSelectionSentinel.get(props.tagId));
});
const childSomethingSayCounter = props.binderContext.getParentsWhoseChildrenHaveSomethingToSayCounter();
const syncChildrenSomethingToSay = () => {
setChildrenHaveSomethingToSay(!!childSomethingSayCounter.get(props.tagId));
};
syncChildrenSomethingToSay();
const childrenHaveSomethingToSayObsId = childSomethingSayCounter.register(props.tagId, () => {
syncChildrenSomethingToSay();
});
const childrenSomethingToSayGreenEdition = props.binderContext.getParentsWhoseChildrenHaveSomethingToSayGreenEditionCounter();
const syncGreenEdition = () => {
setChildIsSelected(!!childrenSomethingToSayGreenEdition.get(props.tagId));
};
syncGreenEdition();
const childrenHaveSomethingToSayGreenEditionId = childrenSomethingToSayGreenEdition.register(props.tagId, () => {
syncGreenEdition();
});
const tagNameChangeObserverId = props.binderContext.db.registerTagNameChangeObserver(props.tagId, (name: string) => {
if(!name)
name = "";
setTagName(parseTagName(name));
});
setTagName(parseTagName(tagDb.getTagName(props.tagId)));
//TODO: filtering for the search results...
// easiest to handle this centrally like the mapper of old...
const childrenChangedId = props.binderContext.db.registerSpecificChildrenObserver(props.tagId, () => {
setHasChildren(tagDb.checkTagHasChildren(props.tagId));
});
return () => {
props.binderContext.objectsSelectedOrNotSignal.unregister(objectdSelectedOrNotId);
props.binderContext.tagActionObservers.unregister(props.tagId, tagActionObserverId);
props.binderContext.tagSelectionSentinel.unregister(props.tagId, tagSelectionObserverId);
childSomethingSayCounter.unregister(props.tagId, childrenHaveSomethingToSayObsId);
//console.log("Unregistering!!!!! green edition for tag: " + props.tagId);
childrenSomethingToSayGreenEdition.unregister(props.tagId, childrenHaveSomethingToSayGreenEditionId);
props.binderContext.db.removeTagNameChangeObserver(props.tagId, tagNameChangeObserverId);
props.binderContext.db.removeSpecificChildrenObserver(props.tagId, childrenChangedId);
};
}, []);
useEffect(() => {
if(!props.binderContext.db.checkLimitTagChildrenToObjectSelection(props.tagId)) {
return;
}
const counter = props.binderContext.getParentsWhoseChildrenHaveSomethingToSayCounter();
const sync = () => {
if(counter.get(props.tagId)) {
setHasChildren(true);
} else {
setHasChildren(false);
}
}
sync();
const id = counter.register(props.tagId, () => {
sync();
});
//This only gets returned if we register, so it's fine
return () => {
counter.unregister(props.tagId, id);
}
});
const toggleShowChildren = (e: any): void => {
setShowChildren(!showChildren);
}
//This is GROSS
//I think it would be better to own the... well the list for the current tags or...
// have the child tag.. idk have a counter or something so if it's the last child it goes away?
// Ughh...
const addTagCallback = () => {
const selectedObjectList = props.binderContext.getSelectedObjects();
if (!selectedObjectList) {
return;
}
tagDb.addTagToObjects(props.tagId, selectedObjectList);
}
const removeTagCallback = () => {
const selectedObjectList = props.binderContext.getSelectedObjects();
if(!selectedObjectList) {
return;
}
tagDb.removeTagFromObjects(props.tagId, selectedObjectList);
};
const tagClickedHandler = (e: React.MouseEvent) => {
if(!e.ctrlKey && !e.shiftKey) {
props.binderContext.setSingleTagSelection(props.tagId);
/*
const currentlySelected = props.binderContext.getSelectedTags();
for(const selTag of currentlySelected) {
if(selTag == props.tagId) {
continue;
}
props.binderContext.setTagSelection(selTag, false);
}
props.binderContext.setTagSelection(props.tagId, true);
*/
} else if(e.ctrlKey && !e.shiftKey) {
props.binderContext.toggleTagSelection(props.tagId);
} //Else we do shift and go down the sorted tags list hmmm... Only truly useful with
// drag and drop tho, which we don't have for multiple tags yet...
};
const selectedTagClass = isSelected ? "tag-list-item-current" : "";
const myDragStartHandler = (e: React.DragEvent<HTMLDivElement>) =>
{
const data = { dragType: "tag", tagId: props.tagId.toString() }
const dataString = JSON.stringify(data);
//console.log("Setting: " + dataString)
e.dataTransfer.setData(TagItemDragDataType, dataString)
//We don't set the dropTarget to ourselves even though that's valid
// because it'll restyle the dragged element bubble thing and we don't want that.
}
const myDragEnterHandler = (e: React.DragEvent<HTMLDivElement>) =>
{
if(!e.dataTransfer.types.includes(TagItemDragDataType)) //lowercased here only
return;
e.preventDefault();
}
const contextMenuHandler = (e: React.MouseEvent) =>
{
const menu: ContextMenuData =
{
spawnX: e.pageX,
spawnY: e.pageY,
contextMenuSections:
[
{
contextMenuItems:
[
{
title: "New Tag Here",
keyBinding: "",
action: () => {
//I like that I can do this on the fly now, but also kinda... don't know if this
// should be encapsulated here or not... idk...
props.binderContext.createNewTagAndAddSelected(tagDb.getTagParent(props.tagId), props.tagId);
}
},
{
title: "New Child Tag",
keyBinding: "",
action: () => {
props.binderContext.createNewTagAndAddSelected(props.tagId);
/*
//unpositioned tags bubble to the top anyway so w/e
const childTagList = tagDb.getSortedTags(props.tagId);
if(childTagList.length < 1) {
console.warn("Just created a tag but the child list is empty");
return; //Weird
}
const topTagId = childTagList[0];
if(topTagId == createdTag) {
return; //nothing to see here
}
//tagDb.repositionTag(createdTag, topTagId, Position.Before);
*/
}
},
{
title: "Delete Tag",
keyBinding: "",
action: () => {
props.binderContext.deleteTag(props.tagId);
}
},
],
},
/*
{
contextMenuItems:
[
{
title: "Include object on child tags",
keyBinding: "",
action: () => { }
},
{
title: "Show only children on selected",
keyBinding: "",
action: () => { }
}
]
}
*/
],
}
e.preventDefault();
getContextMenuStator(props.appContext).spawn(menu);
}
const style = isHidden ? { display: "none" } : {};
if(dropZoneRef.current)
{
const data =
{
tagId: props.tagId,
openWithChildren: showChildren,
} as DragTargetData
const dataAsString = JSON.stringify(data);
dropZoneRef.current.setAttribute(TagDropTargetDataType, dataAsString);
}
const dropdownSymbol: string = (() => {
if(showChildren)
return "▾";
if(childrenHaveSomethingToSay || childIsSelected)
return "⤥";
return "▸";
})();
const childList = showChildren ? (
<div className="tag-list-item-child-tag-container">
<InternalTagList parentTag={props.tagId}
theBinderId={props.theBinderId}
appContext={props.appContext}
binderContext={props.binderContext}
/>
</div>) : undefined;
//It was three cause at some point I had the id or data-tag-id set on both the container and tag item
// but it stayed that way during a hot reload and after a full refresh it's just two again
const computedTagAction = areObjectsSelected ? explicitlySetTagAction : TagAction.None;
const hasChildrenClass = hasChildren ? "tag-list-item-body-with-children" : "";
const renderRelated = (computedTagAction == TagAction.Remove || computedTagAction == TagAction.AddRemove);
const renderUntouchableRelated = renderRelated && props.binderContext.db.checkLimitTagChildrenToObjectSelection(props.binderContext.db.getTagParent(props.tagId));
const limitedTag = props.binderContext.db.checkLimitTagChildrenToObjectSelection(props.tagId);
return (
<div
ref={dropZoneRef}
id={props.tagId.toString()}
style={style}
>
<div className={`tag-list-item`}
ref={tagListItemRef}
onContextMenu={contextMenuHandler}
onDragStart={myDragStartHandler}
draggable={true}
>
<TagMiniButton symbol={dropdownSymbol}
doRender={hasChildren}
currentTag={isSelected}
greenCauseChildSelection={childIsSelected}
callback={toggleShowChildren} />
<button className={`tag-list-item-button tag-list-item-body-button ${selectedTagClass} ${hasChildrenClass}`}
onClick={tagClickedHandler}>
{tagName}
</button>
<div className="tag-list-item-modifier-container">
<TagMiniButton symbol="+"
doRender={((computedTagAction == TagAction.Add || computedTagAction == TagAction.AddRemove) && !renderUntouchableRelated && !limitedTag)}
currentTag={isSelected}
greenCauseChildSelection={false}
callback={(e: any) => addTagCallback()} />
<TagMiniButton symbol="-"
doRender={renderRelated && !renderUntouchableRelated && !limitedTag}
currentTag={isSelected}
greenCauseChildSelection={false}
callback={(e: any) => removeTagCallback()} />
<TagMiniButton symbol="0"
doRender={renderUntouchableRelated && !limitedTag}
currentTag={isSelected}
greenCauseChildSelection={false}
callback={(e: any) => {}} />
</div>
</div>
{childList}
</div>
);
}
export {TagList, TagListItemUpdatePayload, TagAction};

View File

@ -0,0 +1,97 @@
.tag-sidebar
{
display: flex;
flex-direction: column;
width: 250px;
min-width: 65px;
height: 100%;
resize: horizontal;
background-color: #260C3C;
}
.tag-sidebar-tab-bar
{
display: flex;
flex-wrap: nowrap;
background-color: #301749;
}
.tag-sidebar-new-tag-button
{
padding-left: 0px;
padding-right: 0px;
padding-top: 6px;
padding-bottom: 5px;
height: 24px;
width: 100%;
background-color: #301749;
}
.tag-sidebar-new-tag-button:hover
{
background-color: #422a5a;
}
.tag-sidebar-new-tag-button:active
{
background-color: #0d0813;
}
.tag-sidebar-header-input
{
flex: 1;
align-items: center;
font-family: sans-serif;
width: 230px;
margin: 5px;
padding: 5px;
color: white;
height: 15px;
border-style: none;
background-color: transparent;
overflow-wrap: break-word;
resize: none;
}
.tag-sidebar-header-input::placeholder {
color: #6c4981;
}
.tag-sidebar-clear-filter-button
{
font-size: 15pt;
}
.tag-sidebar-header-input::selection
{
background-color: rgb(101, 85, 173);
}
.tag-sidebar-header-input:focus
{
border-style: solid;
border-color: white;
margin: 3px;
border-width: 2px;
outline: none;
border-radius: 3px;
background-color: #260C3C;
}
.tag-sidebar-list-container
{
flex: 1;
overflow-y: scroll;
overflow-x: hidden;
}
.tag-sidebar-list-container > h2
{
display: flex;
justify-content: center;
font-weight: normal;
color: #623a70;
margin: auto;
margin-top: 15px;
}

View File

@ -0,0 +1,203 @@
import React, {useState, useRef} from 'react'
import {TagList} from './taglistitem'
import { BinderConnectionId, BinderTag } from '../backend'
import { StatorContext, StatorObjectBase } from '../statorobject'
import './tagsidebar.css'
import './taglistitem.css'
import './subduedactionbutton.css'
import './navbar.css'
import { BinderContextPair, getBinderListStator, useWatchBinderListStator } from './openbinders'
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;
}
function Header(props: {context: BinderContext})
{
const [value, setValue] = useState<string>("");
const ref = useRef<HTMLInputElement>(null);
const blurHandler = () => {
if (value)
props.context.setSearchQuery(value);
setValue("");
}
const inputHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}
const allDone = (e: any) => {
e.preventDefault();
ref.current?.blur();
}
return (
<div className="tag-sidebar-tab-bar">
{
/*
<button className="tag-sidebar-new-tag-button subdued-action-button"
onClick={handleNewTag}>
+
</button>
*/
}
<form onSubmit={allDone}>
<input className="tag-sidebar-header-input"
ref={ref}
onChange={inputHandler}
onBlur={blurHandler}
value={value}
placeholder={"Search"}
></input>
</form>
</div>
);
}
interface TagSidebarInstanceProps
{
binderContext: BinderContext;
theBinderId: BinderConnectionId;
appContext: StatorContext;
}
function TagSidebarInstance(props: TagSidebarInstanceProps): JSX.Element
{
const binderStator = getBinderListStator(props.appContext);
const [tagList, setTagList] = useState<Array<BinderTag>>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
useWatchObservableValue(props.binderContext.status, (stat: number | undefined) => {
setIsLoading(stat == ContextStatus.LoadingTags);
});
const handleNewTag = () => {
props.binderContext.createNewTagAndAddSelected();
}
const selected = binderStator.checkSelected(props.theBinderId);
const style = selected ? {} : { display: "none" };
//console.log("Show loading? " + isLoading);
return (
<div style={style} className="tag-sidebar">
<Header context={props.binderContext} />
<div className="tag-sidebar-list-container">
{ isLoading ?
<h2>
Loading tags...
</h2>
:
<TagList parentTag={reserved.tagIds.root} // :)
binderContext={props.binderContext}
theBinderId={props.theBinderId}
appContext={props.appContext}
/>
}
</div>
<img className="tag-sidebar-new-tag-button"
src={"static/plus-button.svg"}
draggable={false}
onClick={handleNewTag} />
</div>
);
}
interface TagSidebarProps
{
appContext: StatorContext;
}
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
if(binderListStator.getOpenBinderList().length == 0)
{
return null; //TODO: close it
}
const tagSidebars = binderListStator.getOpenBinderList().map((b: BinderContextPair) =>
{
return (
<TagSidebarInstance key={b.binder.id}
binderContext={b.binder.context}
theBinderId={b.binder.id}
appContext={props.appContext}
/>);
});
const style = tagSidebarStator.hidden ? {display: "none"} : {};
return <div style={style}>{tagSidebars}</div>;
}
interface SidebarViewToggleButtonProps
{
appContext: StatorContext;
}
function SidebarViewToggleButton(props: SidebarViewToggleButtonProps)
{
const sidebarStator = useWatchTagSidebarStator(props.appContext);//Cause this is for global stuff like show/hide
const handleClick = (e: any) =>
{
sidebarStator.toggle();
};
const imgSrc = sidebarStator.hidden ? "static/show-tag-sidebar.svg"
: "static/hide-tag-sidebar.svg";
return (<img
className={"nav-button binder-bar-ham-burber-bubbon"}
draggable={false}
onClick={handleClick}
src={imgSrc}
></img>)
}
export {TagSidebar, SidebarViewToggleButton};

View File

@ -0,0 +1,31 @@
.generic-thumbnail-container
{
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
/*background-color: rgb(200, 178, 207);*/
background-color: #67468a;
color: rgb(206, 206, 206);
position: absolute;
z-index: 3;
}
.generic-thumbnail-container-upper {
flex: 1;
padding: 10px;
overflow:hidden;
font-size: 14px;
overflow-wrap:anywhere;
}
.generic-thumbnail-container-lower {
padding: 10px;
overflow:hidden;
overflow-wrap:anywhere;
color: white;
font-size: 12px;
background-color: rgb(61, 35, 90);
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import './genericthumbnail.css';
interface GenericThumbnailProps {
bodyText: string;
bottomBannerText: string;
}
function GenericThumbnail(props: GenericThumbnailProps) {
return (<div className='generic-thumbnail-container'>
<div className="generic-thumbnail-container-upper">
{props.bodyText}
</div>
<div className='generic-thumbnail-container-lower'>
{props.bottomBannerText}
</div>
</div>);
}
export {GenericThumbnail}

View File

@ -0,0 +1,114 @@
.object-thumbnail
{
height: 150px;
width: 150px;
margin: 6px;
border-radius: 4px;
overflow: hidden;
position: absolute;
}
.object-thumbnail-underlay {
height: 100%;
width: 100%;
z-index: 0;
position: absolute;
}
.object-thumbnail-overlay {
height: 100%;
width: 100%;
z-index: 1;
position: absolute;
}
.object-thumbnail-plaintext-background
{
height: 100%;
width: 100%;
/*background-color: rgb(200, 178, 207);*/
background-color: white;
color: black;
position: absolute;
z-index: 3;
}
.object-thumbnail-plaintext-container
{
margin-top: 10px;
margin-left: 10px;
width: 130px;
height: 130px;
overflow: hidden;
}
.object-thumbnail-text
{
margin: 0px;
}
.object-thumbnail-standin
{
position:absolute;
background-color: #67468a;
color: rgb(141, 116, 153);
width: 100%;
height: 100%;
text-align: center;
font-size: 17pt;
padding-top: 32%;
}
.object-eyecandy-thumbnail
{
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.object-eyecandy-thumbnail-background
{
position: absolute;
/*we stretch them to fit so that it doesn't look like ass if they're
* too far way from being square. They're so blurred you won't notice
* anyway*/
width: 200px;
height: 200px;
left: -25px;
top: -25px;
filter: blur(7px);/**/
z-index: 1;
}
.object-eyecandy-thumbnail-image
{
position: absolute;
max-width: 150px;
max-height: 150px;
background-color: white;
z-index: 2;
}
.object-thumbnail-checkerboard
{
position: absolute;
background-image: url("static/checkerboard.svg");
background-size: 12px;
width: 100%;
height: 100%;
object-fit: contain;
z-index: 2;
}
.object-thumbnail-selected
{
margin: 0px;
border-style: solid;
border-width: 6px;
border-color: white;
box-shadow: 0px 0px 10px white;
}

View File

@ -0,0 +1,413 @@
import React, { useEffect, useState, useRef } from 'react'
//import * as PDFjs from 'pdfjs-dist'
import {BinderConnectionId, BinderObject, IndexedObject, ObjectId, generateThumbnailForObject, getObjectUri, getThumbnailUri} from '../../backend'
import { ObjectViewerProps } from "../objectviewer";
import './objectthumbnail.css'
import {getContextMenuStator} from "../contextmenu";
import { getBinderListStator } from '../openbinders';
import { StatorContext } from '../../statorobject';
import { getMetaApi } from '../../ipcbindings';
import { getObjectViewerOpenStator, getObjectViewerStator } from '../objectviewercontroller';
import { GenericThumbnail } from './genericthumbnail';
import { BinderContext } from '../bindercontext';
//const pdfworker = await import('pdfjs-dist/build/pdf.worker.entry');
//PDFjs.GlobalWorkerOptions.workerSrc = pdfworker;
export interface ObjectThumbnailProps
{
appContext: StatorContext,
binderContext: BinderContext;
obj: IndexedObject;
theBinderId: BinderConnectionId;
x: number;
y: number;
}
class ObjectThumbnailUpdatePayload
{
selected: Boolean = false;
}
function CheckerboardBgImageThumbnail(props: ObjectThumbnailProps)
{
return <></>;
const thumbnailUri = getObjectUri(props.obj.id);
return (
<img className={"object-thumbnail-checkerboard"}
draggable={false}
src={thumbnailUri}
/>
);
}
function EyeCandyImageThumbnail(props: ObjectThumbnailProps) {
const [loaded, setLoaded] = useState(false);
const [lastLoadFailed, setLastLoadFailed] = useState(false);
const [requestedGeneration, setRequestedGeneration] = useState(false);
const [generationFailed, setGenerationFailed] = useState(false);
//const thumbnailUri = "file:///" + props.obj.thumbnailOnDiskPath;
//const thumbnailUri = "http://0.0.0.0:18080/api/binder/" + "standardobjectthumbnail" + "/" + props.theBinderId + "/" + props.obj.b2bsum;
const [thumbnailUri, setThumbnailUri] = useState("");
const [thumbnailJobId, setThumbnailJobId] = useState<number | undefined>(undefined);
useEffect(() => {
(async () => {
const thumbp = await getBinderListStator(props.appContext).getCurrentBinder()?.context.db.getThumbnailPath(props.obj.id);
if(!thumbp) { //this doesn't mean it doesn't exist, it means the binder stator couldn't return a proper context
throw new Error("this should not happen");
setLastLoadFailed(true);
return;
}
setThumbnailUri(thumbp.path);
})();
}, []);
useEffect(() => {
return () => {
if(thumbnailJobId) {
//CANCALE JOBEEE
}
}
}, []);
const loadHandler = (e: any) => {
setLoaded(true);
};
const failureHandler = async (e: any) => {
setLastLoadFailed(true);
if(requestedGeneration) {
setGenerationFailed(true);
//console.log("Already requested thumbnail generation, skipping");
return; //no infinite looopy
}
setRequestedGeneration(true);
//console.log("Requesting thumbnail generation for: " + props.obj.b2bsum);
const currentBinder = getBinderListStator(props.appContext).getCurrentBinder();
if(!currentBinder) {
console.warn("Binder not ready for thumbnail generation");
return;
}
const objectPaths = await currentBinder.context.db.getObjectPath(props.obj.id);
if(!objectPaths) {
console.warn("Object missing");
return;
}
const thumbnailPaths = await currentBinder.context.db.getThumbnailPath(props.obj.id);
if(!thumbnailPaths) {
console.warn("thumbnail path missing");
return;
}
console.log("thumbnail path: " + thumbnailPaths.path);
await getMetaApi().createDirectories(thumbnailPaths.directory);
const jobId = await getMetaApi().generateThumbnailSharp(objectPaths.path, thumbnailPaths.path);
await getMetaApi().longPollThumbnailJob(jobId);
setLastLoadFailed(false);
/*
const jobId = await thumbnailWorker.postJob(objectPaths.path, thumbnailPaths.path, thumbnailPaths.directory, () => {
}, () => {});
*/
setThumbnailJobId(jobId);
};
//standin is from a higher up component so we can just render nothing
if(lastLoadFailed && thumbnailUri && requestedGeneration && !generationFailed) {
return <GenericThumbnail bodyText='' bottomBannerText='Generating Preview...'/>
}
if(!thumbnailUri) {
//Technically, it's not actually "loading" the thumbnail, it's just waiting for the thumbnail uri which
// is actually kind of slow to get back... It would be faster to have that already in the IndexedObject
// structure......
//But I do like that it says loading first...
return <></>; //text/components is apparently too slow. Might just be that it's a react component...
return <GenericThumbnail bodyText='' bottomBannerText='Loading..' />
}
if (generationFailed) {
//mime like 99.999% chance exists already cause it's uh you know
// already requested before we got this far...
// (except the case where it failed to generate because of no disk space and also failed to write mime...)
//TODO: Genericize "failed to load" thumbnail and let it figure out all this shit...
return <GenericThumbnail bodyText={props.binderContext.db.pickFilename(props.obj.id)}
bottomBannerText={props.binderContext.db.getMimeIfExists(props.obj.id)} />
}
const wrappedURI = "file:///" + thumbnailUri;
return (
<div className="object-eyecandy-thumbnail" >
<img
className={"object-eyecandy-thumbnail-background"}
draggable={false}
src={wrappedURI}
/>
<img className={"object-eyecandy-thumbnail-image"}
onLoad={loadHandler}
onError={failureHandler}
draggable={false}
hidden={!loaded && !lastLoadFailed}
src={wrappedURI}
/>
</div>
);
}
function TextPlainThumbnail(props: ObjectThumbnailProps)
{
const [text, setText] = useState<string>("");
return <></>;
const textUri = getObjectUri(props.obj.id);
useEffect(() =>
{
const request = new Request(textUri);
fetch(request).then((resp: Response) =>
{
resp.text().then((value: string) =>
{
setText(value);
});
});
}, [textUri]);
return (
<div className="object-thumbnail-plaintext-background">
<div className='object-thumbnail-plaintext-container'>
<pre className="object-thumbnail-text">{text}</pre>
</div>
</div>
);
}
/*
async function renderPDFthumbnailEventually(bgCanvas: HTMLCanvasElement,
fgCanvas: HTMLCanvasElement,
pdfURI: string,
doneCallback: () => void)
{
const pdf = (await PDFjs.getDocument(pdfURI).promise);
const page = await pdf.getPage(1); //No magic numbers here, silly goose
const viewport = page.getViewport({ scale: 1 }); //why this scale tho?
const context = fgCanvas.getContext('2d');
if (!context)
throw "Context was null";
bgCanvas.width = fgCanvas.width = viewport.width;
bgCanvas.height = fgCanvas.height = viewport.height;
const renderContext =
{
canvasContext: context,
viewport: viewport,
};
page.render(renderContext).promise.then(() =>
{
const bgContext = bgCanvas.getContext('2d');
if (!bgContext)
return;
bgContext.drawImage(fgCanvas, 0, 0);
doneCallback();
});
}
function PDFThumbnail(props: ObjectThumbnailProps)
{
const bgCanvasRef = useRef<HTMLCanvasElement>(null);
const imageCanvasRef = useRef<HTMLCanvasElement>(null);
const [thumbnailReady, setThumbnailReady] = useState<boolean>(false);
useEffect(() =>
{
if (bgCanvasRef.current && imageCanvasRef.current)
{
const pdfURI = getObjectUri(props.obj);
renderPDFthumbnailEventually(bgCanvasRef.current,
imageCanvasRef.current,
pdfURI,
() => { setThumbnailReady(true) });
}
} ,[bgCanvasRef, imageCanvasRef])
let style = { display: "none" };
if(thumbnailReady)
style = {display: "unset"}
return (
<div className='object-eyecandy-thumbnail'>
<canvas style={style} ref={bgCanvasRef} className="object-eyecandy-thumbnail-background"/>
<canvas style={style} ref={imageCanvasRef} className="object-eyecandy-thumbnail-image"/>
</div>
);
}
*/
//really need like a "useMime" custom hook...
//cause this is all so duplicated.....
function ObjectThumbnail(props: ObjectThumbnailProps) {
const context = getBinderListStator(props.appContext).getBinder(props.theBinderId).context;
//I *think* it's faster than using a promise because we can skip re-rendering the thumbnails?
const [mime, setMime] = useState<string>(context.db.getMimeIfExists(props.obj.id));
const [isSelected, setIsSelected] = useState<Boolean>(false);
useEffect(() => {
if(mime) {
return;
}
(async () => {
const queriedMime = await context.db.getMimeGenIfNotExists(props.obj.id);
if(!queriedMime) return;
//this is where the "can't perform react state update" "indicates memeory leak"
// error comes from.
setMime(queriedMime);
})();
});
useEffect(() => {
const observable = context.objectSelectionObservers;
const syncSelection = () => {
if (observable.get(props.obj.id)) {
setIsSelected(true);
} else {
setIsSelected(false);
}
}
const observerId = observable.register(props.obj.id, syncSelection);
syncSelection();
return () => {
observable.unregister(props.obj.id, observerId);
};
}, []);
const clickHandler = (e: React.MouseEvent) => {
//e.stopPropagation(); needed for closeable overlay
e.preventDefault();
if(e.ctrlKey && !e.shiftKey) {
context.toggleObjectSelection(props.obj.id);
} else if(!e.ctrlKey && e.shiftKey) {
context.shiftSelect(props.obj.index);
} else if(!e.ctrlKey && !e.shiftKey) {
if(e.detail == 2) {
context.openViewer(props.obj);
} else {
context.handoffSingleObjectSelection(props.obj);
}
}
}
let myJsx = <div></div>;
//Really this should maybe be the default but there should be a attribute setting
// that allows you to choose what background you want for this particular object in thumbnail or
// in preview.
/*
if(mime == "image/svg+xml") //then we render the fullfat one cause it's still lighter than athumbnail probably
{
myJsx = <CheckerboardBgImageThumbnail {...props}/>;
}
/*
else if (props.obj.mime == "application/pdf")
{
myJsx = <PDFThumbnail {...props} />
}
else if (mime == "text/plain")
{
myJsx = <TextPlainThumbnail {...props}/>
}
*/
if(mime) //split up the thing
{
const splits = mime.split("/");
if(splits.length > 1) {
const mainMimeType = splits[0];
const secondMimeType = splits[1];
if (mainMimeType == "image") {
myJsx = <EyeCandyImageThumbnail {...props} />
} else if (mainMimeType == "application") {
let val = secondMimeType;
if(secondMimeType == "octet-stream") {
val = "unknown";
}
myJsx = <GenericThumbnail bodyText={context.db.pickFilename(props.obj.id)} bottomBannerText={val} />
} else {
myJsx = <GenericThumbnail bodyText={context.db.pickFilename(props.obj.id)} bottomBannerText={mime} />
}
}
}
const style: React.CSSProperties = {left: props.x, top: props.y}
const noopMouseClick = (e: React.MouseEvent) => {
e.stopPropagation();
};
return (
<div className={`object-thumbnail ${isSelected ? "object-thumbnail-selected" : ""}`}
style={style}
tabIndex={1}
onMouseDown={clickHandler}
onClick={noopMouseClick}>
<div className='object-thumbnail-underlay'>
<div className={"object-thumbnail-standin"} tabIndex={1} ></div>
{
//I try this not inside a debug build, right now it's just too slow for clean scrolling down the
// page, it's rendering text that is the problem, apparently, it just takes a liiiittle too long
// and it can't keep up with the scrolling like an empty div can
//<GenericThumbnail bodyText='' bottomBannerText='Loading...' />
}
</div>
<div className='object-thumbnail-overlay'>
{myJsx}
</div>
</div>
);
}
export { ObjectThumbnailUpdatePayload, ObjectThumbnail }

View File

@ -0,0 +1,79 @@
import React from 'react';
import { StatorContext } from '../statorobject';
import { GenericOverlayWrapper } from './genericoverlay';
import { getBinderVersion, UpdaterStatorState, useWatchUpdaterStator } from '../updaterstator';
import { handleGotoEnesdaPage } from '../ipcbindings';
import { getActivationStator } from '../activation';
interface UpdaterOverlayProps
{
appContext: StatorContext;
}
async function handleGotoDownloadPageAsync(context: StatorContext)
{
handleGotoEnesdaPage((await getBinderVersion()) + "/" + getActivationStator(context).licenseKey);
}
function UpdaterOverlayImpl(props: UpdaterOverlayProps)
{
//Updator stator listens to the activation stator for changes so don't worry about it
const stator = useWatchUpdaterStator(props.appContext);
const handleGotoDownloadPage = () =>
{
handleGotoDownloadPageAsync(props.appContext);
};
const GoDownload = (props: {linkTitle: string}) =>
{
return <a href="#" onClick={handleGotoDownloadPage}>{props.linkTitle}</a>
};
const message = (() =>
{
switch(stator.getState())
{
case UpdaterStatorState.FailedToCheck:
return <p>There was an error checking for updates. Check your internet connection or try again later.</p>;
case UpdaterStatorState.Checking:
return <p>Checking for updates...</p>;
case UpdaterStatorState.Unknown:
return <p>Do you want to check for updates?</p>;
case UpdaterStatorState.UpToDate:
return <p>Everything is currently up to date!</p>;
case UpdaterStatorState.UpdateAvailable:
return <p>An update is available <GoDownload linkTitle=" click here to learn more."/></p>;
}
})();
return (<div>
<h1>Updates</h1>
{message}
<form onSubmit={(e: any) => {e.preventDefault(); stator.checkForUpdates()}}>
<input type="submit" className={"generic-overlay-form-button"} value="Check for updates"/>
</form>
</div>);
}
function UpdaterOverlay(props: UpdaterOverlayProps)
{
return (<GenericOverlayWrapper
appContext={props.appContext}
startTab={0}
tabs={
[
{
title: "Updates",
nestedComponent: UpdaterOverlayImpl,
nestedProps: props,
}
]
}/>);
}
export { UpdaterOverlay }

View File

@ -0,0 +1,16 @@
.upload-controller-background
{
z-index: 200; /*higher than nag screen for reasons*/
background-color: rgba(83, 39, 108, 0.44);
backdrop-filter: blur(2px);
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,218 @@
import React from 'react';
import {StatorContext, StatorObjectBase} from "../statorobject";
import './uploadcontroller.css';
enum UploadState {
Preparing,
InProgress,
Error,
Finished,
Cancelled,
}
export class UploadControllerStator extends StatorObjectBase
{
private show: boolean = false;
private totalCount: number = 0;
private succeededCount: number =0;
private failedCount: number =0;
private message: string = "";
private state = UploadState.Preparing;
private error: string = "";
private fileName: string = "";
nextFileName(name: string) {
//increment current count, set name
this.fileName = name;
this.state = UploadState.InProgress;
this.publishChanges();
}
setPreparing() {
this.succeededCount = 0;
this.failedCount = 0;
this.fileName = "";
this.totalCount = 0;
this.show = true;
this.state = UploadState.Preparing;
this.message = "";
this.error = "";
this.publishChanges();
}
setCount(ct: number) {
//Maybe all this should be in a struct destroyed on completion?
this.totalCount = ct;
this.publishChanges();
}
getFileName()
{
return this.fileName;
}
getMessage()
{
return this.message;
}
getTotalCount()
{
return this.totalCount;
}
getSuccessCount() {
return this.succeededCount;
}
getFailCount() {
return this.failedCount;
}
succ() {
++this.succeededCount;
this.publishChanges();
}
fail(msg: string)
{
++this.failedCount;
this.publishChanges();
}
errorOut(msg: string) {
this.message = msg;
this.state = UploadState.Error;
this.publishChanges();
}
finish() {
this.state = UploadState.Finished;
this.publishChanges();
}
checkShow() {
return this.show;
}
checkIsCancelled() {
return this.state == UploadState.Cancelled;
}
close()
{
this.show = false;
this.publishChanges();
}
cancel() {
this.state = UploadState.Cancelled;
this.publishChanges();
}
getState() {
return this.state;
}
}
const UploadControllerStatorKey = "UploadControllerStator";
function useWatchUploadControllerStator(appContext: StatorContext): UploadControllerStator
{
return appContext.useWatchStatorLegacy(UploadControllerStatorKey, new UploadControllerStator(appContext)) as UploadControllerStator;
}
function getUploadControllerStator(appContext: StatorContext): UploadControllerStator
{
return appContext.getStatorLegacy(UploadControllerStatorKey, new UploadControllerStator(appContext)) as UploadControllerStator;
}
export interface UploadControllerProps
{
appContext: StatorContext;
}
function UploadController(props: UploadControllerProps)
{
const myStator = useWatchUploadControllerStator(props.appContext);
if(!myStator.checkShow())
return <></>;
function closer(e: any)
{
e.preventDefault();
if(myStator.getState() == UploadState.InProgress)
myStator.cancel();
else if(myStator.getState() == UploadState.Preparing) {
/*noop*/
} else {
myStator.close();
}
}
const closeButtonValue = myStator.getState() != UploadState.InProgress ? "Close" : "Cancel";
const closeButtonJsx = myStator.getState() == UploadState.Preparing ? (<></>) :
(
<form onSubmit={closer}>
<input type="submit" className="generic-overlay-form-button" value={closeButtonValue} />
</form>
)
let title = "";
let content = "";
let progressString = `${myStator.getSuccessCount()}/${myStator.getTotalCount()}`;
switch(myStator.getState()) {
case UploadState.Cancelled:
title = "Cancelled";
content = progressString;
break;
case UploadState.Finished:
title = "Done!";
const failCount = myStator.getFailCount();
if(failCount != 0) {
content = `Added ${progressString} files, failed to add ${failCount} files :< sorry`;
} else {
content = `Added ${progressString} files`;
}
break;
case UploadState.Error:
title = "Oops!"
content = myStator.getMessage();
break;
case UploadState.InProgress:
title = "Adding...";
content = `${progressString}: ${myStator.getFileName()}`;
break;
case UploadState.Preparing:
title = "Scanning...";
break;
}
/*
const contensist = myStator.checkIsFinished() ? (<p>Added {myStator.getCurrentCount()} files.</p>) :
(<p>{myStator.getCurrentCount()}/{myStator.getTotalCount()}: {myStator.getFileName()}</p>);
*/
return (
<div className={"upload-controller-background"} draggable={false}>
<div className={"center-overlay"}>
<div className={"generic-overlay-content-container"}>
<div className={"generic-overlay-content-container"}>
<div className={"generic-overlay-inner"}>
<h1>{title}</h1>
<p>{content}</p>
{closeButtonJsx}
</div>
</div>
</div>
</div>
</div>
);
}
export {UploadController, getUploadControllerStator}

View File

@ -0,0 +1,36 @@
import { useEffect, useRef} from 'react'
//You must set the ref of the thing you want to not be excluded from
// the close click even as such:
// <elem ref={myRef}/>
function useCloseableOverlay(callback: () => void)
{
const myRef = useRef<HTMLDivElement>(null);
useEffect(() =>
{
function handler(e: any)
{
if(myRef.current && !myRef.current.contains(e.target))
{
callback();
e.preventDefault();
}
};
//In order to stop propogation of a click event, we have to track the call to
// the callback, and then if it's fired, we skip the mouseup event, and
// reset that state...
//(cause it's the proceeding "up" event that triggers a click event)
document.addEventListener("mousedown", handler);
return () => {
document.removeEventListener("mousedown", handler);
}
});
return myRef;
}
export { useCloseableOverlay }

View File

@ -0,0 +1,16 @@
.vibrant-action-button
{
background-color: #48e43d;
color: black;
border-width: 0px;
}
.vibrant-action-button:hover
{
background-color: #52D249;
}
.vibrant-action-button:active
{
background-color: #4bb843;
}

Some files were not shown because too many files have changed in this diff Show More