export const electron = require('@electron/remote'); export const {clipboard, shell, nativeImage, ipcRenderer, dialog, webUtils} = require('electron'); export const app = electron.app; export const fs = require('fs'); export const NodeBuffer = require('buffer'); export const zlib = require('zlib'); export const exec = require('child_process').exec; export const https = require('https'); export const PathModule = require('path'); export const currentwindow = electron.getCurrentWindow(); var dialog_win = null, latest_version = false; export const recent_projects = (function() { let array = []; var raw = localStorage.getItem('recent_projects') if (raw) { try { array = JSON.parse(raw).slice().reverse() } catch (err) {} array = array.filter(project => { return fs.existsSync(project.path); }) } return array })(); app.setAppUserModelId('blockbench') export function initializeDesktopApp() { //Setup $(document.body).on('click auxclick', 'a[href]', (event) => { event.preventDefault(); shell.openExternal(event.currentTarget.href); return true; }); if (Blockbench.startup_count <= 1 && electron.nativeTheme.inForcedColorsMode) { let theme = CustomTheme.themes.find(t => t.id == 'contrast'); CustomTheme.loadTheme(theme); } function makeUtilFolder(name) { let path = PathModule.join(app.getPath('userData'), name) if (!fs.existsSync(path)) fs.mkdirSync(path) } ['backups', 'thumbnails'].forEach(makeUtilFolder) createBackup(true) $('.web_only').remove() if (__dirname.includes('C:\\xampp\\htdocs\\blockbench')) { Blockbench.addFlag('dev') } settings.interface_scale.onChange(); if (Blockbench.platform == 'darwin') { //Placeholder $('#mac_window_menu').show() currentwindow.on('enter-full-screen', () => { $('#mac_window_menu').hide() }) currentwindow.on('leave-full-screen', () => { $('#mac_window_menu').show() }) } else { $('#windows_window_menu').show() } if (Blockbench.platform == 'linux') { // Clear GPU cache: https://github.com/JannisX11/blockbench/issues/1964 let gpu_cache_path = PathModule.join(app.getPath('userData'), 'GPUCache'); try { let cache_files = fs.readdirSync(gpu_cache_path); for (let file_name of cache_files) { fs.unlinkSync(PathModule.join(gpu_cache_path, file_name)); } console.log(`Cleared ${cache_files.length} GPU-cache files`); } catch (err) { console.error('Attempted and failed to clear GPU cache', err); } } } //Load Model export function loadOpenWithBlockbenchFile() { function load(path) { var extension = pathToExtension(path); if (extension == 'png') { Blockbench.read([path], {readtype: 'image'}, (files) => { loadImages(files); }) } else if (Codec.getAllExtensions().includes(extension)) { Blockbench.read([path], {}, (files) => { loadModelFile(files[0]) }) } } ipcRenderer.on('open-model', (event, path) => { load(path); }) ipcRenderer.on('load-tab', (event, model) => { let fake_file = { path: model.editor_state?.save_path || '' }; Codecs.project.load(model, fake_file); if (model.detached_uuid) { ipcRenderer.send('close-detached-project', model.detached_window_id, model.detached_uuid); } }) ipcRenderer.on('accept-detached-tab', (event, value) => { Interface.page_wrapper.classList.toggle('accept_detached_tab', value); }) ipcRenderer.on('close-detached-project', (event, uuid) => { let tab = ModelProject.all.find(project => project.uuid == uuid && project.detached); if (tab) tab.close(true); }) if (electron.process.argv.length >= 2) { let path = electron.process.argv.last(); load(path); } } (function() { console.log('Electron '+process.versions.electron+', Node '+process.versions.node) })() window.confirm = function(message, title) { let index = electron.dialog.showMessageBoxSync(currentwindow, { title: title || electron.app.name, detail: message, type: 'none', noLink: true, buttons: [tl('dialog.ok'), tl('dialog.cancel')] }); return index == 0; } window.alert = function(message, title) { electron.dialog.showMessageBoxSync(electron.getCurrentWindow(), { title: title || electron.app.name, detail: message }); } //Recent Projects export function updateRecentProjects() { recent_projects.splice(Math.clamp(settings.recent_projects.value, 0, 512)); let fav_count = 0; recent_projects.forEach((project, i) => { if (project.favorite) { recent_projects.splice(i, 1); recent_projects.splice(fav_count, 0, project); fav_count++; } }) //Set Local Storage localStorage.setItem('recent_projects', JSON.stringify(recent_projects.slice().reverse())); } export function addRecentProject(data) { var i = recent_projects.length-1; let former_entry; while (i >= 0) { var p = recent_projects[i] if (p.path === data.path) { recent_projects.splice(i, 1); former_entry = p; } i--; } if (data.name.length > 48) data.name = data.name.substr(0, 20) + '...' + data.name.substr(-20); let project = { name: data.name, path: data.path, icon: data.icon, favorite: former_entry ? former_entry.favorite : false, day: new Date().dayOfYear(), } recent_projects.splice(0, 0, project) ipcRenderer.send('add-recent-project', data.path); StartScreen.vue.updateThumbnails([data.path]); Settings.updateSettingsInProfiles(); updateRecentProjects() } export function updateRecentProjectData() { let project = Project.getProjectMemory(); if (!project) return; if (project.name.length > 48) project.name = project.name.substr(0, 20) + '...' + project.name.substr(-20); project.textures = Texture.all.filter(t => t.path).map(t => t.path); if (Format.animation_files) { project.animation_files = []; Animation.all.forEach(anim => { if (anim.path) project.animation_files.safePush(anim.path); }) } Blockbench.dispatchEvent('update_recent_project_data', {data: project}); updateRecentProjects() } export async function updateRecentProjectThumbnail() { let project = Project && Project.getProjectMemory(); if (!project) return; let thumbnail; const resolution = [270, 150]; if (Format.image_editor && Texture.all.length) { await new Promise((resolve, reject) => { let tex = Texture.getDefault(); let frame = new CanvasFrame(resolution[0], resolution[1]); frame.ctx.imageSmoothingEnabled = false; let {width, height} = tex; if (width > resolution[0]) {height /= width / resolution[0]; width = resolution[0];} if (height > resolution[1]) {width /= height / resolution[1]; height = resolution[1];} if (width < resolution[0] && height < resolution[1]) { let factor = Math.min(resolution[0] / width, resolution[1] / height); factor *= 0.92; height *= factor; width *= factor; } frame.ctx.drawImage(tex.img, (resolution[0] - width)/2, (resolution[1] - height)/2, width, height) let url = frame.canvas.toDataURL(); let hash = project.path.hashCode().toString().replace(/^-/, '0'); let path = PathModule.join(app.getPath('userData'), 'thumbnails', `${hash}.png`) thumbnail = url; Blockbench.writeFile(path, { savetype: 'image', content: url }, resolve) }) } else { if (Outliner.elements.length == 0) return; MediaPreview.resize(resolution[0], resolution[1]) MediaPreview.loadAnglePreset(DefaultCameraPresets[0]) MediaPreview.setFOV(30); let center = getSelectionCenter(true); MediaPreview.controls.target.fromArray(center); MediaPreview.controls.target.add(scene.position); let box = Canvas.getModelSize(); let size = Math.max(box[0], box[1]*2) MediaPreview.camera.position.multiplyScalar(size/50) await new Promise((resolve, reject) => { MediaPreview.screenshot({crop: false}, url => { let hash = project.path.hashCode().toString().replace(/^-/, '0'); let path = PathModule.join(app.getPath('userData'), 'thumbnails', `${hash}.png`) thumbnail = url; Blockbench.writeFile(path, { savetype: 'image', content: url }, resolve) let store_path = project.path; project.path = ''; project.path = store_path; }) }) } Blockbench.dispatchEvent('update_recent_project_thumbnail', {data: project, thumbnail}); StartScreen.vue.updateThumbnails([project.path]); // Clean old files if (Math.random() < 0.2) { let folder_path = PathModule.join(app.getPath('userData'), 'thumbnails') let existing_names = []; recent_projects.forEach(project => { let hash = project.path.hashCode().toString().replace(/^-/, '0'); existing_names.safePush(hash) }) fs.readdir(folder_path, (err, files) => { if (!err) { files.forEach((name, i) => { if (existing_names.includes(name.replace(/\..+$/, '')) == false) { try { fs.unlinkSync(folder_path +osfs+ name) } catch (err) {} } }) } }) } } export function loadDataFromModelMemory() { let project = Project && Project.getProjectMemory(); if (!project) return; if (project.textures) { Blockbench.read(project.textures, {}, files => { files.forEach(f => { if (!Texture.all.find(t => t.path == f.path)) { new Texture({name: f.name}).fromFile(f).add(false).fillParticle(); } }) }) } if (project.animation_files && Format.animation_files) { Project.memory_animation_files_to_load = project.animation_files; } Blockbench.dispatchEvent('load_from_recent_project_data', {data: project}); } export function showItemInFolder(path) { ipcRenderer.send('show-item-in-folder', path); } //Window Controls export function updateWindowState(e, type) { let maximized = currentwindow.isMaximized(); $('#header_free_bar').toggleClass('resize_space', !maximized); document.body.classList.toggle('maximized', maximized); } currentwindow.on('maximize', e => updateWindowState(e, 'maximize')); currentwindow.on('unmaximize', e => updateWindowState(e, 'unmaximize')); currentwindow.on('enter-full-screen', e => updateWindowState(e, 'screen')); currentwindow.on('leave-full-screen', e => updateWindowState(e, 'screen')); currentwindow.on('ready-to-show', e => updateWindowState(e, 'load')); //Image Editor export function changeImageEditor(texture, not_found) { let app_file_extension = { 'win32': ['exe'], 'linux': [], 'darwin': ['app'], }; new Dialog({ title: tl('message.image_editor.title'), id: 'image_editor', form: { not_found_text: {type: 'info', text: 'message.image_editor.not_found', condition: not_found == true}, editor: {type: 'select', full_width: true, options: { ps: Blockbench.platform == 'win32' ? 'Photoshop' : undefined, gimp: 'GIMP', pdn: Blockbench.platform == 'win32' ? 'Paint.NET' : undefined, other: 'message.image_editor.file' }}, file: { label: 'message.image_editor.file', nocolon: true, type: 'file', file_type: 'Program', extensions: app_file_extension[Blockbench.platform], readtype: 'none', description: 'message.image_editor.exe', condition: result => result.editor == 'other' } }, onConfirm(result) { let id = result.editor; let path; if (id == 'other') { path = result.file; } else if (Blockbench.platform == 'darwin') { switch (id) { case 'ps': path = '/Applications/Adobe Photoshop 2024/Adobe Photoshop 2024.app'; break; case 'gimp':path = '/Applications/Gimp-2.10.app'; break; } } else if (Blockbench.platform == 'linux') { switch (id) { case 'ps': path = '/usr/share/applications//photoshop.desktop'; break; case 'gimp':path = '/usr/share/applications//gimp.desktop'; break; } } else { switch (id) { case 'ps': path = 'C:\\Program Files\\Adobe\\Adobe Photoshop 2024\\Photoshop.exe'; break; case 'gimp':path = 'C:\\Program Files\\GIMP 2\\bin\\gimp-2.10.exe'; break; case 'pdn': path = 'C:\\Program Files\\paint.net\\PaintDotNet.exe'; break; } } if (path && fs.existsSync(path)) { settings.image_editor.value = path if (texture) { texture.openEditor() } } else { changeImageEditor(texture, true); } }, }).show() } //Default Pack export function openDefaultTexturePath() { let detail = tl('message.default_textures.detail'); if (settings.default_path.value) { detail += '\n\n' + tl('message.default_textures.current') + ': ' + settings.default_path.value; } let buttons = ( settings.default_path.value ? [tl('dialog.continue'), tl('generic.remove'), tl('dialog.cancel')] : [tl('dialog.continue'), tl('dialog.cancel')] ) var answer = electron.dialog.showMessageBoxSync(currentwindow, { type: 'info', buttons, noLink: true, title: tl('message.default_textures.title'), message: tl('message.default_textures.message'), detail }) if (answer === buttons.length-1) { return; } else if (answer === 0) { let path = Blockbench.pickDirectory({ title: tl('message.default_textures.select'), resource_id: 'texture', }); if (path) { settings.default_path.value = path; Settings.saveLocalStorages(); } } else { settings.default_path.value = false; Settings.saveLocalStorages(); } } export function findExistingFile(paths) { for (var path of paths) { if (fs.existsSync(path)) { return path; } } } //Backup export function createBackup(init) { setTimeout(createBackup, limitNumber(parseFloat(settings.backup_interval.value), 1, 10e8)*60000) let duration = parseInt(settings.backup_retain.value)+1 let folder_path = app.getPath('userData')+osfs+'backups' let d = new Date() let days = d.getDate() + (d.getMonth()+1)*30.44 + (d.getYear()-100)*365.25 if (init) { //Clear old backups fs.readdir(folder_path, (err, files) => { if (!err) { files.forEach((name, i) => { let date = name.split('_')[1] if (date) { let nums = date.split('.') nums.forEach((n, ni) => { nums[ni] = parseInt(n) }) let b_days = nums[0] + nums[1]*30.44 + nums[2]*365.25 if (!isNaN(b_days) && days - b_days > duration) { try { fs.unlinkSync(folder_path +osfs+ name) } catch (err) {console.log(err)} } } }) } }) } if (init || !Project || (elements.length === 0 && Texture.all.length === 0)) return; let model = Codecs.project.compile({compressed: true, backup: true}); let short_name = Project.name.replace(/[.]/g, '_').replace(/[^a-zA-Z0-9._-]/g, '').substring(0, 16); if (short_name) short_name = '_' + short_name; let file_name = 'backup_'+d.getDate()+'.'+(d.getMonth()+1)+'.'+(d.getYear()-100)+'_'+d.getHours()+'.'+d.getMinutes() + short_name; let file_path = folder_path+osfs+file_name+'.bbmodel'; fs.writeFile(file_path, model, function (err) { if (err) { console.log('Error creating backup: '+err) } }) } BARS.defineActions(() => { let selected_id; // Remember selected one after re-opening new Action('view_backups', { icon: 'fa-archive', category: 'file', condition: () => isApp, click(e) { let backup_directory = app.getPath('userData')+osfs+'backups'; let files = fs.readdirSync(backup_directory); let entries = files.map((file, i) => { let path = PathModule.join(backup_directory, file); let stats = fs.statSync(path); let size = `${separateThousands(Math.round(stats.size / 1024))} KB`; let entry = { id: file, path, name: file.replace(/backup_\d+\.\d+\.\d+_\d+\.\d+_?/, '').replace(/\.bbmodel$/, '').replace(/_/g, ' ') || 'no name', date: stats.mtime.toLocaleDateString(), time: stats.mtime.toLocaleTimeString().replace(/:\d+ /, ' '), date_long: stats.mtime.toString(), timestamp: stats.mtime.getTime(), size, } return entry; }) entries.sort((a, b) => b.timestamp - a.timestamp); let selected; const dialog = new Dialog({ id: 'view_backups', title: 'action.view_backups', width: 720, buttons: ['dialog.confirm', 'dialog.view_backups.open_folder', 'dialog.cancel'], component: { data() {return { backups: entries, page: 0, per_page: 80, search_term: '', selected: (selected_id ? entries.find(e => e.id == selected_id) : null) }}, methods: { select(backup) { selected = this.selected = backup; selected_id = backup.id; }, open() { dialog.confirm(); }, setPage(number) { this.page = number; this.$refs.backups_list.scrollTop = 0; } }, computed: { filtered_backups() { let term = this.search_term.toLowerCase(); return this.backups.filter(backup => { return backup.name.includes(term); }) }, viewed_backups() { return this.filtered_backups.slice(this.page * this.per_page, (this.page+1) * this.per_page); }, pages() { let pages = []; let length = this.filtered_backups.length; for (let i = 0; i * this.per_page < length; i++) { pages.push(i); } return pages; } }, template: `