smoothie/src/lib/library.js
valerio 1cecec5a97 FIX:
Template Upload
Template Launch
Template Deletion
2026-07-25 16:56:25 +02:00

612 lines
17 KiB
JavaScript

import { currentSettings } from '$lib/settings.js';
import { createIcon, createBanner, ensureDirectory } 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';
import { invoke } from '@tauri-apps/api/core';
import { writable, get } from 'svelte/store';
import { LazyStore } from '@tauri-apps/plugin-store';
import { BASE_LIBRARY_DIR } from '$lib/settings.js';
import { updateDownloadProgress } from './download';
/** INSTALLED BLENDER VERSION TYPE
* @typedef {Object} blenderVersion
* @property {string} version - Version string (e.g., "5.0.0")
* @property {string} path - Full path to version directory
* @property {string} executable - Path to Blender executable
* @property {string} platform - Detected platform (windows, linux, macos)
* @property {Date} [installDate] - Installation date (if available)
* @property {Array<string>} [templates] - List of favourite scenes for this version
*/
export const installedVersionsStore = writable([]);
export const selectedVersionStore = writable(null);
const favouritesStore = new LazyStore('versions.json');
export async function selectVersion(version) {
selectedVersionStore.set(version);
}
export async function generateTemplateBanner(templateDir) {
const bannerPath = `${templateDir}/banner.png`;
if (await exists(bannerPath)) {
return;
}
try {
const templateName = templateDir.split('/').pop() || templateDir.split('\\').pop();
const banner = await createBanner(templateName);
const bannerBuffer = new Uint8Array(await banner.arrayBuffer());
await writeFile(bannerPath, bannerBuffer);
} catch (error) {
console.error(`Failed to generate banner for template: ${templateDir}`, error);
}
}
export async function generateVersionBanner(version) {
const bannerPath = await join(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
*/
export async function generateVersionIcon(version) {
//
//
if (!(await exists(version.path))) {
console.warn(`Version path does not exist: ${version.path}`);
return;
}
const iconPath = `${version.path}/icon.png`;
if (await exists(iconPath)) {
return;
}
try {
const newIcon = await createIcon(version.version);
const iconBuffer = new Uint8Array(await newIcon.arrayBuffer());
await writeFile(iconPath, iconBuffer);
} catch (error) {
console.warn('Failed to create or save icon:', error);
}
}
/**
* Clean up orphaned desktop files for versions that are no longer installed.
* @param {blenderVersion[]} versions Array of currently installed versions
*/
export async function cleanupOrphanedDesktopFiles(versions) {
const currentPlatform = await platform();
if (currentPlatform !== 'linux') {
return;
}
const installedVersionStrings = versions.map((v) => v.version);
const applicationsDir = await join(await localDataDir(), 'applications');
if (!(await exists(applicationsDir))) {
return;
}
try {
const entries = await readDir(applicationsDir);
for (const entry of entries) {
if (entry.isDirectory) {
continue;
}
const fileName = entry.name;
const match = fileName.match(/^blender-smoothie-(.+)\.desktop$/);
if (match) {
const version = match[1];
if (!installedVersionStrings.includes(version)) {
const desktopFilePath = await join(applicationsDir, fileName);
try {
await remove(desktopFilePath);
console.log(`Removed orphaned desktop file: ${desktopFilePath}`);
} catch (error) {
console.warn(`Failed to remove orphaned desktop file ${desktopFilePath}:`, error);
}
}
}
}
} catch (error) {
console.warn('Error cleaning up orphaned desktop files:', error);
}
}
/**
* Remove the .desktop file for a Blender version (Linux only).
* @param {blenderVersion} version
*/
export async function removeDesktopFile(version) {
const currentPlatform = await platform();
if (currentPlatform !== 'linux') {
return;
}
const desktopFilePath = await join(
await localDataDir(),
`/applications/blender-smoothie-${version.version}.desktop`
);
if (!(await exists(desktopFilePath))) {
return;
}
try {
await remove(desktopFilePath);
} catch (error) {
console.warn('Failed to remove .desktop file:', error);
}
}
/**
* Remove all .desktop files for the given versions (Linux only).
* @param {blenderVersion[]} versions
*/
export async function removeAllDesktopFilesForVersions(versions) {
const currentPlatform = await platform();
if (currentPlatform !== 'linux') {
return;
}
for (const version of versions) {
await removeDesktopFile(version);
}
}
/**
* Create a .desktop file for a Blender version (Linux only).
* @param {blenderVersion} version
*/
export async function createDesktopFile(version, forceRecreate = false) {
const currentPlatform = platform();
if (currentPlatform !== 'linux') {
return;
}
const desktopFilePath = await join(
await localDataDir(),
`/applications/blender-smoothie-${version.version}.desktop`
);
if (await exists(desktopFilePath)) {
if (!forceRecreate) {
return;
}
await remove(desktopFilePath);
}
try {
const desktopFileContent = `[Desktop Entry]
Type=Application
Name=Blender ${version.version}
Comment=Blender ${version.version} - 3D creation suite
Exec=${version.executable.replace(/ /g, '\\ ')} %F
Icon=${version.path.replace(/ /g, '\\ ')}/icon.png
Terminal=false
Categories=Graphics;3DGraphics;
MimeType=application/x-blender;
`;
console.log('creating desktop file:', desktopFilePath);
await writeFile(desktopFilePath, new TextEncoder().encode(desktopFileContent));
} catch (error) {
console.warn('Failed to create .desktop file:', error);
}
}
export async function removeTemplate(templatePath) {
if (await exists(templatePath)) {
try {
await remove(templatePath, { recursive: true });
} catch (error) {
console.warn(`Failed to remove template path: ${templatePath}`, error);
}
} else {
console.warn(`Template path does not exist: ${templatePath}`);
}
}
export async function removeVersion(version) {
if (await exists(version.path)) {
try {
await remove(version.path, { recursive: true });
} catch (error) {
console.warn(`Failed to remove version path: ${version.path}`, error);
}
} else {
console.warn(`Version path does not exist: ${version.path}`);
}
}
/**
* Toggle the favourite status of a version.
* @param {blenderVersion} version The version to toggle.
*/
export async function toggleFavourite(version) {
const versions = get(installedVersionsStore);
console.log();
const versionObj = versions.find((v) => v.version === version.version);
if (versionObj) {
versionObj.favourite = !versionObj.favourite;
} else {
console.error('version not found', version);
}
installedVersionsStore.set(versions);
await favouritesStore.set(version.version, versionObj.favourite);
await favouritesStore.save();
}
/**
* Remove a template from the library.
* @param {blenderVersion} version The version to remove the template from.
* @param {string} template The name of the template to remove.
*/
export async function deleteTemplate(version, template) {
const templatePath = await join(await getTemplatesDir(version), template);
try {
await removeTemplate(templatePath);
await getInstalledVersions();
} catch (error) {
console.error('Failed to delete template', error);
}
}
/**
* Delete a version and remove it from disk.
* @param {blenderVersion} version The version to delete.
*/
export async function deleteVersion(version) {
try {
// Remove the version directory from disk
await removeVersion(version);
updateDownloadProgress(version.version, { loaded: 0, total: 0, percent: 0 });
// Remove the .desktop file if it exists (Linux only)
await removeDesktopFile(version);
// Remove from favourites store
await favouritesStore.delete(version.version);
await favouritesStore.save();
// Update the current installed versions list
const versions = get(installedVersionsStore);
const filteredVersions = versions.filter((v) => v.version !== version.version);
installedVersionsStore.set(filteredVersions);
console.log(`Successfully deleted version ${version.version}`);
} catch (error) {
console.error(`Failed to delete version ${version.version}:`, error);
throw error;
}
}
/**
* Get all installed Blender versions from the library directory.
* @returns {Promise<blenderVersion[]>} Array of installed versions
*/
export async function getInstalledVersions() {
const favouriteEntries = await favouritesStore.entries();
const favourites = Object.fromEntries(favouriteEntries);
try {
const settings = get(currentSettings);
const libraryDir = settings.libraryDir;
if (!libraryDir) {
installedVersionsStore.set([]);
return [];
}
const blenderLibraryPath = await join(libraryDir, BASE_LIBRARY_DIR, 'blender');
if (!exists(blenderLibraryPath)) {
installedVersionsStore.set([]);
return [];
}
// Read all version directories
const entries = await readDir(blenderLibraryPath);
const versions = [];
// Filter out non-directory entries
for (const entry of entries) {
if (!entry.isDirectory) {
continue;
}
const version = entry.name;
const versionPath = await join(blenderLibraryPath, version);
try {
// Find the Blender executable in this version directory
const executable = await findBlenderExecutable(versionPath);
if (executable) {
const currentPlatform = platform();
// Get templates for this version
let newVersion = {
version,
path: versionPath,
executable,
platform: currentPlatform,
favourite: favourites[version] || false,
templates: []
};
var templates = await getTemplates(newVersion);
newVersion.templates = templates;
//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);
}
versions.push(newVersion);
} else {
console.warn(`No executable found deleting folder ${version}`);
//await remove(versionPath, { recursive: true });
}
} catch (error) {
console.warn(`Error processing version ${version}:`, error);
// Skip this version if we can't process it
}
}
installedVersionsStore.set(versions);
await cleanupOrphanedDesktopFiles(versions);
const selectedVersion = get(selectedVersionStore);
if (selectedVersion) {
const refreshed = versions.find((v) => v.version === selectedVersion.version);
selectedVersionStore.set(refreshed || selectedVersion);
} else {
if (versions.length > 0) {
let favouriteVersion = versions.find((v) => v.favourite);
if (favouriteVersion) {
selectedVersionStore.set(favouriteVersion);
} else {
selectedVersionStore.set(versions[0]);
}
}
}
return versions;
} catch (error) {
console.error('Error getting installed versions:', error);
installedVersionsStore.set([]);
return [];
}
}
/**
*
* @param {blenderVersion} version
* @returns {Promise<string[]>} List of .blend files paths in the templates directory
*/
export async function getTemplates(version) {
try {
const templatesDir = await getTemplatesDir(version)
await ensureDirectory(templatesDir);
const entries = await readDir(templatesDir);
const templates = [];
const ignoredDirs = ['Storyboarding', '2D_Animation', 'Sculpting', 'VFX', 'Video_Editing'];
for (const entry of entries) {
if (entry.isDirectory && !ignoredDirs.includes(entry.name)) {
const startupFile = await join(templatesDir, entry.name, 'startup.blend');
await generateTemplateBanner(await join(templatesDir, entry.name));
if (await exists(startupFile)) {
templates.push(entry.name);
}
}
}
return templates;
} catch (error) {
console.error(error);
return [];
}
}
/**
* 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 registerVersion(version) {
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}`
};
}
// Launch the executable
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}`
};
}
}
/**
* Returns the templates directory for a given Blender version
* @param {blenderVersion} version
* @returns {Promise<string>}
*/
export async function getTemplatesDir(version) {
const versionNumber = version?.version;
try {
const templatesDir = await join(
version.path,
versionNumber.split('.').slice(0, 2).join('.'),
'scripts',
'startup',
'bl_app_templates_system'
);
await ensureDirectory(templatesDir);
console.log(templatesDir)
return templatesDir;
} catch (error) {
console.error(error);
return null;
}
}
/**
*
* @param {blenderVersion} version
* @param {string} template
* @returns {Promise<string>}
*/
export async function getTemplate(version, template) {
try {
// Launch the executable
const templatesDir = await join(await getTemplatesDir(version),
template
);
return templatesDir;
} catch (error) {
console.error(error);
return null;
}
}
/**
* 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`
};
}
const blenderConfigPath = await join(libraryDir, BASE_LIBRARY_DIR, 'config', version.version);
// Launch the executable
let bannerPath;
if (template) {
const templateDir = await getTemplate(version, template);
bannerPath = await join(templateDir, 'banner.png');
} else {
bannerPath = await join(version.path, 'banner.png');
}
const args = template ? ['--app-template', template] : [];
/*if (template) {
} else {
bannerPath = await join(version.path, 'banner.png');
} */
console.log('launching', version.executable, args);
invoke('launch_binary', {
path: version.executable,
args: args,
env: {
BLENDER_USER_RESOURCES: blenderConfigPath,
BLENDER_CUSTOM_SPLASH_BANNER: bannerPath
}
});
} catch (error) {
console.error('Error launching Blender:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `. failed: ${errorMessage}`
};
}
}
/**
*
* @param {string} versionPath
* @returns {Promise<string | null>} Path to the Blender executable, or null if not found
*/
async function findBlenderExecutable(versionPath) {
const settings = get(currentSettings);
const currentPlatform = settings.defaultPlatform;
const platformStr = String(currentPlatform);
// Map platform strings to executable names
const executableMap = {
windows: 'blender.exe',
linux: 'blender',
macos: 'blender'
// Add more platforms here as needed
};
// Default to 'blender' if platform not in map
const executableName = executableMap[platformStr] || 'blender';
const executablePath = await join(versionPath, executableName);
try {
const stats = await stat(executablePath);
if (stats.isFile) {
return executablePath;
}
} catch (error) {
// Executable not found at the expected path
console.warn(`Blender executable not found at ${executablePath}:`, error);
}
return null;
}