Release 0.7.13-alpha

This commit is contained in:
Skyler Lehmkuhl 2025-01-18 00:31:57 -05:00
commit 73fd191a28
8 changed files with 779 additions and 482 deletions

View File

@ -1,3 +1,12 @@
# 0.7.12-alpha:
New features:
- Add "New Window" command
- Enable files to be opened with Lightningbeam
Bugfixes:
- Fix error when an object is deleted from a frame
- Fix parent references being lost
- Fix objects not showing up when imported multiple times
# 0.7.11-alpha: # 0.7.11-alpha:
Bugfixes: Bugfixes:
- Fix duplicate objects showing up after grouping - Fix duplicate objects showing up after grouping

35
create_release.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
# Ensure the script stops on error
set -e
# Check if a version argument was passed
if [ -z "$1" ]; then
echo "Usage: ./create-release.sh <version>"
exit 1
fi
VERSION=$1
RELEASE_BRANCH="release"
MAIN_BRANCH="main"
CONFIG_FILE="src-tauri/tauri.conf.json"
echo "Updating version to $VERSION in $CONFIG_FILE..."
jq --arg version "$VERSION" '.version = $version' $CONFIG_FILE > tmp.json && mv tmp.json $CONFIG_FILE
echo "Committing to main..."
git add $CONFIG_FILE
git commit -m "Bump version to $VERSION"
echo "Checking out the release branch..."
git checkout $RELEASE_BRANCH
echo "Merging the main branch into $RELEASE_BRANCH..."
git merge $MAIN_BRANCH --no-ff -m "Release $VERSION"
echo "Pushing changes to the release branch..."
git push origin $RELEASE_BRANCH
git checkout $MAIN_BRANCH
echo "Release $VERSION created and pushed successfully!"

974
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View File

