Workspaces now fully functional (using indexedDB)

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2023-01-27 01:39:08 -03:00
parent 827349bf05
commit c4ef6ccce4
15 changed files with 397 additions and 49 deletions

View file

@ -1,5 +1,6 @@
:root {
--c-primary: #2c3333;
--c-disabled: rgb(81, 81, 81);
--c-hover: hsl(180, 7%, 30%);
--c-active: hsl(180, 7%, 25%);
--c-primary-accent: hsl(180, 7%, 40%);

View file

@ -105,6 +105,39 @@
mask-image: url("../res/icons/paintbrush.svg");
}
.ui.inline-icon.icon-save::after,
.ui.icon > .icon-save {
-webkit-mask-image: url("../res/icons/save.svg");
mask-image: url("../res/icons/save.svg");
}
.ui.inline-icon.icon-pencil::after,
.ui.icon > .icon-pencil {
-webkit-mask-image: url("../res/icons/pencil.svg");
mask-image: url("../res/icons/pencil.svg");
}
.ui.inline-icon.icon-download::after,
.ui.icon > .icon-download {
-webkit-mask-image: url("../res/icons/download.svg");
mask-image: url("../res/icons/download.svg");
}
.ui.inline-icon.icon-upload::after,
.ui.icon > .icon-upload {
-webkit-mask-image: url("../res/icons/upload.svg");
mask-image: url("../res/icons/upload.svg");
}
.ui.inline-icon.icon-more-horizontal::after,
.ui.icon > .icon-more-horizontal {
-webkit-mask-image: url("../res/icons/more-horizontal.svg");
mask-image: url("../res/icons/more-horizontal.svg");
}
.ui.inline-icon.icon-trash::after,
.ui.icon > .icon-trash {
-webkit-mask-image: url("../res/icons/trash.svg");
mask-image: url("../res/icons/trash.svg");
}
.ui.inline-icon.icon-expand::after,
.ui.icon > .icon-expand {
-webkit-mask-image: url("../res/icons/expand.svg");

59
css/ui/workspace.css Normal file
View file

@ -0,0 +1,59 @@
#workspace-select input.autocomplete-text {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
padding-left: 5px;
border: none;
text-overflow: ellipsis;
}
#workspace-select-area .buttons > *:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.workspace-btn {
cursor: pointer;
border: 0;
width: 21px;
height: 21px;
background-color: var(--c-primary);
}
.workspace-btn:disabled {
cursor: default;
background-color: var(--c-disabled) !important;
}
.workspace-btn:hover {
background-color: var(--c-hover);
}
.workspace-btn:active {
background-color: var(--c-active);
}
.workspace-collapsible {
position: relative;
width: 0;
overflow: visible;
}
.workspace-collapsible > *:first-child {
display: flex;
width: fit-content;
height: 21px;
transition-duration: 50ms;
}
.workspace-collapsible.collapsed > *:first-child {
width: 0 !important;
overflow: hidden !important;
}

View file

