WIP: Template Upload
- Drag and drop - Launche template TODO: Banner generation TODO: File dialog
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<script>
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { onMount } from 'svelte';
|
||||
import { addTemplateFile } from '$lib/file_utils';
|
||||
let dragging = $state(false);
|
||||
let hasBlend = $state(false);
|
||||
let dropzoneEl = $state(null);
|
||||
|
||||
let { version } = $props();
|
||||
|
||||
onMount(() => {
|
||||
const win = getCurrentWindow();
|
||||
let unlisten = () => {};
|
||||
|
||||
win
|
||||
.onDragDropEvent((event) => {
|
||||
if (!dropzoneEl) return;
|
||||
|
||||
const payload = event.payload;
|
||||
|
||||
if (payload.type === 'enter') {
|
||||
hasBlend = payload.paths.some((p) => p.endsWith('.blend'));
|
||||
if (!hasBlend) return;
|
||||
}
|
||||
|
||||
if (payload.type === 'leave') {
|
||||
dragging = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 'enter', 'over', and 'drop' all have position data
|
||||
const rect = dropzoneEl.getBoundingClientRect();
|
||||
const scale = window.devicePixelRatio || 1;
|
||||
const cssX = payload.position.x / scale;
|
||||
const cssY = payload.position.y / scale;
|
||||
|
||||
const isInside =
|
||||
cssX >= rect.left && cssX <= rect.right && cssY >= rect.top && cssY <= rect.bottom;
|
||||
|
||||
if (payload.type === 'over') {
|
||||
dragging = hasBlend && isInside;
|
||||
} else if (payload.type === 'enter') {
|
||||
dragging = isInside;
|
||||
} else if (payload.type === 'drop') {
|
||||
dragging = false;
|
||||
if (hasBlend && isInside) {
|
||||
const blendFiles = payload.paths.filter((p) => p.endsWith('.blend'));
|
||||
if (blendFiles.length > 0) {
|
||||
console.log('Dropped Blender files:', blendFiles);
|
||||
blendFiles.forEach((file) => {
|
||||
if (version) {
|
||||
addTemplateFile(version, file);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((fn) => {
|
||||
unlisten = fn;
|
||||
});
|
||||
|
||||
return () => unlisten();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="dropzone" class:dragging bind:this={dropzoneEl}>Drop Blender Files</div>
|
||||
|
||||
<style>
|
||||
.dropzone {
|
||||
height: 3rem;
|
||||
|
||||
width: 100%;
|
||||
border-color: var(--light-accent);
|
||||
border-width: 3px;
|
||||
border-style: solid;
|
||||
border-radius: 5px;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--light-accent);
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
color 0.15s;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.dropzone.dragging {
|
||||
height: 5rem;
|
||||
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,11 @@
|
||||
<script>
|
||||
import VersionCard from './VersionCard.svelte';
|
||||
import TemplateCard from './TemplateCard.svelte';
|
||||
|
||||
import { send, receive } from '$lib/transition.js';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { selectedVersionStore } from '$lib/library.js';
|
||||
import { Splitpanes, Pane } from 'svelte-splitpanes';
|
||||
|
||||
let selectedVersion = $state(null);
|
||||
|
||||
@@ -13,45 +16,69 @@
|
||||
let { installedVersions } = $props();
|
||||
import '@webtui/css/base.css';
|
||||
import '@webtui/css/utils/box.css';
|
||||
import Dropzone from './Dropzone.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let favourites = $derived(installedVersions?.filter((v) => v.favourite) || []);
|
||||
let installed = $derived(installedVersions?.filter((v) => !v.favourite) || []);
|
||||
</script>
|
||||
|
||||
<h2>Favourites</h2>
|
||||
<Splitpanes id="split">
|
||||
<Pane>
|
||||
<h2>Favourites</h2>
|
||||
|
||||
<div id="container">
|
||||
<div id="library">
|
||||
{#if favourites.length}
|
||||
{#each favourites as version (version.version)}
|
||||
<div
|
||||
animate:flip={{ duration: 200 }}
|
||||
in:receive={{ key: version.version }}
|
||||
out:send={{ key: version.version }}
|
||||
>
|
||||
<VersionCard {version} {selectedVersion} />
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="minor">No favourites</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Installed</h2>
|
||||
<div id="container">
|
||||
<div id="library">
|
||||
{#each installed as version (version.version)}
|
||||
<div
|
||||
animate:flip={{ duration: 200 }}
|
||||
in:receive={{ key: version.version }}
|
||||
out:send={{ key: version.version }}
|
||||
>
|
||||
<VersionCard {version} {selectedVersion} />
|
||||
<div id="container">
|
||||
<div id="library">
|
||||
{#if favourites.length}
|
||||
{#each favourites as version (version.version)}
|
||||
<div
|
||||
animate:flip={{ duration: 200 }}
|
||||
in:receive={{ key: version.version }}
|
||||
out:send={{ key: version.version }}
|
||||
>
|
||||
<VersionCard {version} {selectedVersion} />
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="minor">No favourites</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Installed</h2>
|
||||
<div id="container">
|
||||
<div id="library">
|
||||
{#each installed as version (version.version)}
|
||||
<div
|
||||
animate:flip={{ duration: 200 }}
|
||||
in:receive={{ key: version.version }}
|
||||
out:send={{ key: version.version }}
|
||||
>
|
||||
<VersionCard {version} {selectedVersion} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane>
|
||||
<div id="templates">
|
||||
{#if selectedVersion}
|
||||
<h2>Blender {selectedVersion.version}</h2>
|
||||
<TemplateCard template={null} version={selectedVersion} />
|
||||
{#if selectedVersion?.templates.length > 0}
|
||||
{#each selectedVersion?.templates as template (template)}
|
||||
<div>
|
||||
<TemplateCard {template} version={selectedVersion} />
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="minor">No templates</p>
|
||||
{/if}
|
||||
<Dropzone version={selectedVersion}></Dropzone>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
<style>
|
||||
.minor {
|
||||
@@ -65,12 +92,27 @@
|
||||
max-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#templates {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
padding: 0 0 0 1rem;
|
||||
}
|
||||
#library {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
min-height: 140px;
|
||||
padding-bottom: 1rem;
|
||||
overflow: visible;
|
||||
}
|
||||
:global(.splitpanes__pane) {
|
||||
background-color: unset !important;
|
||||
}
|
||||
:global(.splitpanes__splitter) {
|
||||
background-color: var(--light) !important;
|
||||
width: 0.5rem !important;
|
||||
border: none !important;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -88,6 +88,9 @@
|
||||
<style>
|
||||
.btncont {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.menu-container {
|
||||
height: 100%;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
let { onclick, children } = $props();
|
||||
</script>
|
||||
|
||||
<button type="button" class="menu-item" {onclick}>{@render children?.()}</button>
|
||||
|
||||
<style>
|
||||
.menu-item {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: var(--font);
|
||||
font-size: 1rem;
|
||||
color: var(--black);
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
.menu-item:hover {
|
||||
background-color: var(--light-accent);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script>
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Menu from '$lib/components/Menu.svelte';
|
||||
import MenuItem from '$lib/components/MenuItem.svelte';
|
||||
import { show } from '$lib/components/Dialog.svelte';
|
||||
import { deleteTemplate } from '$lib/library';
|
||||
|
||||
import { launchBlenderVersion } from '$lib/library';
|
||||
let { template, version } = $props();
|
||||
let templateName = $derived(
|
||||
template
|
||||
.split('/')
|
||||
.pop()
|
||||
.replace('.blend', '')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles the deletion of a template.
|
||||
*
|
||||
* @param {string} template - The template file path to delete.
|
||||
*/
|
||||
async function handleDelete(template) {
|
||||
const prompt = await show({
|
||||
title: 'Delete',
|
||||
message: `This will remove Blender ${version.version} and all associated files from your system.`,
|
||||
buttons: ['Cancel', 'Continue'],
|
||||
type: 'warning'
|
||||
});
|
||||
if (prompt == 'Continue') {
|
||||
deleteTemplate(template);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if template == null}
|
||||
<div id="base-card">
|
||||
<Button fill={true} color="accent" onclick={() => launchBlenderVersion(version)}
|
||||
>Launch Base</Button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div id="template-card">
|
||||
<div class="leftmenu">
|
||||
<Menu>
|
||||
<MenuItem onclick={() => handleDelete(template)}>Delete</MenuItem>
|
||||
</Menu>
|
||||
{templateName}
|
||||
</div>
|
||||
<Button style="small" onclick={() => launchBlenderVersion(version, template)}>→</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#base-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#template-card {
|
||||
height: 3rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
background-color: var(--light);
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
transition: width 0.3s ease;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.leftmenu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,8 @@
|
||||
} from '$lib/library';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Menu from '$lib/components/Menu.svelte';
|
||||
import MenuItem from '$lib/components/MenuItem.svelte';
|
||||
|
||||
import { show } from '$lib/components/Dialog.svelte';
|
||||
|
||||
let { version, selectedVersion } = $props();
|
||||
@@ -42,8 +44,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popup bind:this={popup} content="Version {version.version} is now the default on your system"
|
||||
></Popup>
|
||||
<Popup bind:this={popup} content="Version {version.version} is now the default on your system" />
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -51,72 +52,65 @@
|
||||
onclick={async () => {
|
||||
await selectVersion(version);
|
||||
}}
|
||||
ondblclick={async () => {
|
||||
console.log('double click', version);
|
||||
await launchBlenderVersion(version);
|
||||
}}
|
||||
id="card"
|
||||
class:selected
|
||||
>
|
||||
<div class="row">
|
||||
{version.version}
|
||||
<Button style="iconbutton" onclick={() => toggleFavourite(version)}
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
class="fav-btn {version.favourite ? 'fav' : ''}"
|
||||
viewBox="0 0 16 16"
|
||||
<Button style="iconbutton" onclick={() => toggleFavourite(version)}
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
class="fav-btn {version.favourite ? 'fav' : ''}"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M2 2v13.5a.5.5 0 0 0 .74.439L8 13.069l5.26 2.87A.5.5 0 0 0 14 15.5V2a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
{version.version}
|
||||
<Menu>
|
||||
{#snippet children({ close })}
|
||||
<MenuItem
|
||||
onclick={async () => {
|
||||
await handleDelete();
|
||||
close();
|
||||
}}>Delete</MenuItem
|
||||
>
|
||||
<path
|
||||
d="M2 2v13.5a.5.5 0 0 0 .74.439L8 13.069l5.26 2.87A.5.5 0 0 0 14 15.5V2a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Button onclick={() => launchBlenderVersion(version)}>Launch</Button>
|
||||
<Menu>
|
||||
{#snippet children({ close })}
|
||||
<button
|
||||
type="button"
|
||||
class="menu-item"
|
||||
onclick={async () => {
|
||||
await handleDelete();
|
||||
close();
|
||||
}}>Delete</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-item"
|
||||
onclick={async () => {
|
||||
await handleDefault();
|
||||
close();
|
||||
}}>Default</button
|
||||
>
|
||||
{/snippet}
|
||||
</Menu>
|
||||
</div>
|
||||
<MenuItem
|
||||
onclick={async () => {
|
||||
await handleDefault();
|
||||
close();
|
||||
}}>Default</MenuItem
|
||||
>
|
||||
{/snippet}
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
aspect-ratio: 1 / 0.7;
|
||||
width: 100%;
|
||||
min-width: 170px;
|
||||
max-width: 200px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--light);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#card.selected {
|
||||
background-color: var(--light-accent);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#card:hover {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
@@ -127,18 +121,4 @@
|
||||
.fav-btn.fav {
|
||||
fill: var(--accent);
|
||||
}
|
||||
.menu-item {
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: var(--font);
|
||||
font-size: 1rem;
|
||||
color: var(--black);
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
.menu-item:hover {
|
||||
background-color: var(--light-accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
+17
-30
@@ -11,6 +11,7 @@ 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';
|
||||
import { getInstalledVersions } from './library';
|
||||
figlet.parseFont('Big', shadow);
|
||||
|
||||
/**
|
||||
@@ -58,10 +59,10 @@ export async function createIcon(iconText) {
|
||||
|
||||
/**
|
||||
* Create a custom banner with version number next to the baseIcon image
|
||||
* @param {string} versionText - Version text to display
|
||||
* @param {string} text - Version text to display
|
||||
* @returns {Promise<Blob>} Promise resolving to a PNG blob of the banner
|
||||
*/
|
||||
export async function createBanner(versionText) {
|
||||
export async function createBanner(text) {
|
||||
// Create canvas element for banner
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -82,11 +83,6 @@ export async function createBanner(versionText) {
|
||||
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';
|
||||
@@ -102,7 +98,9 @@ export async function createBanner(versionText) {
|
||||
const textX = 20;
|
||||
const textY = canvas.height / 2;
|
||||
|
||||
const bannerText = await figlet.text(versionText + ' - Base', { font: 'Big' });
|
||||
const bannerText = await figlet.text(text, {
|
||||
font: 'Big'
|
||||
});
|
||||
const lines = bannerText.split('\n');
|
||||
const lineHeight = fontSize * 1.2;
|
||||
const startY = textY - (lines.length * lineHeight) / 2 + lineHeight / 2;
|
||||
@@ -121,6 +119,17 @@ export async function createBanner(versionText) {
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
|
||||
export async function addTemplateFile(version, templateFilePath) {
|
||||
const newFileName = await pathBasename(templateFilePath);
|
||||
const templatesPath = await join(version.path, 'templates', newFileName);
|
||||
await copyFile(templateFilePath, templatesPath);
|
||||
|
||||
await getInstalledVersions();
|
||||
|
||||
console.log('adding ', templateFilePath, ' to ', version.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create library directory structure if it doesn't exist
|
||||
* @param {string} libraryDir - Library directory path
|
||||
@@ -131,10 +140,6 @@ export async function ensureLibraryStructure(libraryDir) {
|
||||
const baseDir = await join(libraryDir, BASE_LIBRARY_DIR);
|
||||
const blenderDir = await join(baseDir, 'blender');
|
||||
await mkdir(blenderDir, { recursive: true });
|
||||
|
||||
const templatesDir = await join(baseDir, 'templates');
|
||||
await mkdir(templatesDir, { recursive: true });
|
||||
|
||||
const configDir = await join(baseDir, 'config');
|
||||
await mkdir(configDir, { recursive: true });
|
||||
|
||||
@@ -201,24 +206,6 @@ export async function ensureDirectory(path) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the basename of a path (file or directory name)
|
||||
* @param {string} path - Full path
|
||||
* @returns {Promise<string>} Basename
|
||||
*/
|
||||
export async function basename(path) {
|
||||
return pathBasename(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory name of a path
|
||||
* @param {string} path - Full path
|
||||
* @returns {Promise<string>} Directory name
|
||||
*/
|
||||
export async function dirname(path) {
|
||||
return pathDirname(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file or directory recursively
|
||||
* @param {string} source - Source path
|
||||
|
||||
+162
-9
@@ -1,5 +1,5 @@
|
||||
import { currentSettings } from '$lib/settings.js';
|
||||
import { createIcon, createBanner } from './file_utils';
|
||||
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';
|
||||
@@ -7,7 +7,6 @@ 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 { confirm } from '@tauri-apps/plugin-dialog';
|
||||
import { updateDownloadProgress } from './download';
|
||||
|
||||
/** INSTALLED BLENDER VERSION TYPE
|
||||
@@ -17,6 +16,7 @@ import { updateDownloadProgress } from './download';
|
||||
* @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([]);
|
||||
@@ -27,8 +27,23 @@ 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 = `${version.path}/banner.png`;
|
||||
const bannerPath = await join(version.path, 'banner.png');
|
||||
if (await exists(bannerPath)) {
|
||||
return;
|
||||
}
|
||||
@@ -188,6 +203,18 @@ MimeType=application/x-blender;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -218,6 +245,19 @@ export async function toggleFavourite(version) {
|
||||
await favouritesStore.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a template from the library.
|
||||
* @param {string} templatePath The path of the template to remove.
|
||||
*/
|
||||
export async function deleteTemplate(templatePath) {
|
||||
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.
|
||||
@@ -286,12 +326,17 @@ export async function getInstalledVersions() {
|
||||
const executable = await findBlenderExecutable(versionPath);
|
||||
if (executable) {
|
||||
const currentPlatform = platform();
|
||||
|
||||
// Get templates for this version
|
||||
var templates = await getTemplates(version);
|
||||
|
||||
let newVersion = {
|
||||
version,
|
||||
path: versionPath,
|
||||
executable,
|
||||
platform: currentPlatform,
|
||||
favourite: favourites[version] || false
|
||||
favourite: favourites[version] || false,
|
||||
templates: templates || []
|
||||
};
|
||||
|
||||
//check if version has an icon, if not generate one
|
||||
@@ -314,7 +359,21 @@ export async function getInstalledVersions() {
|
||||
}
|
||||
|
||||
installedVersionsStore.set(versions);
|
||||
cleanupOrphanedDesktopFiles(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) {
|
||||
@@ -324,6 +383,51 @@ export async function getInstalledVersions() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} version
|
||||
* @returns {Promise<string[]>} List of .blend files paths in the templates directory
|
||||
*/
|
||||
export async function getTemplates(version) {
|
||||
try {
|
||||
const settings = get(currentSettings);
|
||||
const libraryDir = settings.libraryDir;
|
||||
|
||||
if (!libraryDir) {
|
||||
console.error('No library directory set');
|
||||
return [];
|
||||
}
|
||||
const blenderConfigPath = await join(libraryDir, BASE_LIBRARY_DIR, 'blender', version);
|
||||
|
||||
const templatesDir = await join(
|
||||
blenderConfigPath,
|
||||
version.split('.').slice(0, 2).join('.'),
|
||||
'scripts',
|
||||
'startup',
|
||||
'bl_app_templates_system'
|
||||
);
|
||||
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
|
||||
@@ -360,6 +464,34 @@ export async function registerVersion(version) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} version
|
||||
* @param {string} template
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getTemplateDir(version, template) {
|
||||
try {
|
||||
const settings = get(currentSettings);
|
||||
const libraryDir = settings.libraryDir;
|
||||
const blenderConfigPath = await join(libraryDir, BASE_LIBRARY_DIR, 'config', version);
|
||||
|
||||
// Launch the executable
|
||||
const templatesDir = await join(
|
||||
blenderConfigPath,
|
||||
version.split('.').slice(0, 2).join('.'),
|
||||
'scripts',
|
||||
'startup',
|
||||
'bl_app_templates_system',
|
||||
template
|
||||
);
|
||||
return templatesDir;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a specific Blender version.
|
||||
* @param {blenderVersion} version - The version object to launch
|
||||
@@ -391,18 +523,34 @@ export async function launchBlenderVersion(version, template = null) {
|
||||
if (!libraryDir) {
|
||||
return {
|
||||
success: false,
|
||||
message: `couldn't find library: ${version.executable}`
|
||||
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 getTemplateDir(version.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: args,
|
||||
env: {
|
||||
BLENDER_USER_RESOURCES: blenderConfigPath,
|
||||
BLENDER_CUSTOM_SPLASH_BANNER: await join(version.path, 'banner.png')
|
||||
BLENDER_CUSTOM_SPLASH_BANNER: bannerPath
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -410,11 +558,16 @@ export async function launchBlenderVersion(version, template = null) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Launch failed: ${errorMessage}`
|
||||
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;
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
#tabcontent {
|
||||
background-color: var(--light);
|
||||
box-sizing: border-box;
|
||||
height: calc(100vh - 3rem);
|
||||
height: calc(100vh - 5rem);
|
||||
padding: 1rem;
|
||||
max-height: 100%;
|
||||
}
|
||||
@@ -215,8 +215,8 @@
|
||||
}
|
||||
.version {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
bottom: 2.8rem;
|
||||
right: 3rem;
|
||||
z-index: 2;
|
||||
text-align: right;
|
||||
color: var(--light-accent);
|
||||
|
||||
Reference in New Issue
Block a user