FEAT: Launch with custom config location

FEAT: Generate banner
FEAT: Start blender with banner
This commit is contained in:
valerio 2026-03-10 22:14:27 +01:00
parent 132b1ccee7
commit b9b6bd1490
11 changed files with 224 additions and 30 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

9
.prettierignore Normal file
View File

@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/

17
.prettierrc Normal file
View File

@ -0,0 +1,17 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": [
"prettier-plugin-svelte"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

9
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"recommendations": [
"svelte.svelte-vscode",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"svelte.enable-ts-plugin": true
}

View File

@ -16,6 +16,7 @@
"@tauri-apps/plugin-upload": "~2",
"@tauri-apps/plugin-window-state": "~2",
"@webtui/css": "^0.1.6",
"figlet": "^1.11.0",
"svelte-splitpanes": "^8.0.12",
"svelte-tabs": "^1.1.0",
},
@ -283,6 +284,8 @@
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
@ -335,6 +338,8 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"figlet": ["figlet@1.11.0", "", { "dependencies": { "commander": "^14.0.0" }, "bin": { "figlet": "bin/index.js" } }, "sha512-EEx3OS/l2bFqcUNN2NM9FPJp8vAMrgbCxsbl2hbcJNNxOEwVe3mEzrhan7TbJQViZa8mMqhihlbCaqD+LyYKTQ=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],

View File

@ -27,6 +27,7 @@
"@tauri-apps/plugin-upload": "~2",
"@tauri-apps/plugin-window-state": "~2",
"@webtui/css": "^0.1.6",
"figlet": "^1.11.0",
"svelte-splitpanes": "^8.0.12",
"svelte-tabs": "^1.1.0"
},

View File

