This commit is contained in:
metacryst
2025-11-13 15:30:11 -06:00
commit 493f1c2e1f
16 changed files with 3008 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

68
app.js Normal file
View File

@@ -0,0 +1,68 @@
import { app, BrowserWindow, globalShortcut } from 'electron';
import paths from 'path'
import "./server/index.js"
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = paths.dirname(__filename);
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
frame: false,
titleBarStyle: "hidden",
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
webviewTag: true,
}
});
win.loadFile('server/index.html');
win.webContents.on('did-fail-load', (e, code, desc) => {
console.log('Webview failed:', desc);
});
}
let devToolsOpened = false;
function toggleDevTools(win) {
if (devToolsOpened) {
win.closeDevTools();
} else {
win.openDevTools();
}
devToolsOpened = !devToolsOpened;
}
app.on("ready", async () => {
// await Forms.init();
createWindow();
// Register global shortcuts
app.on("browser-window-focus", () => {
const reloadShortcut = process.platform === "darwin" ? "Command+R" : "Control+R";
globalShortcut.register(reloadShortcut, () => {
BrowserWindow.getFocusedWindow().reload();
});
globalShortcut.register("\\", () => {
toggleDevTools(BrowserWindow.getFocusedWindow());
});
});
app.on("browser-window-blur", () => {
globalShortcut.unregisterAll();
});
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});

3
db/db.json Normal file
View File

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

1615
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "downloads-lister",
"version": "1.0.0",
"type": "module",
"main": "app.js",
"scripts": {
"start": "electron ."
},
"devDependencies": {
"electron": "^30.0.0"
},
"dependencies": {
"chalk": "^5.6.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"moment": "^2.30.1",
"ws": "^8.18.3"
}
}

76
server/db/db.js Normal file
View File

