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} [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} 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} 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} */ 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} */ 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} 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; }