@ -12,6 +12,7 @@
<link href="css/ui/generic.css?v=30837f8" rel="stylesheet" />
<link href="css/ui/workspace.css?v=30837f8" rel="stylesheet" />
<link href="css/ui/history.css?v=0b03861" rel="stylesheet" />
<link href="css/ui/layers.css?v=1d66c2b" rel="stylesheet" />
<link href="css/ui/toolbar.css?v=109c78f" rel="stylesheet" />
@ -38,6 +39,46 @@
</button>
</div>
<div id="info" class="menu-container" style="min-width: 200px">
<div
id="workspace-select-area"
style="display: flex; margin-bottom: 5px">
<div id="workspace-select"></div>
<div class="buttons" style="display: flex">
<button
id="save-workspace-btn"
class="ui inline-icon icon-save workspace-btn"
title="Save Workspace"></button>
<button
id="rename-workspace-btn"
class="ui inline-icon icon-pencil workspace-btn"
title="Rename Workspace"></button>
<button
id="more-workspace-btn"
class="ui inline-icon icon-more-horizontal workspace-btn"
title="More Options"></button>
<div
id="more-workspace-menu"
class="workspace-collapsible collapsed">
<div>
<button
id="export-workspace-btn"
class="ui inline-icon icon-download workspace-btn"
title="Export Workspace"></button>
<button
id="import-workspace-btn"
class="ui inline-icon icon-upload workspace-btn"
title="Import Workspace"></button>
<button
id="delete-workspace-btn"
class="ui inline-icon icon-trash workspace-btn"
title="Delete Workspace"></button>
</div>
<div style="width: 5px; background-color: var(--c-primary)"></div>
</div>
<div style="width: 5px; background-color: var(--c-primary)"></div>
</div>
</div>
<div class="host-field-wrapper">
<div class="host-field">
<div class="label">Host</div>
@ -164,8 +205,6 @@
<!-- Save/load image section -->
<button type="button" class="collapsible">Save/Upscaling</button>
<div class="content">
<button onclick="saveWorkspaceToFile()">Save Workspace</button>
<button onclick="loadWorkspaceFromFile()">Load Workspace</button>
<button onclick="downloadCanvas()">Save canvas</button>
<br />
<label>Choose upscaler</label>
@ -365,6 +404,7 @@
<script
src="js/lib/workspaces.js?v=4fbd55b"
type="text/javascript"></script>
<script src="js/lib/db.js?v=ac30d16" type="text/javascript"></script>
<script src="js/lib/input.js?v=aa14afc" type="text/javascript"></script>
<script src="js/lib/layers.js?v=1a452a1" type="text/javascript"></script>
<script src="js/lib/commands.js?v=262f0bf" type="text/javascript"></script>
@ -412,6 +452,9 @@
type="text/javascript"></script>
<!-- Initialize -->
<script
src="js/initalize/workspace.populate.js?v=fd01c47"
type="text/javascript"></script>
<script
src="js/initalize/shortcuts.populate.js?v=fd01c47"
type="text/javascript"></script>

View file