@ -1,10 +1,10 @@
use std::sync::{Arc, Mutex}; use std::{path::PathBuf, sync::Mutex};
use tauri_plugin_log::{Target, TargetKind}; use tauri_plugin_log::{Target, TargetKind};
use log::{trace, info, debug, warn, error}; use log::{trace, info, debug, warn, error};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use chrono::Local; use chrono::Local;
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder}; use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder};
#[derive(Default)] #[derive(Default)]
@ -39,27 +39,131 @@ fn error(msg: String) {
error!("{}",msg); error!("{}",msg);
} }
use tauri::PhysicalSize;
#[tauri::command] #[tauri::command]
async fn create_window(app: tauri::AppHandle) { async fn create_window(app: tauri::AppHandle, path: Option<String>) {
let state = app.state::<Mutex<AppState>>(); let state = app.state::<Mutex<AppState>>();
// Lock the mutex to get mutable access: // Lock the mutex to get mutable access:
let mut state = state.lock().unwrap(); let mut state = state.lock().unwrap();
// Increment the counter and generate a unique window label // Increment the counter and generate a unique window label
let window_label = format!("window{}", state.counter); let window_label = format!("window{}", state.counter);
state.counter += 1; state.counter += 1;
// Build the new window with the unique label // Build the new window with the unique label
let _webview_window = WebviewWindowBuilder::new(&app, &window_label, WebviewUrl::App("index.html".into())) let webview_window = WebviewWindowBuilder::new(&app, &window_label, WebviewUrl::App("index.html".into()))
.title("Lightningbeam")
.build() .build()
.unwrap(); .unwrap();
// Get the current monitor's screen size from the new window
if let Ok(Some(monitor)) = webview_window.current_monitor() {
let screen_size = monitor.size(); // Get the size of the monitor
let width = 4096;
let height = 4096;
// Set the window size to be the smaller of the specified size or the screen size
let new_width = width.min(screen_size.width as u32);
let new_height = height.min(screen_size.height as u32 - 100);
// Set the size using PhysicalSize
webview_window.set_size(tauri::Size::Physical(PhysicalSize::new(new_width, new_height)))
.expect("Failed to set window size");
} else {
eprintln!("Could not detect the current monitor.");
}
// Set the opened file if provided
if let Some(val) = path {
// Pass path data to the window via JavaScript
webview_window.eval(&format!("window.openedFiles = [\"{val}\"]")).unwrap();
// Set the window title if provided
webview_window.set_title(&val).expect("Failed to set window title");
}
}
fn handle_file_associations(app: AppHandle, files: Vec<PathBuf>) {
// -- Scope handling start --
// You can remove this block if you only want to know about the paths, but not actually "use" them in the frontend.
// This requires the `fs` tauri plugin and is required to make the plugin's frontend work:
use tauri_plugin_fs::FsExt;
let fs_scope = app.fs_scope();
// This is for the `asset:` protocol to work:
let asset_protocol_scope = app.asset_protocol_scope();
for file in &files {
// This requires the `fs` plugin:
let _ = fs_scope.allow_file(file);
// This is for the `asset:` protocol:
let _ = asset_protocol_scope.allow_file(file);
}
// -- Scope handling end --
let files = files
.into_iter()
.map(|f| {
let file = f.to_string_lossy().replace('\\', "\\\\"); // escape backslash
format!("\"{file}\"",) // wrap in quotes for JS array
})
.collect::<Vec<_>>()
.join(",");
warn!("{}",files);
let window = app.get_webview_window("main").unwrap();
window.eval(&format!("window.openedFiles = [{files}]")).unwrap();
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let pkg_name = env!("CARGO_PKG_NAME").to_string(); let pkg_name = env!("CARGO_PKG_NAME").to_string();
tauri::Builder::default() tauri::Builder::default()
.setup(|app| {
{
app.manage(Mutex::new(AppState::default()));
}
#[cfg(any(windows, target_os = "linux"))] // Windows/Linux needs different handling from macOS
{
let mut files = Vec::new();
// NOTICE: `args` may include URL protocol (`your-app-protocol://`)
// or arguments (`--`) if your app supports them.
// files may aslo be passed as `file://path/to/file`
for maybe_file in std::env::args().skip(1) {
// skip flags like -f or --flag
if maybe_file.starts_with('-') {
continue;
}
// handle `file://` path urls and skip other urls
if let Ok(url) = Url::parse(&maybe_file) {
// if let Ok(url) = url::Url::parse(&maybe_file) {
if let Ok(path) = url.to_file_path() {
files.push(path);
}
} else {
files.push(PathBuf::from(maybe_file))
}
}
handle_file_associations(app.handle().clone(), files);
}
#[cfg(debug_assertions)] // only include this code on debug builds
{
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
window.close_devtools();
}
Ok(())
})
.plugin( .plugin(
tauri_plugin_log::Builder::new() tauri_plugin_log::Builder::new()
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal) .timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
@ -81,26 +185,28 @@ pub fn run() {
Target::new(TargetKind::LogDir { file_name: Some("logs".to_string()) }), Target::new(TargetKind::LogDir { file_name: Some("logs".to_string()) }),
Target::new(TargetKind::Webview), Target::new(TargetKind::Webview),
]) ])
.build(), .build()
) )
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet, trace, debug, info, warn, error, create_window]) .invoke_handler(tauri::generate_handler![greet, trace, debug, info, warn, error, create_window])
// .manage(window_counter) // .manage(window_counter)
.setup(|app| { .build(tauri::generate_context!())
#[cfg(debug_assertions)] // only include this code on debug builds .expect("error while running tauri application")
{ .run(
let window = app.get_webview_window("main").unwrap(); #[allow(unused_variables)]
window.open_devtools(); |app, event| {
window.close_devtools(); #[cfg(any(target_os = "macos", target_os = "ios"))]
} if let tauri::RunEvent::Opened { urls } = event {
{ let files = urls
app.manage(Mutex::new(AppState::default())); .into_iter()
} .filter_map(|url| url.to_file_path().ok())
Ok(()) .collect::<Vec<_>>();
})
.run(tauri::generate_context!()) handle_file_associations(app.clone(), files);
.expect("error while running tauri application"); }
},
);
tracing_subscriber::fmt().with_env_filter(EnvFilter::new(format!("{}=trace", pkg_name))).init(); tracing_subscriber::fmt().with_env_filter(EnvFilter::new(format!("{}=trace", pkg_name))).init();
} }

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Lightningbeam", "productName": "Lightningbeam",
"version": "0.7.11-alpha", "version": "0.7.13-alpha",
"identifier": "org.lightningbeam.core", "identifier": "org.lightningbeam.core",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"
@ -17,7 +17,10 @@
} }
], ],
"security": { "security": {
"csp": null "csp": null,
"assetProtocol": {
"enable": true
}
} }
}, },
"bundle": { "bundle": {
@ -43,6 +46,14 @@
"files": {}, "files": {},
"release": "1" "release": "1"
} }
} },
"fileAssociations": [
{
"ext": [
"beam"
],
"mimeType": "text/plain"
}
]
} }
} }