@ -1,5 +1,6 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use std::collections::HashMap;
use std::fs::File;
use std::path::Path;
@ -50,13 +51,24 @@ fn strip_single_top_level_directory(target_dir: &Path) -> Result<bool, std::io::
}
#[tauri::command]
fn launch_binary(path: String, args: Vec<String>) -> Result<String, String> {
fn launch_binary(
path: String,
args: Vec<String>,
env: Option<HashMap<String, String>>,
) -> Result<String, String> {
use std::process::{Command, Stdio};
Command::new(path)
let mut command = Command::new(path);
command
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.stderr(Stdio::null());
if let Some(env_vars) = env {
command.envs(env_vars);
}
command
.spawn()
.map(|_| "Launched".into())
.map_err(|e| e.to_string())

View File

@ -1,6 +1,10 @@
<script>
import { deleteVersion, launchBlenderVersion } from '$lib/library';
import { toggleFavourite } from '$lib/library';
import {
deleteVersion,
launchBlenderVersion,
toggleFavourite,
registerVersion
} from '$lib/library';
import Button from '$lib/components/Button.svelte';
import Menu from '$lib/components/Menu.svelte';
let { version } = $props();
@ -44,8 +48,10 @@
<button
type="button"
class="menu-item"
onclick={close}
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && close()}>Option 3</button
onclick={() => registerVersion(version) && close()}
onkeydown={(e) =>
(e.key === 'Enter' || e.key === ' ') && registerVersion(version) && close()}
>Default</button
>
{/snippet}
</Menu>

View File

@ -7,7 +7,11 @@ import {
import { stat, mkdir, readDir, copyFile, rename, remove } from '@tauri-apps/plugin-fs';
import { open } from '@tauri-apps/plugin-dialog';
import baseicon from '$lib/assets/baseicon.png';
import europa from '$lib/assets/fonts/Europa-Mono-Medium.otf';
import { BASE_LIBRARY_DIR } from './settings';
import figlet from 'figlet';
import shadow from 'figlet/fonts/Classy';
figlet.parseFont('Big', shadow);
/**
* Create a custom icon with text overlay
@ -19,45 +23,32 @@ export async function createIcon(iconText) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Load the base icon image
const img = new Image();
img.src = baseicon;
// Wait for image to load
await new Promise((resolve) => {
img.onload = resolve;
});
// Set canvas dimensions to match image
canvas.width = img.width;
canvas.height = img.height;
// Draw the base icon
ctx.drawImage(img, 0, 0);
// Set text properties
ctx.fillStyle = '#7d70ba';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// Load the custom font
const fontFace = new FontFace(
'Europa-Mono',
`url(${baseicon.replace('baseicon.png', 'fonts/Europa-Mono-Medium.otf')})`
);
const fontFace = new FontFace('Europa-Mono', `url(${europa})`);
await fontFace.load();
document.fonts.add(fontFace);
// Set font size based on canvas dimensions
const fontSize = canvas.width / 6;
ctx.font = `${fontSize}px Europa-Mono`;
// Set font size based on canvas dimensions
// Add version text in the center
const text = iconText || 'XXX';
ctx.fillText(text, 150, 150);
// Convert canvas to blob
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(blob);
@ -65,6 +56,71 @@ export async function createIcon(iconText) {
});
}
/**
* Create a custom banner with version number next to the baseIcon image
* @param {string} versionText - Version text to display
* @returns {Promise<Blob>} Promise resolving to a PNG blob of the banner
*/
export async function createBanner(versionText) {
// Create canvas element for banner
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set banner dimensions
canvas.width = 600;
canvas.height = 90;
// Draw background
ctx.fillStyle = '#171a21';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Load and draw base icon
const img = new Image();
img.src = baseicon;
await new Promise((resolve) => {
img.onload = resolve;
});
// Calculate icon position (centered vertically, left-aligned)
const iconSize = 70;
const iconX = 20;
const iconY = (canvas.height - iconSize) / 2;
// Add version text
ctx.fillStyle = '#f5f5f5';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const fontFace = new FontFace('Europa-Mono', `url(${europa})`);
await fontFace.load();
document.fonts.add(fontFace);
const fontSize = 10;
ctx.font = `${fontSize}px Europa-Mono`;
// Position text to the right of the icon
const textX = 20;
const textY = canvas.height / 2;
const bannerText = await figlet.text(versionText + ' - Base', { font: 'Big' });
const lines = bannerText.split('\n');
const lineHeight = fontSize * 1.2;
const startY = textY - (lines.length * lineHeight) / 2 + lineHeight / 2;
ctx.font = `${fontSize}px monospace`;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
lines.forEach((line, index) => {
ctx.fillText(line, textX, startY + index * lineHeight);
});
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(blob);
}, 'image/png');
});
}
/**
* Create library directory structure if it doesn't exist
* @param {string} libraryDir - Library directory path
@ -72,13 +128,10 @@ export async function createIcon(iconText) {
*/
export async function ensureLibraryStructure(libraryDir) {
try {
// Create main library directory
const baseDir = await join(libraryDir, BASE_LIBRARY_DIR);
// Create blender subdirectory
const blenderDir = await join(baseDir, 'blender');
await mkdir(blenderDir, { recursive: true });
// Create templates subdirectory
const templatesDir = await join(baseDir, 'templates');
await mkdir(templatesDir, { recursive: true });
@ -99,7 +152,7 @@ export async function ensureLibraryStructure(libraryDir) {
*/
export async function selectDirectory(currentPath) {
try {
// Use currentPath if provided, otherwise use app data directory
// Use currentPath if proided, otherwise use app data directory
let defaultPath = await appDataDir();
if (currentPath) {
defaultPath = currentPath;

View File

@ -1,5 +1,5 @@
import { currentSettings } from '$lib/settings.js';
import { createIcon } from './file_utils';
import { createIcon, createBanner } from './file_utils';
import { platform } from '@tauri-apps/plugin-os';
import { join, localDataDir } from '@tauri-apps/api/path';
import { exists, stat, readDir, writeFile, remove } from '@tauri-apps/plugin-fs';
@ -22,6 +22,20 @@ import { updateDownloadProgress } from './download';
export const currentInstalledVersions = writable([]);
const favouritesStore = new LazyStore('versions.json');
export async function generateVersionBanner(version) {
const bannerPath = `${version.path}/banner.png`;
if (await exists(bannerPath)) {
return;
}
try {
const banner = await createBanner(version.version);
const bannerBuffer = new Uint8Array(await banner.arrayBuffer());
await writeFile(bannerPath, bannerBuffer);
} catch (error) {
console.error(`Failed to generate banner for version: ${version.version}`, error);
}
}
/**
* Create icon and write it to disk
* @param {blenderVersion} version
@ -39,7 +53,6 @@ export async function generateVersionIcon(version) {
}
try {
const newIcon = await createIcon(version.version);
const iconPath = `${version.path}/icon.png`;
const iconBuffer = new Uint8Array(await newIcon.arrayBuffer());
await writeFile(iconPath, iconBuffer);
} catch (error) {
@ -286,6 +299,7 @@ export async function getInstalledVersions() {
//check if version has an icon, if not generate one
await generateVersionIcon(newVersion);
await generateVersionBanner(newVersion);
// create desktop file if autoCreateShortcuts is enabled
if (settings.autoCreateShortcuts) {
await createDesktopFile(newVersion);
@ -314,11 +328,11 @@ export async function getInstalledVersions() {
}
/**
* Launch a specific Blender version.
* Register a Blender version with the system.
* @param {blenderVersion} version - The version object to launch
* @returns {Promise<{success: boolean, message: string}>} Result of launch attempt
*/
export async function launchBlenderVersion(version) {
export async function registerVersion(version) {
try {
if (!version || !version.executable) {
return {
@ -338,7 +352,62 @@ export async function launchBlenderVersion(version) {
};
}
// Launch the executable
invoke('launch_binary', { path: version.executable, args: [] });
invoke('launch_binary', { path: version.executable, args: ['--register'] });
} catch (error) {
console.error('Error registering Blender:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `Registration failed: ${errorMessage}`
};
}
}
/**
* Launch a specific Blender version.
* @param {blenderVersion} version - The version object to launch
* @returns {Promise<{success: boolean, message: string}>} Result of launch attempt
*/
export async function launchBlenderVersion(version, template = null) {
try {
if (!version || !version.executable) {
return {
success: false,
message: 'Invalid version or executable not found'
};
}
// Check if executable exists
try {
await stat(version.executable);
} catch (error) {
console.error(error);
return {
success: false,
message: `Executable not found: ${version.executable}`
};
}
const settings = get(currentSettings);
const libraryDir = settings.libraryDir;
if (!libraryDir) {
return {
success: false,
message: `couldn't find library: ${version.executable}`
};
}
const blenderConfigPath = await join(libraryDir, BASE_LIBRARY_DIR, 'config', version.version);
// Launch the executable
invoke('launch_binary', {
path: version.executable,
args: [],
env: {
BLENDER_USER_RESOURCES: blenderConfigPath,
BLENDER_CUSTOM_SPLASH_BANNER: await join(version.path, 'banner.png')
}
});
} catch (error) {
console.error('Error launching Blender:', error);
const errorMessage = error instanceof Error ? error.message : String(error);