@ -886,7 +886,7 @@ async function exportWorkspaceState() {
async function importWorkspaceState(state) {
// Start from zero, effectively
await commands.undo(commands._history.length);
await commands.clear();
// Setup initial layer
const layer = uil.layerIndex.default;
@ -965,19 +965,6 @@ async function saveWorkspaceToFile() {
link.click();
}
async function loadWorkspaceFromFile() {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.addEventListener("change", async (evn) => {
let files = Array.from(input.files);
const json = await files[0].text();
importWorkspaceState(JSON.parse(json));
});
input.click();
}
async function getUpscalers() {
/*
so for some reason when upscalers request returns upscalers, the real-esrgan model names are incorrect, and need to be fetched from /sdapi/v1/realesrgan-models

View file

@ -0,0 +1,179 @@
(() => {
const saveWorkspaceBtn = document.getElementById("save-workspace-btn");
const renameWorkspaceBtn = document.getElementById("rename-workspace-btn");
const moreWorkspaceBtn = document.getElementById("more-workspace-btn");
const expandedWorkspaceMenu = document.getElementById("more-workspace-menu");
const exportWorkspaceBtn = document.getElementById("export-workspace-btn");
const importWorkspaceBtn = document.getElementById("import-workspace-btn");
const deleteWorkspaceBtn = document.getElementById("delete-workspace-btn");
moreWorkspaceBtn.addEventListener("click", () => {
expandedWorkspaceMenu.classList.toggle("collapsed");
});
const workspaceAutocomplete = createAutoComplete(
"Workspace",
document.getElementById("workspace-select")
);
workspaceAutocomplete.options = [{name: "Default", value: "default"}];
workspaceAutocomplete.value = "default";
renameWorkspaceBtn.disabled = true;
deleteWorkspaceBtn.disabled = true;
workspaceAutocomplete.onchange.on(async ({name, value}) => {
if (value === "default") {
renameWorkspaceBtn.disabled = true;
deleteWorkspaceBtn.disabled = true;
await commands.clear();
return;
}
renameWorkspaceBtn.disabled = false;
deleteWorkspaceBtn.disabled = false;
const workspaces = db
.transaction("workspaces", "readonly")
.objectStore("workspaces");
workspaces.get(value).onsuccess = (e) => {
console.debug("[workspace.populate] Loading workspace");
const res = e.target.result;
const {workspace} = res;
importWorkspaceState(workspace);
};
});
/**
* Updates Workspace selection list
*/
const listWorkspaces = async (value = undefined) => {
const options = [{name: "Default", value: "default"}];
const workspaces = db
.transaction("workspaces", "readonly")
.objectStore("workspaces");
workspaces.openCursor().onsuccess = (e) => {
/** @type {IDBCursor} */
const c = e.target.result;
if (c) {
options.push({name: c.value.name, value: c.key});
c.continue();
} else {
const previousValue = workspaceAutocomplete.value;
workspaceAutocomplete.options = options;
workspaceAutocomplete.value = value ?? previousValue;
}
};
};
const saveWorkspaceToDB = async (value) => {
const workspace = await exportWorkspaceState();
const workspaces = db
.transaction("workspaces", "readwrite")
.objectStore("workspaces");
let id = value;
if (value === "default" && commands._history.length > 0) {
// If Workspace is the Default
const name = (prompt("Please enter the workspace name") ?? "").trim();
if (name) {
id = guid();
workspaces.add({id, name, workspace}).onsuccess = () => {
listWorkspaces(id);
alert(`Workspace saved as '${name}'`);
};
}
} else {
workspaces.get(id).onsuccess = (e) => {
const ws = e.target.result;
if (ws) {
workspaces.put({id, workspace}).onsuccess = () => {
alert(`Workspace saved as '${ws.value.name}'`);
listWorkspaces();
};
}
};
}
};
// Normal Workspace Export/Import
exportWorkspaceBtn.addEventListener("click", () => saveWorkspaceToFile());
importWorkspaceBtn.addEventListener("click", () => {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.addEventListener("change", async (evn) => {
let files = Array.from(input.files);
const json = await files[0].text();
await importWorkspaceState(JSON.parse(json));
saveWorkspaceToDB("default");
});
input.click();
});
const onDatabaseLoad = async () => {
// Get workspaces from database
listWorkspaces();
// Save Workspace Button
saveWorkspaceBtn.addEventListener(
"click",
saveWorkspaceToDB(workspaceAutocomplete.value)
);
// Rename Workspace
renameWorkspaceBtn.addEventListener("click", () => {
const workspaces = db
.transaction("workspaces", "readwrite")
.objectStore("workspaces");
let id = workspaceAutocomplete.value;
workspaces.get(id).onsuccess = (e) => {
const workspace = e.target.result;
const name = prompt(
`Please enter the new workspace name.\nOriginal is '${workspace.name}'`
).trim();
if (!name) return;
workspace.name = name;
workspaces.put(workspace).onsuccess = () => {
listWorkspaces();
};
};
});
// Delete Workspace
deleteWorkspaceBtn.addEventListener("click", () => {
const workspaces = db
.transaction("workspaces", "readwrite")
.objectStore("workspaces");
let id = workspaceAutocomplete.value;
workspaces.get(id).onsuccess = (e) => {
const workspace = e.target.result;
if (
confirm(
`Do you really want to delete the workspace '${workspace.name}'?`
)
) {
workspaces.delete(id).onsuccess = (e) => {
listWorkspaces("default");
};
}
};
});
};
if (db) onDatabaseLoad();
else ondatabaseload.on(onDatabaseLoad);
})();

View file

@ -57,6 +57,21 @@ const commands = makeReadOnly(
}
},
/**
* Clears the history
*/
async clear() {
await this.undo(this._history.length);
this._history.splice(0, this._history.length);
_commands_events.emit({
action: "clear",
state: {},
current: commands._current,
});
},
/**
* Imports an exported command and runs it
*

36
js/lib/db.js Normal file
View file

@ -0,0 +1,36 @@
const idb = window.indexedDB.open("openoutpaint", 2);
idb.onerror = (e) => {
console.warn("[stamp] Failed to connect to IndexedDB");
console.warn(e);
};
idb.onupgradeneeded = (e) => {
const db = e.target.result;
console.debug(`[stamp] Setting up database version ${db.version}`);
if (e.oldVersion < 1) {
// Resources Store
const resourcesStore = db.createObjectStore("resources", {
keyPath: "id",
});
resourcesStore.createIndex("name", "name", {unique: false});
}
// Workspaces Store
const workspacesStore = db.createObjectStore("workspaces", {
keyPath: "id",
});
workspacesStore.createIndex("name", "name", {unique: false});
};
/** @type {IDBDatabase} */
let db = null;
/** @type {Observer<{db: IDBDatabase}>} */
const ondatabaseload = new Observer();
idb.onsuccess = (e) => {
db = e.target.result;
ondatabaseload.emit({db});
};