View File

@ -354,6 +354,7 @@ let config = {
undo: "<mod>z", undo: "<mod>z",
redo: "<mod>Z", redo: "<mod>Z",
new: "<mod>n", new: "<mod>n",
newWindow: "<mod>N",
save: "<mod>s", save: "<mod>s",
saveAs: "<mod>S", saveAs: "<mod>S",
open: "<mod>o", open: "<mod>o",
@ -3688,6 +3689,9 @@ class GraphicsObject extends Widget {
graphicsObject.currentFrameNum = json.currentFrameNum; graphicsObject.currentFrameNum = json.currentFrameNum;
graphicsObject.currentLayer = json.currentLayer; graphicsObject.currentLayer = json.currentLayer;
graphicsObject.children = []; graphicsObject.children = [];
if (json.parent in pointerList) {
graphicsObject.parent = pointerList[json.parent]
}
for (let layer of json.layers) { for (let layer of json.layers) {
graphicsObject.layers.push(Layer.fromJSON(layer)); graphicsObject.layers.push(Layer.fromJSON(layer));
} }
@ -3714,6 +3718,7 @@ class GraphicsObject extends Widget {
json.currentFrameNum = this.currentFrameNum; json.currentFrameNum = this.currentFrameNum;
json.currentLayer = this.currentLayer; json.currentLayer = this.currentLayer;
json.layers = []; json.layers = [];
json.parent = this.parent?.idx
for (let layer of this.layers) { for (let layer of this.layers) {
json.layers.push(layer.toJSON(randomizeUuid)); json.layers.push(layer.toJSON(randomizeUuid));
} }
@ -4369,6 +4374,10 @@ function decrementFrame() {
updateUI(); updateUI();
} }
function newWindow(path) {
invoke("create_window", {app: window.__TAURI__.app, path: path})
}
function _newFile(width, height, fps) { function _newFile(width, height, fps) {
root = new GraphicsObject("root"); root = new GraphicsObject("root");
context.objectStack = [root]; context.objectStack = [root];
@ -4409,7 +4418,7 @@ async function _save(path) {
// console.log(action.name); // console.log(action.name);
// } // }
const fileData = { const fileData = {
version: "1.7.6", version: "1.7.7",
width: config.fileWidth, width: config.fileWidth,
height: config.fileHeight, height: config.fileHeight,
fps: config.framerate, fps: config.framerate,
@ -4583,6 +4592,19 @@ async function _open(path, returnJson = false) {
undoStack.push(action); undoStack.push(action);
} }
} else { } else {
if (file.version < "1.7.7") {
function setParentReferences(obj, parentIdx = null) {
if (obj.type === "GraphicsObject") {
obj.parent = parentIdx; // Set the parent property
}
Object.values(obj).forEach(child => {
if (typeof child === 'object' && child !== null) setParentReferences(child, obj.type === "GraphicsObject" ? obj.idx : parentIdx);
})
}
setParentReferences(file.json)
console.log(file.json)
}
if (file.version < "1.7.6") { if (file.version < "1.7.6") {
function restoreLineColors(obj) { function restoreLineColors(obj) {
// Step 1: Create colorMapping dictionary // Step 1: Create colorMapping dictionary
@ -4773,23 +4795,27 @@ async function importFile() {
function assignUUIDs(obj, existing) { function assignUUIDs(obj, existing) {
const uuidCache = {}; // Cache to store UUIDs for existing values const uuidCache = {}; // Cache to store UUIDs for existing values
function deepAssign(obj) { function replaceUuids(obj) {
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (typeof value === "object" && value !== null) { if (typeof value === "object" && value !== null) {
// Recurse for nested objects replaceUuids(value);
deepAssign(value);
} else if (value in existing && key != "name") { } else if (value in existing && key != "name") {
// If the value is in the "existing" list, assign a UUID
if (!uuidCache[value]) { if (!uuidCache[value]) {
uuidCache[value] = uuidv4(); // Store the generated UUID for the value uuidCache[value] = uuidv4();
} }
obj[key] = uuidCache[value]; // Assign the UUID to the object property obj[key] = uuidCache[value];
} else if (key in existing) { }
// If the value is in the "existing" list, assign a UUID }
if (!uuidCache[key]) { }
uuidCache[key] = uuidv4(); // Store the generated UUID for the value
} function replaceReferences(obj) {
obj[key] = uuidCache[key]; // Assign the UUID to the object property for (const [key, value] of Object.entries(obj)) {
if (key in existing) {
obj[uuidCache[key]] = obj[key];
delete obj[key]
}
if (typeof value === "object" && value !== null) {
replaceReferences(value);
} else if (value in uuidCache) { } else if (value in uuidCache) {
obj[key] = value obj[key] = value
} }
@ -4797,7 +4823,8 @@ async function importFile() {
} }
// Start the recursion with the provided object // Start the recursion with the provided object
deepAssign(obj); replaceUuids(obj);
replaceReferences(obj)
return obj; // Return the updated object return obj; // Return the updated object
} }
@ -5944,6 +5971,7 @@ function stage() {
for (let child of context.selection) { for (let child of context.selection) {
if (!context.activeObject.currentFrame) continue; if (!context.activeObject.currentFrame) continue;
if (!context.activeObject.currentFrame.keys) continue; if (!context.activeObject.currentFrame.keys) continue;
if (!child.idx in context.activeObject.currentFrame.keys) continue;
context.activeObject.currentFrame.keys[child.idx].x += context.activeObject.currentFrame.keys[child.idx].x +=
mouse.x - context.lastMouse.x; mouse.x - context.lastMouse.x;
context.activeObject.currentFrame.keys[child.idx].y += context.activeObject.currentFrame.keys[child.idx].y +=
@ -6389,6 +6417,7 @@ function timeline() {
const frame = layer.getFrame(timeline_cvs.clicked_frame); const frame = layer.getFrame(timeline_cvs.clicked_frame);
if (frame.exists) { if (frame.exists) {
console.log(frame.keys)
if (!e.shiftKey) { if (!e.shiftKey) {
// Check if the clicked frame is already in the selection // Check if the clicked frame is already in the selection
const existingIndex = context.selectedFrames.findIndex( const existingIndex = context.selectedFrames.findIndex(
@ -6700,7 +6729,9 @@ function outliner(object = undefined) {
async function startup() { async function startup() {
await loadConfig(); await loadConfig();
createNewFileDialog(_newFile, _open, config); createNewFileDialog(_newFile, _open, config);
showNewFileDialog(config); if (!window.openedFiles) {
showNewFileDialog(config);
}
} }
startup(); startup();
@ -7881,6 +7912,12 @@ async function renderMenu() {
action: newFile, action: newFile,
accelerator: getShortcut("new"), accelerator: getShortcut("new"),
}, },
{
text: "New Window",
enabled: true,
action: newWindow,
accelerator: getShortcut("newWindow"),
},
{ {
text: "Save", text: "Save",
enabled: true, enabled: true,
@ -8306,3 +8343,10 @@ function renderAll() {
} }
renderAll(); renderAll();
if (window.openedFiles?.length>0) {
_open(window.openedFiles[0])
for (let i=1; i<window.openedFiles.length; i++) {
newWindow(window.openedFiles[i])
}
}

View File

@ -145,12 +145,14 @@ function createNewFileDialog(newFileCallback, openFileCallback, config) {
} }
function showNewFileDialog(config) { function showNewFileDialog(config) {
if (!overlay) return;
overlay.style.display = 'block'; overlay.style.display = 'block';
newFileDialog.style.display = 'block'; newFileDialog.style.display = 'block';
displayFiles(config.recentFiles); // Reload the recent files displayFiles(config.recentFiles); // Reload the recent files
} }
function closeDialog() { function closeDialog() {
if (!overlay) return;
overlay.style.display = 'none'; overlay.style.display = 'none';
newFileDialog.style.display = 'none'; newFileDialog.style.display = 'none';
} }