FEAT: Launch with custom config location
FEAT: Generate banner FEAT: Start blender with banner
This commit is contained in:
parent
132b1ccee7
commit
b9b6bd1490
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal 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
9
.prettierignore
Normal 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
17
.prettierrc
Normal 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
9
.vscode/extensions.json
vendored
Normal 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
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"svelte.enable-ts-plugin": true
|
||||
}
|
||||
5
bun.lock
5
bun.lock
@ -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=="],
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user