@@ -0,0 +1,76 @@
import chalk from 'chalk'
import path from 'path';
import fs from 'fs/promises';
import { pathToFileURL } from 'url';
export default class Database {
#nodes;
#edges;
#labels = {}
constructor() {
this.getData()
}
async getData() {
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
let dbJson;
try {
dbJson = JSON.parse(dbData);
} catch {
dbJson = []
}
this.#nodes = dbJson["nodes"];
this.#edges = dbJson["edges"];
console.log(chalk.yellow("DB established."))
Object.preventExtensions(this);
}
// superKey = "nodes" || "edges"
async writeData(superKey, key, value) {
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
let dbJson;
try {
dbJson = JSON.parse(dbData);
} catch {
dbJson = []
}
dbJson[superKey][key] = value;
await fs.writeFile(path.join(process.cwd(), 'db/db.json'), JSON.stringify(dbJson, null, 2), 'utf8')
}
async getLabelModels() {
const labelHandlers = {};
const labelDir = path.join(process.cwd(), 'src/model/labels');
const files = await fs.readdir(labelDir);
for (const file of files) {
if (!file.endsWith('.js')) continue;
const label = path.basename(file, '.js');
const modulePath = path.join(labelDir, file);
const module = await import(pathToFileURL(modulePath).href);
labelHandlers[label] = module.default;
if (!this.#labels[label]) {
this.#labels[label] = [];
}
}
return labelHandlers
}
generateUserID() {
let id = this.#labels["User"].length + 1;
while (this.get.user(`user-${id}`)) {
id++;
}
return `user-${id}`; // O(1) most of the time
}
async getAll() {
return { nodes: this.#nodes }
}
}

67
server/db/getDownloads.js Normal file
View File

@@ -0,0 +1,67 @@
// getDownloadsFiles.js
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
const DOWNLOADS = path.join(os.homedir(), 'Downloads');
/**
* Recursively get all files in Downloads with name, path, and inode.
* @returns {Promise<Array<{name: string, path: string, inode: number|null}>>}
*/
async function getAllDownloadsFiles() {
const files = [];
async function scan(dir) {
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch (err) {
console.warn(`Cannot read directory: ${dir}`, err.message);
return;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
try {
// Get stats (inode = ino on Unix, null on Windows)
// const stats = await fs.stat(fullPath);
// const inode = stats.ino ?? null;
if (entry.isDirectory()) {
// Recurse into subdirectories
await scan(fullPath);
} else if (entry.isFile()) {
const parsed = path.parse(entry.name); // splits name/ext
const realExtension = path.extname(fullPath); // from full path (more reliable)
// Use realExtension if available, fallback to parsed.ext
const extension = realExtension || parsed.ext;
// Build clean name + extension
const nameWithExt = parsed.name + extension;
files.push({
name: entry.name, // original (may hide ext)
// nameWithExt: nameWithExt, // ALWAYS has correct ext
// displayName: nameWithExt, // use this in UI
// extension: extension.slice(1).toLowerCase() || null, // "jpg", null if none
// path: fullPath,
// inode: stats.ino ?? null,
// size: stats.size,
// mtime: stats.mtime.toISOString()
});
}
// Skip symlinks, sockets, etc. (or handle if needed)
} catch (err) {
console.warn(`Cannot access: ${fullPath}`, err.message);
}
}
}
await scan(DOWNLOADS);
return files;
}
export default getAllDownloadsFiles;

29
server/index.html Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Downloads</title>
<link rel="icon" href="Quill.png">
<style>
body {
margin: 0;
overflow: hidden;
}
.draggable {
-webkit-app-region: drag;
height: 20px;
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 999;
}
</style>
</head>
<body>
<div class="draggable"></div>
<webview src="http://localhost:3020" style="width:100vw; height:100vh;"></webview>
</body>
</html>

115
server/index.js Normal file
View File

@@ -0,0 +1,115 @@
import path from 'path';
import fs from 'fs/promises'
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import express from 'express';
import cors from 'cors'
import http from 'http'
import chalk from 'chalk'
import moment from 'moment'
import { initWebSocket } from './ws.js'
import Database from "./db/db.js"
import getAllDownloadsFiles from "./db/getDownloads.js"
class Server {
db;
UIPath = path.join(__dirname, '../ui')
registerRoutes(router) {
router.get('/', this.index)
router.get('/*', this.get)
return router
}
index = async (req, res) => {
let filePath = path.join(this.UIPath, "index.html");
let html = await fs.readFile(filePath, 'utf-8');
let downloads = await getAllDownloadsFiles()
const jsonString = JSON.stringify(downloads, null, 2); // pretty-print optional
const scriptTag = `<script type="application/json" id="jsonData">\n${jsonString}\n</script>`;
const headEndRegex = /<\/head\s*>/i;
const match = html.match(headEndRegex);
const insertPos = match.index;
let result = html.slice(0, insertPos) + scriptTag + '\n' + html.slice(insertPos);
res.type('html').send(result);
}
get = async (req, res) => {
let url = req.url
let filePath = path.join(this.UIPath, url);
res.sendFile(filePath, (err) => {
if (err) {
console.error(`Error serving ${filePath}:`, err);
res.status(err.status || 500).send('File not found or error serving file.');
}
});
}
logRequest(req, res, next) {
const formattedDate = moment().format('M.D');
const formattedTime = moment().format('h:mma');
if(req.url.includes("/api/")) {
console.log(chalk.blue(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`));
} else {
if(req.url === "/")
console.log(chalk.gray(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`));
}
next();
}
logResponse(req, res, next) {
const originalSend = res.send;
res.send = function (body) {
if(res.statusCode >= 400) {
console.log(chalk.blue( `<-${chalk.red(res.statusCode)}- ${req.method} ${req.url} | ${chalk.red(body)}`));
} else {
console.log(chalk.blue(`<-${res.statusCode}- ${req.method} ${req.url}`));
}
originalSend.call(this, body);
};
next();
}
constructor() {
this.db = new Database()
const app = express();
app.use(cors({ origin: '*' }));
app.use(express.json());
app.use(this.logRequest);
app.use(this.logResponse);
let router = express.Router();
this.registerRoutes(router)
app.use('/', router);
const server = http.createServer(app);
initWebSocket(server);
const PORT = 3020;
server.listen(PORT, () => {
console.log("\n")
console.log(chalk.yellow("**************Downloads****************"))
console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`));
console.log(chalk.yellow("***************************************"))
console.log("\n")
});
process.on('SIGINT', async () => {
console.log(chalk.red('Closing server...'));
console.log(chalk.green('Database connection closed.'));
process.exit(0);
});
Object.preventExtensions(this);
}
}
const server = new Server()

31
server/ws.js Normal file
View File

@@ -0,0 +1,31 @@
import { WebSocket, WebSocketServer } from 'ws';
let wss;
export function initWebSocket(server) {
wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
console.log('✅ New WebSocket client connected');
ws.on('close', () => {
console.log('Client disconnected');
});
});
console.log('WebSocket server initialized');
}
// Broadcast a message to all connected clients
export function broadcast(reqType, data) {
if (!wss) return;
const payload = typeof data === 'string' ? data : JSON.stringify(data);
const message = `${reqType}|${payload}`;
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}

872
ui/_/code/quill.js Normal file
View File

@@ -0,0 +1,872 @@
/*
Sam Russell
Captured Sun
11.9.25 - changed p(innerText) to p(innerHTML), adjusted onNavigate to work for multiple elements and with correct "this" scope
11.7.25 - changed registerShadow() to register(), changed onClick() to be like onHover()
11.6.25 - adding default value for "button()" "children" parameter
10.29.25 - adding "gap()" and "label()" functions
*/
/* $ NAVIGATION */
let oldPushState = history.pushState;
history.pushState = function pushState() {
let ret = oldPushState.apply(this, arguments);
window.dispatchEvent(new Event('pushstate'));
window.dispatchEvent(new Event('navigate'));
return ret;
};
window.addEventListener('popstate', () => {
window.dispatchEvent(new Event('navigate'));
});
window.navigateTo = function(url) {
window.dispatchEvent(new Event('navigate'));
window.history.pushState({}, '', url);
}
/* $ SELECTOR */
HTMLElement.prototype.$ = function(selector) {
return window.$(selector, this)
}
DocumentFragment.prototype.$ = function(selector) {
return window.$(selector, this)
}
window.$ = function(selector, el = document) {
return el.querySelector(selector)
}
window.$$ = function(selector, el = document) {
return Array.from(el.querySelectorAll(selector))
}
/* CONSOLE */
console.red = function(message) {
this.log(`%c${message}`, "color: rgb(254, 79, 42);");
};
console.green = function(message) {
this.log(`%c${message}`, "color: rgb(79, 254, 42);");
}
/* GET CSS VARIABLES FOR DARK OR LIGHT MODE */
window.darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.add(darkMode ? 'dark' : 'light');
window.getColor = function(name) {
const rootStyles = getComputedStyle(document.documentElement);
const color = rootStyles.getPropertyValue(`--${name}`).trim();
if(!color) {
throw new Error("Color not found")
}
return color
}
/* MOBILE */
window.isMobile = function isMobile() {
return /Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.test(navigator.userAgent);
}
window.css = function css(cssString) {
let container = document.querySelector("style#pageStyle");
if(!container) {
container = document.createElement('style');
container.id = "pageStyle";
document.head.appendChild(container);
}
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
primarySelector = primarySelector.replace(/\*/g, "all");
primarySelector = primarySelector.replace(/#/g, "id-");
primarySelector = primarySelector.replace(/,/g, "");
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
if(!stylesheet) {
stylesheet = document.createElement('style');
stylesheet.id = primarySelector;
stylesheet.appendChild(document.createTextNode(cssString));
container.appendChild(stylesheet);
} else {
stylesheet.innerText = cssString
}
}
window.html = function html(elementString) {
let parser = new DOMParser();
let doc = parser.parseFromString(elementString, 'text/html');
return doc.body.firstChild;
}
window.util = {}
window.util.observeClassChange = (el, callback) => {
if (!el || !(el instanceof Element)) {
throw new Error("observeClassChange requires a valid DOM element.");
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
callback(el.classList);
}
}
});
observer.observe(el, {
attributes: true,
attributeFilter: ["class"]
});
return observer; // Optional: return it so you can disconnect later
}
/* PAGE SETUP */
Object.defineProperty(Array.prototype, 'last', {
get() {
return this[this.length - 1];
},
enumerable: false,
});
/* QUILL */
window.quill = {
rendering: [],
render: (el) => {
if(el instanceof Shadow) {
let parent = quill.rendering[quill.rendering.length-1]
if(!parent) {
parent = document.body
}
parent.appendChild(el)
} else {
if(!el.render) {el.render = () => {}}
let parent = quill.rendering[quill.rendering.length-1]
if(!parent) throw new Error("Quill: no parent for element")
parent.appendChild(el)
}
quill.rendering.push(el)
el.render()
quill.rendering.pop(el)
},
rerender: (el) => {
Array.from(el.attributes).forEach(attr => el.removeAttribute(attr.name));
el.innerHTML = ""
el.removeAllListeners()
quill.rendering.push(el)
el.render()
quill.rendering.pop()
},
loadPage: () => {
let URL = window.location.pathname
if(!window.routes[URL]) {
throw new Error("No URL for this route: ", URL)
}
let pageClass = window[routes[URL]]
document.title = pageClass.title ?? document.title
document.body.innerHTML = ""
let page = new pageClass()
quill.render(page)
},
isStack: (el) => {
return el.classList.contains("HStack") || el.classList.contains("ZStack") || el.classList.contains("VStack")
},
}
window.Shadow = class extends HTMLElement {
constructor() {
super()
}
}
window.register = (el, tagname) => {
if (typeof el.prototype.render !== 'function') {
throw new Error("Element must have a render: " + el.prototype.constructor.name)
}
if(!tagname) {
tagname = el.prototype.constructor.name.toLowerCase() + "-"
}
customElements.define(tagname, el)
if(el.css) {
css(el.css)
}
window[el.prototype.constructor.name] = function (...params) {
let instance = new el(...params)
quill.render(instance)
return instance
}
}
HTMLElement.prototype.rerender = function() {
quill.rerender(this)
}
/* Styling */
window.vh = "vh"
window.vw = "vw"
window.px = "px"
window.em = "em"
window.rem = "rem"
window.inches = "in"
HTMLElement.prototype.addStyle = function(func) {
return func(this)
}
window.css = function css(cssString) {
let container = document.querySelector("style#pageStyle");
if(!container) {
container = document.createElement('style');
container.id = "pageStyle";
document.head.appendChild(container);
}
let primarySelector = cssString.substring(0, cssString.indexOf("{")).trim();
primarySelector = primarySelector.replace(/\*/g, "all");
primarySelector = primarySelector.replace(/#/g, "id-");
primarySelector = primarySelector.replace(/,/g, "");
let stylesheet = container.querySelector(`:scope > style[id='${primarySelector}']`)
if(!stylesheet) {
stylesheet = document.createElement('style');
stylesheet.id = primarySelector;
stylesheet.appendChild(document.createTextNode(cssString));
container.appendChild(stylesheet);
} else {
stylesheet.innerText = cssString
}
}
function extendHTMLElementWithStyleSetters() {
let allStyleProps = Object.keys(document.createElement("div").style)
allStyleProps.forEach(prop => {
if(prop === "translate") return
HTMLElement.prototype[prop] = function(value) {
this.style[prop] = value;
return this;
};
});
}
extendHTMLElementWithStyleSetters();
HTMLElement.prototype.padding = function(one, two, three = "px") {
const setPadding = (side, val) => {
const directionName = `padding${side.charAt(0).toUpperCase()}${side.slice(1)}`;
this.style[directionName] = (typeof val === 'number') ? `${val}${three}` : val;
};
if(one === "horizontal" || one === "vertical") { // is one a direction
if (one === "horizontal") {
setPadding("left", two);
setPadding("right", two);
} else if (one === "vertical") {
setPadding("top", two);
setPadding("bottom", two);
}
} else { // is two a value
if(typeof one !== 'number' || isNaN(one)) {
this.style.padding = one
} else {
this.style.padding = one + two
}
}
return this;
};
HTMLElement.prototype.paddingTop = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingTop = value + unit
return this
}
HTMLElement.prototype.paddingLeft = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingLeft = value + unit
return this
}
HTMLElement.prototype.paddingBottom = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingBottom = value + unit
return this
}
HTMLElement.prototype.paddingRight = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingRight = value + unit
return this
}
HTMLElement.prototype.paddingVertical = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingTop = value + unit
this.style.paddingBottom = value + unit
return this
}
HTMLElement.prototype.paddingHorizontal = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.paddingRight = value + unit
this.style.paddingLeft = value + unit
return this
}
HTMLElement.prototype.margin = function(direction, value, unit = "px") {
if (!value) {
this.style.margin = direction;
return this;
}
const setMargin = (side, val) => {
const directionName = `margin${side.charAt(0).toUpperCase()}${side.slice(1)}`;
this.style[directionName] = (typeof val === 'number') ? `${val}${unit}` : val;
};
if (direction === "horizontal") {
setMargin("left", value);
setMargin("right", value);
} else if (direction === "vertical") {
setMargin("top", value);
setMargin("bottom", value);
} else {
setMargin(direction, value);
}
return this;
};
HTMLElement.prototype.marginTop = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginTop = value + unit
return this
}
HTMLElement.prototype.marginLeft = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginLeft = value + unit
return this
}
HTMLElement.prototype.marginBottom = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginBottom = value + unit
return this
}
HTMLElement.prototype.marginRight = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.marginRight = value + unit
return this
}
HTMLElement.prototype.width = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.width = value + unit
return this
}
HTMLElement.prototype.minWidth = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.minWidth = value + unit
return this
}
HTMLElement.prototype.height = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.height = value + unit
return this
}
HTMLElement.prototype.minHeight = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.minHeight = value + unit
return this
}
HTMLElement.prototype.fontSize = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
switch(value) {
case "6xl":
value = "3.75"; unit = "rem"
break;
case "5xl":
value = "3"; unit = "rem"
break;
case "4xl":
value = "2.25"; unit = "rem"
break;
case "3xl":
value = "1.875"; unit = "rem"
break;
case "2xl":
value = "1.5"; unit = "rem"
break;
case "xl":
value = "1.25"; unit = "rem"
break;
case "l":
value = "1.125"; unit = "rem"
break;
case "s":
value = "0.875"; unit = "rem"
break;
case "xs":
value = "0.75"; unit = "rem"
break;
default:
break;
}
this.style.fontSize = value + unit
return this
}
function checkPositionType(el) {
let computed = window.getComputedStyle(el).position
if(!(computed === "absolute" || computed === "fixed")) {
el.style.position = "absolute"
}
}
HTMLElement.prototype.x = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.left = value + unit
return this
}
HTMLElement.prototype.y = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.top = value + unit
return this
}
HTMLElement.prototype.xRight = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.right = value + unit
return this
}
HTMLElement.prototype.yBottom = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
checkPositionType(this)
this.style.bottom = value + unit
return this
}
HTMLElement.prototype.borderRadius = function(value, unit = "px") {
if (typeof value !== 'number' || isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.borderRadius = value + unit
return this
}
HTMLElement.prototype.positionType = function (value) {
if(!(value === "absolute" || value === "relative" || value === "static" || value === "fixed" || value === "sticky")) {
console.error("HTMLElement.overlflow: must have valid overflow value!")
return;
}
this.style.position = value
return this
}
HTMLElement.prototype.gap = function(value, unit = "px") {
if (typeof value !== 'number' || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
this.style.gap = value + unit
return this
}
HTMLElement.prototype.backgroundImage = function (...values) {
const formatted = values
.map(v => {
if(v.includes("/") && !v.includes("gradient")) {
v = "url(" + v + ")"
}
return String(v).trim();
})
.join(", ");
this.style.backgroundImage = formatted;
return this;
};
/* Elements */
quill.setChildren = function(el, innerContent) {
if(typeof innerContent === "string") {
el.innerText = innerContent
} else if(typeof innerContent === "function") {
el.render = innerContent
} else {
throw new Error("Children of unknown type")
}
}
window.a = function a( href, inner=href ) {
if(!href) throw new Error("quill a: missing href argument. Function: a( href, inner=href )")
let link = document.createElement("a")
link.setAttribute('href', href);
quill.setChildren(link, inner)
quill.render(link)
return link
}
window.img = function img(src, width="", height="") {
let image = document.createElement("img")
if(!src || !(typeof src==="string")) {
throw new Error("img: missing first argument: src | String")
} else {
image.src = src
}
if(width && typeof width === "string") {
image.style.width = width
} else if(width) {
image.style.width = width + "px"
}
if(height && typeof height === "string") {
image.style.height = height
} else if(height) {
image.style.height = height + "px"
}
quill.render(image)
return image
}
HTMLImageElement.prototype.backgroundColor = function(value) {
if (this.src.endsWith('.svg') || this.src.startsWith('data:image/svg+xml')) {
fetch(this.src).then(response => response.text())
.then(svgText => {
const modifiedSvg = svgText.replace(/\bfill="[^"]*"/g, `fill="${value}"`);
const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' });
this.src = URL.createObjectURL(blob);
}).catch(error => {
console.error('Error updating SVG fill:', error);
});
} else {
this.style.backgroundColor = value;
}
return this; // Always returns the element itself
};
window.p = function p(innerHTML) {
let el = document.createElement("p")
if(typeof innerText === "function") {
el.render = innerHTML
} else {
el.innerHTML = innerHTML
}
el.style.margin = "0";
quill.render(el)
return el
}
window.h1 = function h1(innerText) {
let el = document.createElement("h1")
el.innerText = innerText
quill.render(el)
return el
}
window.h2 = function h2(innerText) {
let el = document.createElement("h2")
el.innerText = innerText
quill.render(el)
return el
}
window.h3 = function h3(innerText) {
let el = document.createElement("h3")
el.innerText = innerText
quill.render(el)
return el
}
window.div = function (innerText) {
let el = document.createElement("div")
el.innerText = innerText ?? ""
quill.render(el)
return el
}
window.span = function (innerText) {
let el = document.createElement("span")
el.innerText = innerText
quill.render(el)
return el
}
window.button = function (children = "") {
let el = document.createElement("button")
quill.setChildren(el, children)
quill.render(el)
return el
}
window.form = function(cb) {
let el = document.createElement("form")
el.render = cb
quill.render(el)
return el
}
window.input = function(placeholder, width, height) {
let el = document.createElement("input")
el.placeholder = placeholder
el.style.width = width
el.style.height = height
quill.render(el)
return el
}
window.label = function(text) {
let el = document.createElement("label")
el.innerText = text
quill.render(el)
return el
}
window.textarea = function(placeholder) {
let el = document.createElement("textarea")
el.placeholder = placeholder
quill.render(el)
return el
}
/* STACKS */
window.VStack = function (cb = () => {}) {
let styles = `
display: flex;
flex-direction: column;
`
let nowRendering = quill.rendering[quill.rendering.length-1]
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.style.cssText += styles
nowRendering.classList.add("VStack")
cb()
return nowRendering
}
let div = document.createElement("div")
div.classList.add("VStack")
div.style.cssText += styles
div.render = cb
quill.render(div)
return div
}
window.HStack = function (cb = () => {}) {
let styles = `
display: flex;
flex-direction: row;
`;
let nowRendering = quill.rendering[quill.rendering.length - 1];
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.style.cssText += styles;
nowRendering.classList.add("HStack")
cb();
return nowRendering;
}
let div = document.createElement("div");
div.classList.add("HStack");
div.style.cssText += styles;
div.render = cb;
quill.render(div);
return div;
};
window.ZStack = function (cb = () => {}) {
let nowRendering = quill.rendering[quill.rendering.length - 1];
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
nowRendering.classList.add("ZStack")
cb();
return nowRendering;
}
let div = document.createElement("div");
div.classList.add("ZStack");
div.render = cb;
quill.render(div);
return div;
};
/* EVENTS */
HTMLElement.prototype.onAppear = function(func) {
func(this);
return this;
};
HTMLElement.prototype.onClick = function(func) {
const onMouseDown = () => func.call(this, true);
const onMouseUp = () => func.call(this, false);
this._storeListener("mousedown", onMouseDown);
this._storeListener("mouseup", onMouseUp);
return this;
};
HTMLElement.prototype.onMouseDown = function(func) {
this._storeListener("mousedown", func);
return this;
};
HTMLElement.prototype.onMouseUp = function(func) {
this._storeListener("mouseup", func);
return this;
};
HTMLElement.prototype.onRightClick = function(func) {
this._storeListener("contextmenu", func);
return this;
};
HTMLElement.prototype.onHover = function(cb) {
const onEnter = () => cb.call(this, true);
const onLeave = () => cb.call(this, false);
this._storeListener("mouseover", onEnter);
this._storeListener("mouseleave", onLeave);
return this;
};
HTMLElement.prototype.onFocus = function(cb) {
if (!this.matches('input, textarea, select, button')) {
throw new Error("Can't put focus event on non-form element!");
}
const onFocus = () => cb.call(this, true);
const onBlur = () => cb.call(this, false);
this._storeListener("focus", onFocus);
this._storeListener("blur", onBlur);
return this;
};
HTMLElement.prototype.onKeyDown = function(cb) {
this._storeListener("keydown", cb);
return this;
};
HTMLElement.prototype.onInput = function(cb) {
if(!this.matches('input, textarea, [contenteditable=""], [contenteditable="true"]'))
throw new Error("Can't put input event on non-input element!")
this._storeListener("input", cb);
return this;
};
/* WHY THIS LISTENER IS THE WAY IT IS:
- If we dispatch the "navigate" event on the window (as one would expect for a "navigate" event), a listener placed on the element will not pick it up.
- However, if we add the event as a window event, it won't have the "this" scope that a callback normally would.
- Then, if we try to add that scope using bind(), it makes the function.toString() unreadable, which means we will get false positives for duplicate listeners.
- Therefore, we just have to attach the navigate event to the element, and manually trigger that when the window listener fires.
*/
HTMLElement.prototype.onNavigate = function(cb) {
this._storeListener("navigate", cb);
window.addEventListener("navigate", () => this.dispatchEvent(new CustomEvent("navigate")))
return this;
};
HTMLElement.prototype.onEvent = function(name, cb) {
window._storeListener(window, name, cb);
return this;
};
HTMLElement.prototype._storeListener = function(type, handler, options) {
window._storeListener(this, type, handler, options)
}
window.__listeners = []
function _storeListener(target, type, handler, options) {
if (!target.__listeners) target.__listeners = [];
const optionsString = JSON.stringify(options);
const index = target.__listeners.findIndex(listener =>
listener.type === type &&
listener.handler.toString() === handler.toString() &&
JSON.stringify(listener.options) === optionsString
);
if (index === -1) { // Listener is new
target.addEventListener(type, handler, options);
target.__listeners.push({ type, handler, options });
} else { // Listener is a duplicate, can be replaced
const old = target.__listeners[index];
target.removeEventListener(old.type, old.handler, old.options);
// Replace with the new one
target.addEventListener(type, handler, options);
target.__listeners[index] = { type, handler, options };
}
}
HTMLElement.prototype.removeAllListeners = function() {
if (!this.__listeners) return;
for (const { type, handler, options } of this.__listeners) {
this.removeEventListener(type, handler, options);
}
this.__listeners = [];
return this;
};
/* ATTRIBUTES */
HTMLElement.prototype.attr = function(attributes) {
if (
typeof attributes !== "object" ||
attributes === null ||
Array.isArray(attributes)
) {
throw new TypeError("attr() expects an object with key-value pairs");
}
for (const [key, value] of Object.entries(attributes)) {
this.setAttribute(key, value);
}
return this;
};

24
ui/_/code/shared.css Normal file
View File

@@ -0,0 +1,24 @@
:root {
--main: #BD410D;
--accent: var(--tan);
--accent2: var(--brown);
--tan: #FEB279;
--purple: #251D44;
--green: #0B5538;
--red: #BC1C02;
--brown: #c6a476;
--darkbrown: #60320c;
--darkred: #7f0b09;
--orange: #FE9201;
--periwinkle: #655BAF;
}
@media (prefers-color-scheme: dark) {
:root {
--main: #BD410D;
--accent: var(--tan);
--accent2: var(--green);
}
}

BIN
ui/_/icons/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

73
ui/components/Home.js Normal file
View File

@@ -0,0 +1,73 @@
class Home extends Shadow {
render() {
ZStack(() => {
input("", "20vw")
.backgroundColor("var(--darkred)")
.color("var(--accent)")
.border("none")
.borderTop("1px solid var(--accent)")
.borderBottom("1px solid var(--accent)")
.outline("0.5px solid black")
.borderRadius(7, px)
.transition("padding .2s")
.padding(0.5, em)
.position("absolute")
.x(50, vw).y(7, em)
.transform("translate(-50%, -50%)")
.onInput(() => {
console.log('typing')
})
.onFocus(function (focusing) {
if(focusing) {
this.style.padding = "1em"
this.style.borderColor = "var(--darkred)"
} else {
this.style.padding = "0.5em"
this.style.borderColor = "var(--accent)"
}
})
VStack(() => {
let json = this.getJSONData()
for(let i=0; i<100; i++) {
p(json[i].name)
.marginLeft(0, em)
}
})
.paddingLeft(5, em)
.paddingTop(10, em)
})
.backgroundColor("var(--main)")
.display("block")
.width(100, vw).height("auto")
.color("var(--accent)")
.fontFamily("Arial")
.onAppear(() => {
document.body.style.backgroundColor = "var(--main)"
})
}
getJSONData() {
const script = document.getElementById('jsonData');
if (!script) {
console.warn('initial-data script not found');
return null;
}
try {
// script.textContent is the raw JSON string
const data = JSON.parse(script.textContent);
console.log(data)
return data;
} catch (err) {
console.error('Failed to parse initial-data JSON:', err);
return null;
}
}
}
register(Home)

13
ui/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Downloads</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/_/icons/logo.png">
<link rel="stylesheet" href="/_/code/shared.css"></script>
<script src="/_/code/quill.js"></script>
<script type="module" src="/index.js"></script>
</head>
<body style="margin: 0px;">
</body>
</html>

2
ui/index.js Normal file
View File

@@ -0,0 +1,2 @@
import "./components/Home.js"
Home()