Add config saving/loading and recent files list on new file dialog

This commit is contained in:
Skyler Lehmkuhl 2024-12-21 04:26:01 -05:00
parent 570bb10f04
commit 237b8882cf
3 changed files with 232 additions and 92 deletions

View File

@ -13,7 +13,7 @@ const {
message: messageDialog,
confirm: confirmDialog,
} = window.__TAURI__.dialog;
const { documentDir, join, basename } = window.__TAURI__.path;
const { documentDir, join, basename, appLocalDataDir } = window.__TAURI__.path;
const { Menu, MenuItem, Submenu } = window.__TAURI__.menu ;
const { getCurrentWindow } = window.__TAURI__.window;
const { getVersion } = window.__TAURI__.app;
@ -66,15 +66,19 @@ let maxFileVersion = "2.0"
let filePath = undefined
let fileExportPath = undefined
let fileWidth = 1500
let fileHeight = 1000
let fileFps = 12
// let fileWidth = 1500
// let fileHeight = 1000
// let fileFps = 12
let playing = false
let clipboard = []
const CONFIG_FILE_PATH = 'config.json';
const defaultConfig = {
};
let tools = {
select: {
icon: "/assets/select.svg",
@ -250,6 +254,43 @@ let config = {
group: "<mod>g",
zoomIn: "<mod>+",
zoomOut: "<mod>-",
},
fileWidth: 800,
fileHeight: 600,
framerate: 24,
recentFiles: []
}
// Load the configuration from the file system
async function loadConfig() {
try {
const configPath = await join(await appLocalDataDir(), CONFIG_FILE_PATH);
const configData = await readTextFile(configPath);
config = JSON.parse(configData);
updateUI()
console.log(config)
} catch (error) {
console.log('Error loading config, returning default config:', error);
}
}
// Save the configuration to a file
async function saveConfig() {
try {
const configPath = await join(await appLocalDataDir(), CONFIG_FILE_PATH);
await writeTextFile(configPath, JSON.stringify(config, null, 2));
} catch (error) {
console.error('Error saving config:', error);
}
}
async function addRecentFile(filePath) {
if (!config.recentFiles.includes(filePath)) {
config.recentFiles.unshift(filePath);
if (config.recentFiles.length > 10) {
config.recentFiles = config.recentFiles.slice(0, 10);
}
await saveConfig(config);
}
}
@ -488,7 +529,7 @@ let actions = {
newAudioLayer.track.add(0,action.uuid)
object.audioLayers.push(newAudioLayer)
// TODO: compute image height better
generateWaveform(img, player.buffer, 50, 25, fileFps)
generateWaveform(img, player.buffer, 50, 25, config.framerate)
updateLayers()
},
rollback: (action) => {
@ -1985,11 +2026,11 @@ class GraphicsObject {
let frame = layer.getFrame(this.currentFrameNum)
for (let shape of frame.shapes) {
if (context.shapeselection.indexOf(shape) >= 0) {
invertPixels(ctx, fileWidth, fileHeight)
invertPixels(ctx, config.fileWidth, config.fileHeight)
}
shape.draw(context)
if (context.shapeselection.indexOf(shape) >= 0) {
invertPixels(ctx, fileWidth, fileHeight)
invertPixels(ctx, config.fileWidth, config.fileHeight)
}
}
for (let child of layer.children) {
@ -2362,7 +2403,7 @@ function playPause() {
console.log(1)
for (let i in audioLayer.sounds) {
let sound = audioLayer.sounds[i]
sound.player.start(0,context.activeObject.currentFrameNum / fileFps)
sound.player.start(0,context.activeObject.currentFrameNum / config.framerate)
}
}
advanceFrame()
@ -2384,7 +2425,7 @@ function advanceFrame() {
updateUI()
if (playing) {
if (context.activeObject.currentFrameNum < context.activeObject.maxFrame - 1) {
setTimeout(advanceFrame, 1000/fileFps)
setTimeout(advanceFrame, 1000/config.framerate)
} else {
playing = false
for (let audioLayer of context.activeObject.audioLayers) {
@ -2407,9 +2448,10 @@ function decrementFrame() {
function _newFile(width, height, fps) {
root = new GraphicsObject("root");
context.objectStack = [root]
fileWidth = width
fileHeight = height
fileFps = fps
config.fileWidth = width
config.fileHeight = height
config.framerate = fps
saveConfig()
undoStack = []
redoStack = []
for (let stage of document.querySelectorAll(".stage")) {
@ -2425,7 +2467,7 @@ function _newFile(width, height, fps) {
async function newFile() {
if (await confirmDialog("Create a new file? Unsaved work will be lost.", {title: "New file", kind: "warning"})) {
showNewFileDialog()
showNewFileDialog(config)
}
}
@ -2433,14 +2475,15 @@ async function _save(path) {
try {
const fileData = {
version: "1.3",
width: fileWidth,
height: fileHeight,
fps: fileFps,
width: config.fileWidth,
height: config.fileHeight,
fps: config.framerate,
actions: undoStack
}
const contents = JSON.stringify(fileData);
await writeTextFile(path, contents)
filePath = path
addRecentFile(path)
lastSaveIndex = undoStack.length - 1;
console.log(`${path} saved successfully!`);
} catch (error) {
@ -2470,6 +2513,50 @@ async function saveAs() {
if (path != undefined) _save(path);
}
async function _open(path) {
try {
const contents = await readTextFile(path)
let file = JSON.parse(contents)
if (file.version == undefined) {
await messageDialog("Could not read file version!", { title: "Load error", kind: 'error' })
return
}
if (file.version >= minFileVersion) {
if (file.version < maxFileVersion) {
_newFile(file.width, file.height, file.fps)
if (file.actions == undefined) {
await messageDialog("File has no content!", {title: "Parse error", kind: 'error'})
return
}
for (let action of file.actions) {
if (!(action.name in actions)) {
await messageDialog(`Invalid action ${action.name}. File may be corrupt.`, { title: "Error", kind: 'error'})
return
}
console.log(action.name)
await actions[action.name].execute(action.action)
undoStack.push(action)
}
lastSaveIndex = undoStack.length - 1;
filePath = path
addRecentFile(path)
updateUI()
} else {
await messageDialog(`File ${path} was created in a newer version of Lightningbeam and cannot be opened in this version.`, { title: 'File version mismatch', kind: 'error' });
}
} else {
await messageDialog(`File ${path} is too old to be opened in this version of Lightningbeam.`, { title: 'File version mismatch', kind: 'error' });
}
} catch (e) {
console.log(e )
if (e instanceof SyntaxError) {
await messageDialog(`Could not parse ${path}, ${e.message}`, { title: 'Error', kind: 'error' })
} else if (e.startsWith("failed to read file as text")) {
await messageDialog(`Could not parse ${path}, is it actually a Lightningbeam file?`, { title: 'Error', kind: 'error' })
}
}
}
async function open() {
closeDialog()
const path = await openFileDialog({
@ -2484,46 +2571,7 @@ async function open() {
defaultPath: await documentDir(),
});
if (path) {
try {
const contents = await readTextFile(path)
let file = JSON.parse(contents)
if (file.version == undefined) {
await messageDialog("Could not read file version!", { title: "Load error", kind: 'error' })
return
}
if (file.version >= minFileVersion) {
if (file.version < maxFileVersion) {
_newFile(file.width, file.height, file.fps)
if (file.actions == undefined) {
await messageDialog("File has no content!", {title: "Parse error", kind: 'error'})
return
}
for (let action of file.actions) {
if (!(action.name in actions)) {
await messageDialog(`Invalid action ${action.name}. File may be corrupt.`, { title: "Error", kind: 'error'})
return
}
console.log(action.name)
await actions[action.name].execute(action.action)
undoStack.push(action)
}
lastSaveIndex = undoStack.length - 1;
filePath = path
updateUI()
} else {
await messageDialog(`File ${path} was created in a newer version of Lightningbeam and cannot be opened in this version.`, { title: 'File version mismatch', kind: 'error' });
}
} else {
await messageDialog(`File ${path} is too old to be opened in this version of Lightningbeam.`, { title: 'File version mismatch', kind: 'error' });
}
} catch (e) {
console.log(e )
if (e instanceof SyntaxError) {
await messageDialog(`Could not parse ${path}, ${e.message}`, { title: 'Error', kind: 'error' })
} else if (e.startsWith("failed to read file as text")) {
await messageDialog(`Could not parse ${path}, is it actually a Lightningbeam file?`, { title: 'Error', kind: 'error' })
}
}
_open(path)
}
}
@ -2680,8 +2728,8 @@ async function render() {
const frames = [];
const canvas = document.createElement('canvas');
canvas.width = fileWidth; // Set desired width
canvas.height = fileHeight; // Set desired height
canvas.width = config.fileWidth; // Set desired width
canvas.height = config.fileHeight; // Set desired height
let exportContext = {
...context,
ctx: canvas.getContext('2d'),
@ -2695,7 +2743,7 @@ async function render() {
root.currentFrameNum = i
exportContext.ctx.fillStyle = "white"
exportContext.ctx.rect(0,0,fileWidth, fileHeight)
exportContext.ctx.rect(0,0,config.fileWidth, config.fileHeight)
exportContext.ctx.fill()
await root.draw(exportContext)
@ -2709,7 +2757,7 @@ async function render() {
}
// Step 3: Use UPNG.js to create the animated PNG
const apng = UPNG.encode(frames, canvas.width, canvas.height, 0, parseInt(100/fileFps));
const apng = UPNG.encode(frames, canvas.width, canvas.height, 0, parseInt(100/config.framerate));
// Step 4: Save the APNG file (in Tauri, use writeFile or in the browser, download it)
const apngBlob = new Blob([apng], { type: 'image/png' });
@ -2774,8 +2822,8 @@ function stage() {
let scroller = document.createElement("div")
let stageWrapper = document.createElement("div")
stage.className = "stage"
stage.width = 1500
stage.height = 1000
stage.width = config.fileWidth
stage.height = config.fileHeight
scroller.className = "scroll"
stageWrapper.className = "stageWrapper"
let selectionRect = document.createElement("div")
@ -2998,7 +3046,7 @@ function stage() {
// We didn't find an existing region to paintbucket, see if we can make one
try {
regionPoints = floodFillRegion(mouse,epsilon,fileWidth,fileHeight,context, debugPoints, debugPaintbucket)
regionPoints = floodFillRegion(mouse,epsilon,config.fileWidth,config.fileHeight,context, debugPoints, debugPaintbucket)
} catch (e) {
updateUI()
throw e;
@ -3007,7 +3055,7 @@ function stage() {
console.log(regionPoints.length)
if (regionPoints.length>0 && regionPoints.length < 10) {
// probably a very small area, rerun with minimum epsilon
regionPoints = floodFillRegion(mouse,1,fileWidth,fileHeight,context, debugPoints)
regionPoints = floodFillRegion(mouse,1,config.fileWidth,config.fileHeight,context, debugPoints)
}
let points = []
for (let point of regionPoints) {
@ -3700,9 +3748,14 @@ function infopanel() {
return panel
}
async function startup() {
await loadConfig()
createNewFileDialog(_newFile, _open, config);
showNewFileDialog(config)
createNewFileDialog(_newFile);
showNewFileDialog()
}
startup()
function createPaneMenu(div) {
const menuItems = ["Item 1", "Item 2", "Item 3"]; // The items for the menu
@ -3877,22 +3930,22 @@ function updateLayout(element) {
function updateUI() {
for (let canvas of canvases) {
canvas.width = fileWidth * context.zoomLevel
canvas.height = fileHeight * context.zoomLevel
canvas.style.width = `${fileWidth * context.zoomLevel}px`
canvas.style.height = `${fileHeight * context.zoomLevel}px`
canvas.width = config.fileWidth * context.zoomLevel
canvas.height = config.fileHeight * context.zoomLevel
canvas.style.width = `${config.fileWidth * context.zoomLevel}px`
canvas.style.height = `${config.fileHeight * context.zoomLevel}px`
let ctx = canvas.getContext("2d")
ctx.resetTransform();
ctx.scale(context.zoomLevel, context.zoomLevel)
ctx.beginPath()
ctx.fillStyle = "white"
ctx.fillRect(0,0,fileWidth,fileHeight)
ctx.fillRect(0,0,config.fileWidth,config.fileHeight)
context.ctx = ctx;
root.draw(context)
if (context.activeObject != root) {
ctx.fillStyle = "rgba(255,255,255,0.5)"
ctx.fillRect(0,0,fileWidth,fileHeight)
ctx.fillRect(0,0,config.fileWidth,config.fileHeight)
context.activeObject.draw(context)
}
if (context.activeShape) {
@ -3903,7 +3956,7 @@ function updateUI() {
if (debugQuadtree) {
ctx.fillStyle = "rgba(255,255,255,0.5)"
ctx.fillRect(0,0,fileWidth,fileHeight)
ctx.fillRect(0,0,config.fileWidth,config.fileHeight)
const ep = 2.5
const bbox = {
x: { min: context.mousePos.x - ep, max: context.mousePos.x + ep },
@ -4707,4 +4760,5 @@ function startToneOnUserInteraction() {
document.addEventListener("click", startTone);
document.addEventListener("keydown", startTone);
}
startToneOnUserInteraction()
startToneOnUserInteraction()

View File

@ -1,7 +1,11 @@
const { basename, dirname, join } = window.__TAURI__.path;
let overlay;
let newFileDialog;
function createNewFileDialog(callback) {
let displayFiles
function createNewFileDialog(newFileCallback, openFileCallback, config) {
overlay = document.createElement('div');
overlay.id = 'overlay';
document.body.appendChild(overlay);
@ -11,7 +15,6 @@ function createNewFileDialog(callback) {
newFileDialog.classList.add('hidden');
document.body.appendChild(newFileDialog);
// Create dialog content dynamically
const title = document.createElement('h3');
title.textContent = 'Create New File';
newFileDialog.appendChild(title);
@ -27,7 +30,8 @@ function createNewFileDialog(callback) {
widthInput.type = 'number';
widthInput.id = 'width';
widthInput.classList.add('dialog-input');
widthInput.value = '1500'; // Default value
console.log(config.fileWidth)
widthInput.value = config.fileWidth;
newFileDialog.appendChild(widthInput);
// Create Height input
@ -41,7 +45,7 @@ function createNewFileDialog(callback) {
heightInput.type = 'number';
heightInput.id = 'height';
heightInput.classList.add('dialog-input');
heightInput.value = '1000'; // Default value
heightInput.value = config.fileHeight;
newFileDialog.appendChild(heightInput);
// Create FPS input
@ -55,7 +59,7 @@ function createNewFileDialog(callback) {
fpsInput.type = 'number';
fpsInput.id = 'fps';
fpsInput.classList.add('dialog-input');
fpsInput.value = '24'; // Default value
fpsInput.value = config.framerate;
newFileDialog.appendChild(fpsInput);
// Create Create button
@ -65,31 +69,87 @@ function createNewFileDialog(callback) {
createButton.onclick = createNewFile;
newFileDialog.appendChild(createButton);
// Recent Files Section
const recentFilesTitle = document.createElement('h4');
recentFilesTitle.textContent = 'Recent Files';
newFileDialog.appendChild(recentFilesTitle);
const recentFilesList = document.createElement('ul');
recentFilesList.id = 'recentFilesList';
newFileDialog.appendChild(recentFilesList);
// Create the new file (simulation)
function createNewFile() {
const width = document.getElementById('width').value;
const height = document.getElementById('height').value;
const fps = document.getElementById('fps').value;
const width = parseInt(document.getElementById('width').value);
const height = parseInt(document.getElementById('height').value);
const fps = parseInt(document.getElementById('fps').value);
console.log(`New file created with width: ${width} and height: ${height}`);
callback(width, height, fps)
// Add any further logic to handle the new file creation here
closeDialog(); // Close the dialog after file creation
newFileCallback(width, height, fps)
closeDialog();
}
async function displayRecentFiles(recentFiles) {
const recentFilesList = document.getElementById('recentFilesList');
const recentFilesTitle = document.querySelector('h4');
recentFilesList.innerHTML = '';
// Only show the list if there are recent files
if (recentFiles.length === 0) {
recentFilesTitle.style.display = 'none';
} else {
recentFilesTitle.style.display = 'block';
const filenames = {};
for (let filePath of recentFiles) {
const filename = await basename(filePath);
const dirPath = await dirname(filePath);
if (!filenames[filename]) {
filenames[filename] = [];
}
filenames[filename].push(dirPath);
}
Object.keys(filenames).forEach((filename) => {
const filePaths = filenames[filename];
// If only one directory, just display the filename
if (filePaths.length === 1) {
const listItem = document.createElement('li');
listItem.textContent = filename;
listItem.onclick = () => openFile(filePaths[0], filename);
recentFilesList.appendChild(listItem);
} else {
// For duplicates, display each directory with the filename
filePaths.forEach((dirPath) => {
const listItem = document.createElement('li');
listItem.innerHTML = `${filename} (${dirPath}/)`;
listItem.onclick = () => openFile(dirPath, filename);
recentFilesList.appendChild(listItem);
});
}
});
}
}
displayFiles = displayRecentFiles
async function openFile(dirPath, filename) {
console.log(await join(dirPath, filename))
openFileCallback(await join(dirPath, filename))
closeDialog()
}
// Close the dialog if the overlay is clicked
overlay.onclick = closeDialog;
}
// Show the dialog
function showNewFileDialog() {
function showNewFileDialog(config) {
overlay.style.display = 'block';
newFileDialog.style.display = 'block';
displayFiles(config.recentFiles); // Reload the recent files
}
// Close the dialog
function closeDialog() {
overlay.style.display = 'none';
newFileDialog.style.display = 'none';

View File

@ -537,6 +537,25 @@ button {
#newFileDialog .dialog-button:hover {
background-color: #0056b3;
}
#recentFilesList li {
word-wrap: break-word;
max-width: 100%;
white-space: normal;
overflow-wrap: break-word;
padding: 5px;
}
#recentFilesList {
list-style-type: none;
padding-left: 0;
}
#recentFilesList li:hover {
cursor: pointer;
background-color: #f0f0f0;
border-radius: 5px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
}
#popupMenu {
background-color: #eee;
@ -596,6 +615,13 @@ button {
#newFileDialog .dialog-input {
border: 1px solid #333;
}
#recentFilesList li:hover {
cursor: pointer;
background-color: #555;
border-radius: 5px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
}
#popupMenu {
background-color: #222;
}