View file

@ -209,7 +209,6 @@ function createSlider(name, wrapper, options = {}) {
* @param {{name: string, value: string, optionelcb: (el: HTMLOptionElement) => void}[]} options.options Options to add to the selector
* @param {object} extraEl Additional element to include in wrapper div (e.g. model refresh button)
* @param {string} extraClass Additional class to attach to the autocomplete input element
* @returns {AutoCompleteElement}
*/
function createAutoComplete(
name,

View file

@ -41,7 +41,7 @@
};
_commands_events.on((message) => {
if (message.action === "run") {
if (message.action === "run" || message.action === "clear") {
Array.from(historyView.children).forEach((child) => {
if (
!commands._history.find((entry) => `hist-${entry.id}` === child.id)

View file

@ -146,13 +146,9 @@ const stampTool = () =>
};
// Open IndexedDB connection
const IDBOpenRequest = window.indexedDB.open("stamp", 1);
// Synchronizes resources array with the DOM and Local Storage
const syncResources = () => {
// Saves to IndexedDB
/** @type {IDBDatabase} */
const db = state.stampDB;
const resources = db
.transaction("resources", "readwrite")
.objectStore("resources");
@ -590,35 +586,9 @@ const stampTool = () =>
state.ctxmenu.resourceList = resourceList;
// Performs resource fetch from IndexedDB
IDBOpenRequest.onerror = (e) => {
console.warn("[stamp] Failed to connect to IndexedDB");
console.warn(e);
};
IDBOpenRequest.onupgradeneeded = (e) => {
const db = e.target.result;
console.debug(`[stamp] Setting up database version ${db.version}`);
const resourcesStore = db.createObjectStore("resources", {
keyPath: "id",
});
resourcesStore.createIndex("name", "name", {unique: false});
};
IDBOpenRequest.onsuccess = async (e) => {
const loadResources = async () => {
console.debug("[stamp] Connected to IndexedDB");
state.stampDB = e.target.result;
state.stampDB.onerror = (evn) => {
console.warn(`[stamp] Database Error:`);
console.warn(evn.target.errorCode);
};
/** @type {IDBDatabase} */
const db = state.stampDB;
/** @type {IDBRequest<{id: string, name: string, src: string}[]>} */
const FetchAllTransaction = db
.transaction("resources")
@ -648,6 +618,9 @@ const stampTool = () =>
syncResources();
};
};
if (db) loadResources();
else ondatabaseload.on(loadResources);
}
},
populateContextMenu: (menu, state) => {

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>

After

Width:  |  Height:  |  Size: 314 B

5
res/icons/pencil.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="2" x2="22" y2="6"></line>
<path d="M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z"></path>
</svg>

After

Width:  |  Height:  |  Size: 290 B

6
res/icons/save.svg Normal file
View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 374 B

6
res/icons/upload.svg Normal file
View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>

After

Width:  |  Height:  |  Size: 345 B