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, message: messageDialog,
confirm: confirmDialog, confirm: confirmDialog,
} = window.__TAURI__.dialog; } = 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 { Menu, MenuItem, Submenu } = window.__TAURI__.menu ;
const { getCurrentWindow } = window.__TAURI__.window; const { getCurrentWindow } = window.__TAURI__.window;
const { getVersion } = window.__TAURI__.app; const { getVersion } = window.__TAURI__.app;
@ -66,15 +66,19 @@ let maxFileVersion = "2.0"
let filePath = undefined let filePath = undefined
let fileExportPath = undefined let fileExportPath = undefined
let fileWidth = 1500 // let fileWidth = 1500
let fileHeight = 1000 // let fileHeight = 1000
let fileFps = 12 // let fileFps = 12
let playing = false let playing = false
let clipboard = [] let clipboard = []
const CONFIG_FILE_PATH = 'config.json';
const defaultConfig = {
};
let tools = { let tools = {
select: { select: {
icon: "/assets/select.svg", icon: "/assets/select.svg",
@ -250,6 +254,43 @@ let config = {
group: "<mod>g", group: "<mod>g",
zoomIn: "<mod>+", zoomIn: "<mod>+",
zoomOut: "<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) newAudioLayer.track.add(0,action.uuid)
object.audioLayers.push(newAudioLayer) object.audioLayers.push(newAudioLayer)
// TODO: compute image height better // TODO: compute image height better
generateWaveform(img, player.buffer, 50, 25, fileFps) generateWaveform(img, player.buffer, 50, 25, config.framerate)
updateLayers() updateLayers()
}, },
rollback: (action) => { rollback: (action) => {
@ -1985,11 +2026,11 @@ class GraphicsObject {
let frame = layer.getFrame(this.currentFrameNum) let frame = layer.getFrame(this.currentFrameNum)
for (let shape of frame.shapes) { for (let shape of frame.shapes) {
if (context.shapeselection.indexOf(shape) >= 0) { if (context.shapeselection.indexOf(shape) >= 0) {
invertPixels(ctx, fileWidth, fileHeight) invertPixels(ctx, config.fileWidth, config.fileHeight)
} }
shape.draw(context) shape.draw(context)
if (context.shapeselection.indexOf(shape) >= 0) { if (context.shapeselection.indexOf(shape) >= 0) {
invertPixels(ctx, fileWidth, fileHeight) invertPixels(ctx, config.fileWidth, config.fileHeight)
} }
} }
for (let child of layer.children) { for (let child of layer.children) {
@ -2362,7 +2403,7 @@ function playPause() {
console.log(1) console.log(1)
for (let i in audioLayer.sounds) { for (let i in audioLayer.sounds) {
let sound = audioLayer.sounds[i] let sound = audioLayer.sounds[i]
sound.player.start(0,context.activeObject.currentFrameNum / fileFps) sound.player.start(0,context.activeObject.currentFrameNum / config.framerate)
} }
} }
advanceFrame() advanceFrame()
@ -2384,7 +2425,7 @@ function advanceFrame() {
updateUI() updateUI()
if (playing) { if (playing) {
if (context.activeObject.currentFrameNum < context.activeObject.maxFrame - 1) { if (context.activeObject.currentFrameNum < context.activeObject.maxFrame - 1) {
setTimeout(advanceFrame, 1000/fileFps) setTimeout(advanceFrame, 1000/config.framerate)
} else { } else {
playing = false playing = false
for (let audioLayer of context.activeObject.audioLayers) { for (let audioLayer of context.activeObject.audioLayers) {
@ -2407,9 +2448,10 @@ function decrementFrame() {
function _newFile(width, height, fps) { function _newFile(width, height, fps) {
root = new GraphicsObject("root"); root = new GraphicsObject("root");
context.objectStack = [root] context.objectStack = [root]
fileWidth = width config.fileWidth = width
fileHeight = height config.fileHeight = height
fileFps = fps config.framerate = fps
saveConfig()
undoStack = [] undoStack = []
redoStack = [] redoStack = []
for (let stage of document.querySelectorAll(".stage")) { for (let stage of document.querySelectorAll(".stage")) {
@ -2425,7 +2467,7 @@ function _newFile(width, height, fps) {
async function newFile() { async function newFile() {
if (await confirmDialog("Create a new file? Unsaved work will be lost.", {title: "New file", kind: "warning"})) { 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 { try {
const fileData = { const fileData = {
version: "1.3", version: "1.3",
width: fileWidth, width: config.fileWidth,
height: fileHeight, height: config.fileHeight,
fps: fileFps, fps: config.framerate,
actions: undoStack actions: undoStack
} }
const contents = JSON.stringify(fileData); const contents = JSON.stringify(fileData);
await writeTextFile(path, contents) await writeTextFile(path, contents)
filePath = path filePath = path
addRecentFile(path)
lastSaveIndex = undoStack.length - 1; lastSaveIndex = undoStack.length - 1;
console.log(`${path} saved successfully!`); console.log(`${path} saved successfully!`);
} catch (error) { } catch (error) {
@ -2470,20 +2513,7 @@ async function saveAs() {
if (path != undefined) _save(path); if (path != undefined) _save(path);
} }
async function open() { async function _open(path) {
closeDialog()
const path = await openFileDialog({
multiple: false,
directory: false,
filters: [
{
name: 'Lightningbeam files (.beam)',
extensions: ['beam'],
},
],
defaultPath: await documentDir(),
});
if (path) {
try { try {
const contents = await readTextFile(path) const contents = await readTextFile(path)
let file = JSON.parse(contents) let file = JSON.parse(contents)
@ -2509,6 +2539,7 @@ async function open() {
} }
lastSaveIndex = undoStack.length - 1; lastSaveIndex = undoStack.length - 1;
filePath = path filePath = path
addRecentFile(path)
updateUI() updateUI()
} else { } 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' }); 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' });
@ -2525,6 +2556,23 @@ async function open() {
} }
} }
} }
async function open() {
closeDialog()
const path = await openFileDialog({
multiple: false,
directory: false,
filters: [
{
name: 'Lightningbeam files (.beam)',
extensions: ['beam'],
},
],
defaultPath: await documentDir(),
});
if (path) {
_open(path)
}
} }
function revert() { function revert() {
@ -2680,8 +2728,8 @@ async function render() {
const frames = []; const frames = [];
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = fileWidth; // Set desired width canvas.width = config.fileWidth; // Set desired width
canvas.height = fileHeight; // Set desired height canvas.height = config.fileHeight; // Set desired height
let exportContext = { let exportContext = {
...context, ...context,
ctx: canvas.getContext('2d'), ctx: canvas.getContext('2d'),
@ -2695,7 +2743,7 @@ async function render() {
root.currentFrameNum = i root.currentFrameNum = i
exportContext.ctx.fillStyle = "white" exportContext.ctx.fillStyle = "white"
exportContext.ctx.rect(0,0,fileWidth, fileHeight) exportContext.ctx.rect(0,0,config.fileWidth, config.fileHeight)
exportContext.ctx.fill() exportContext.ctx.fill()
await root.draw(exportContext) await root.draw(exportContext)
@ -2709,7 +2757,7 @@ async function render() {
} }
// Step 3: Use UPNG.js to create the animated PNG // 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) // Step 4: Save the APNG file (in Tauri, use writeFile or in the browser, download it)
const apngBlob = new Blob([apng], { type: 'image/png' }); const apngBlob = new Blob([apng], { type: 'image/png' });
@ -2774,8 +2822,8 @@ function stage() {
let scroller = document.createElement("div") let scroller = document.createElement("div")
let stageWrapper = document.createElement("div") let stageWrapper = document.createElement("div")
stage.className = "stage" stage.className = "stage"
stage.width = 1500 stage.width = config.fileWidth
stage.height = 1000 stage.height = config.fileHeight
scroller.className = "scroll" scroller.className = "scroll"
stageWrapper.className = "stageWrapper" stageWrapper.className = "stageWrapper"
let selectionRect = document.createElement("div") 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 // We didn't find an existing region to paintbucket, see if we can make one
try { try {
regionPoints = floodFillRegion(mouse,epsilon,fileWidth,fileHeight,context, debugPoints, debugPaintbucket) regionPoints = floodFillRegion(mouse,epsilon,config.fileWidth,config.fileHeight,context, debugPoints, debugPaintbucket)
} catch (e) { } catch (e) {
updateUI() updateUI()
throw e; throw e;
@ -3007,7 +3055,7 @@ function stage() {
console.log(regionPoints.length) console.log(regionPoints.length)
if (regionPoints.length>0 && regionPoints.length < 10) { if (regionPoints.length>0 && regionPoints.length < 10) {
// probably a very small area, rerun with minimum epsilon // 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 = [] let points = []
for (let point of regionPoints) { for (let point of regionPoints) {
@ -3700,9 +3748,14 @@ function infopanel() {
return panel return panel
} }
async function startup() {
await loadConfig()
createNewFileDialog(_newFile, _open, config);
showNewFileDialog(config)
createNewFileDialog(_newFile); }
showNewFileDialog()
startup()
function createPaneMenu(div) { function createPaneMenu(div) {
const menuItems = ["Item 1", "Item 2", "Item 3"]; // The items for the menu const menuItems = ["Item 1", "Item 2", "Item 3"]; // The items for the menu
@ -3877,22 +3930,22 @@ function updateLayout(element) {
function updateUI() { function updateUI() {
for (let canvas of canvases) { for (let canvas of canvases) {
canvas.width = fileWidth * context.zoomLevel canvas.width = config.fileWidth * context.zoomLevel
canvas.height = fileHeight * context.zoomLevel canvas.height = config.fileHeight * context.zoomLevel
canvas.style.width = `${fileWidth * context.zoomLevel}px` canvas.style.width = `${config.fileWidth * context.zoomLevel}px`
canvas.style.height = `${fileHeight * context.zoomLevel}px` canvas.style.height = `${config.fileHeight * context.zoomLevel}px`
let ctx = canvas.getContext("2d") let ctx = canvas.getContext("2d")
ctx.resetTransform(); ctx.resetTransform();
ctx.scale(context.zoomLevel, context.zoomLevel) ctx.scale(context.zoomLevel, context.zoomLevel)
ctx.beginPath() ctx.beginPath()
ctx.fillStyle = "white" ctx.fillStyle = "white"
ctx.fillRect(0,0,fileWidth,fileHeight) ctx.fillRect(0,0,config.fileWidth,config.fileHeight)
context.ctx = ctx; context.ctx = ctx;
root.draw(context) root.draw(context)
if (context.activeObject != root) { if (context.activeObject != root) {
ctx.fillStyle = "rgba(255,255,255,0.5)" 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) context.activeObject.draw(context)
} }
if (context.activeShape) { if (context.activeShape) {
@ -3903,7 +3956,7 @@ function updateUI() {
if (debugQuadtree) { if (debugQuadtree) {
ctx.fillStyle = "rgba(255,255,255,0.5)" 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 ep = 2.5
const bbox = { const bbox = {
x: { min: context.mousePos.x - ep, max: context.mousePos.x + ep }, x: { min: context.mousePos.x - ep, max: context.mousePos.x + ep },
@ -4708,3 +4761,4 @@ function startToneOnUserInteraction() {
document.addEventListener("keydown", startTone); document.addEventListener("keydown", startTone);
} }
startToneOnUserInteraction() startToneOnUserInteraction()

View File

@ -1,7 +1,11 @@
const { basename, dirname, join } = window.__TAURI__.path;
let overlay; let overlay;
let newFileDialog; let newFileDialog;
function createNewFileDialog(callback) { let displayFiles
function createNewFileDialog(newFileCallback, openFileCallback, config) {
overlay = document.createElement('div'); overlay = document.createElement('div');
overlay.id = 'overlay'; overlay.id = 'overlay';
document.body.appendChild(overlay); document.body.appendChild(overlay);
@ -11,7 +15,6 @@ function createNewFileDialog(callback) {
newFileDialog.classList.add('hidden'); newFileDialog.classList.add('hidden');
document.body.appendChild(newFileDialog); document.body.appendChild(newFileDialog);
// Create dialog content dynamically
const title = document.createElement('h3'); const title = document.createElement('h3');
title.textContent = 'Create New File'; title.textContent = 'Create New File';
newFileDialog.appendChild(title); newFileDialog.appendChild(title);
@ -27,7 +30,8 @@ function createNewFileDialog(callback) {
widthInput.type = 'number'; widthInput.type = 'number';
widthInput.id = 'width'; widthInput.id = 'width';
widthInput.classList.add('dialog-input'); widthInput.classList.add('dialog-input');
widthInput.value = '1500'; // Default value console.log(config.fileWidth)
widthInput.value = config.fileWidth;
newFileDialog.appendChild(widthInput); newFileDialog.appendChild(widthInput);
// Create Height input // Create Height input
@ -41,7 +45,7 @@ function createNewFileDialog(callback) {
heightInput.type = 'number'; heightInput.type = 'number';
heightInput.id = 'height'; heightInput.id = 'height';
heightInput.classList.add('dialog-input'); heightInput.classList.add('dialog-input');
heightInput.value = '1000'; // Default value heightInput.value = config.fileHeight;
newFileDialog.appendChild(heightInput); newFileDialog.appendChild(heightInput);
// Create FPS input // Create FPS input
@ -55,7 +59,7 @@ function createNewFileDialog(callback) {
fpsInput.type = 'number'; fpsInput.type = 'number';
fpsInput.id = 'fps'; fpsInput.id = 'fps';
fpsInput.classList.add('dialog-input'); fpsInput.classList.add('dialog-input');
fpsInput.value = '24'; // Default value fpsInput.value = config.framerate;
newFileDialog.appendChild(fpsInput); newFileDialog.appendChild(fpsInput);
// Create Create button // Create Create button
@ -65,31 +69,87 @@ function createNewFileDialog(callback) {
createButton.onclick = createNewFile; createButton.onclick = createNewFile;
newFileDialog.appendChild(createButton); 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() { function createNewFile() {
const width = document.getElementById('width').value; const width = parseInt(document.getElementById('width').value);
const height = document.getElementById('height').value; const height = parseInt(document.getElementById('height').value);
const fps = document.getElementById('fps').value; const fps = parseInt(document.getElementById('fps').value);
console.log(`New file created with width: ${width} and height: ${height}`); console.log(`New file created with width: ${width} and height: ${height}`);
callback(width, height, fps) newFileCallback(width, height, fps)
closeDialog();
// Add any further logic to handle the new file creation here }
closeDialog(); // Close the dialog after file creation
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; overlay.onclick = closeDialog;
} }
// Show the dialog function showNewFileDialog(config) {
function showNewFileDialog() {
overlay.style.display = 'block'; overlay.style.display = 'block';
newFileDialog.style.display = 'block'; newFileDialog.style.display = 'block';
displayFiles(config.recentFiles); // Reload the recent files
} }
// Close the dialog
function closeDialog() { function closeDialog() {
overlay.style.display = 'none'; overlay.style.display = 'none';
newFileDialog.style.display = 'none'; newFileDialog.style.display = 'none';

View File

@ -537,6 +537,25 @@ button {
#newFileDialog .dialog-button:hover { #newFileDialog .dialog-button:hover {
background-color: #0056b3; 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 { #popupMenu {
background-color: #eee; background-color: #eee;
@ -596,6 +615,13 @@ button {
#newFileDialog .dialog-input { #newFileDialog .dialog-input {
border: 1px solid #333; 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 { #popupMenu {
background-color: #222; background-color: #222;
} }