612 lines
17 KiB
JavaScript
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;
|
|
}
|