const settings = {}; class Setting { constructor(id, data) { this.id = id; settings[id] = this; this.type = 'toggle'; if (data.type) this.type = data.type; if (Settings.stored[id]) { this.value = Settings.stored[id].value; } else if (data.value != undefined) { this.value = data.value } else { switch (this.type) { case 'toggle': this.value = true; break; case 'number': this.value = 0; break; case 'text': this.value = ''; break; case 'password': this.value = ''; break; case 'select': this.value; break; case 'click': this.value = false; break; } } this.condition = data.condition; this.category = data.category || 'general'; this.name = data.name || tl(`settings.${id}`); this.description = data.description || tl(`settings.${id}.desc`); this.launch_setting = data.launch_setting || false; if (this.type == 'number') { this.min = data.min; this.max = data.max; this.step = data.step; } if (this.type == 'click') { this.icon = data.icon; this.click = data.click; } if (this.type == 'select') { this.options = data.options; } if (this.type == 'password') { this.hidden = true; } if (typeof data.onChange == 'function') { this.onChange = data.onChange } //add to structure var category = Settings.structure[this.category]; if (category) { category.items[id] = this; let before = category.open; category.open = false; Vue.nextTick(() => { category.open = before; }) } if (!this.icon) { if (this.type == 'toggle') this.icon = this.value ? 'check_box' : 'check_box_outline_blank'; if (this.type == 'number') this.icon = 'tag'; if (this.type == 'password') this.icon = 'password'; if (this.type == 'text') this.icon = 'format_color_text'; if (this.type == 'select') this.icon = 'list'; if (!this.icon) this.icon = 'settings'; } this.keybind_label = tl('data.setting'); } delete() { if (settings[this.id]) { delete settings[this.id]; } if (Settings.structure[this.category] && Settings.structure[this.category].items[this.id]) { delete Settings.structure[this.category].items[this.id]; } } set(value) { if (value === undefined || value === null) return; let old_value = this.value; if (this.type == 'number' && typeof value == 'number') { if (this.snap) { value = Math.round(value / this.snap) * this.snap; } this.value = Math.clamp(value, this.min, this.max) } else if (this.type == 'toggle') { this.value = !!value; } else if (this.type == 'click') { this.value = value; } else if (typeof value == 'string') { this.value = value; } if (typeof this.onChange == 'function' && this.value !== old_value) { this.onChange(this.value); } Settings.saveLocalStorages(); } trigger(e) { let {type} = this; let setting = this; if (type == 'toggle') { this.set(!this.value); Settings.save(); } else if (type == 'click') { this.click(e) } else if (type == 'select') { let list = []; for (let key in this.options) { list.push({ id: key, name: this.options[key], icon: this.value == key ? 'radio_button_checked' : 'radio_button_unchecked', click: () => { this.set(key); Settings.save(); } }) } new Menu(list).open(e.target); } else if (type == 'click') { this.click(e) } else { new Dialog({ id: 'setting_' + this.id, title: tl('data.setting'), form: { input: { value: this.value, label: this.name, description: this.description, type: this.type }, description: this.description ? { type: 'info', text: this.description } : undefined }, onConfirm({input}) { setting.set(input); Settings.save(); this.hide().delete(); }, onCancel() { this.hide().delete(); } }).show(); } } } const Settings = { structure: {}, stored: {}, setup() { if (localStorage.getItem('settings') != null) { Settings.stored = JSON.parse(localStorage.getItem('settings')); } //General new Setting('language', {value: 'en', type: 'select', options: Language.options}); new Setting('username', {value: '', type: 'text'}); new Setting('streamer_mode', {value: false, onChange() { StartScreen.vue._data.redact_names = settings.streamer_mode.value; Interface.status_bar.vue.streamer_mode = settings.streamer_mode.value; updateStreamerModeNotification(); }}); //Interface new Setting('interface_scale', {category: 'interface', value: 100, min: 40, max: 200, type: 'number', condition: isApp, onChange() { var factor = Math.clamp(settings.interface_scale.value, 40, 200) / 100; currentwindow.webContents.setZoomFactor(factor) resizeWindow() }}); new Setting('hide_tab_bar', {category: 'interface', value: Blockbench.isMobile, onChange() { updateTabBarVisibility(); }}); new Setting('mobile_panel_side', {category: 'interface', value: 'right', condition: Blockbench.isMobile, type: 'select', options: { 'right': tl('generic.right'), 'left': tl('generic.left'), }, onChange() { document.body.classList.toggle('mobile_sidebar_left', settings.mobile_panel_side.value == 'left'); }}); new Setting('status_bar_modifier_keys', {category: 'interface', value: true, condition: !Blockbench.isTouch, onChange(value) { Interface.status_bar.vue.show_modifier_keys = value; }}); new Setting('origin_size', {category: 'interface', value: 10, type: 'number'}); new Setting('control_size', {category: 'interface', value: 10, type: 'number'}); new Setting('motion_trails', {category: 'interface', value: true, onChange() { if (Animator.open) { scene[this.value ? 'add' : 'remove'](Animator.motion_trail); } }}); new Setting('seethrough_outline', {category: 'interface', value: false}); new Setting('outliner_colors', {category: 'interface', value: false}); new Setting('preview_checkerboard', {category: 'interface', value: true, onChange() { $('#center').toggleClass('checkerboard', settings.preview_checkerboard.value); }}); new Setting('uv_checkerboard', {category: 'interface', value: true, onChange(val) { UVEditor.vue.checkerboard = val; }}); new Setting('timecode_frame_number',{category: 'interface', value: false, onChange() { Timeline.vue.updateTimecodes(); }}); //Preview new Setting('brightness', {category: 'preview', value: 50, type: 'number'}); new Setting('shading', {category: 'preview', value: true, onChange() { Canvas.updateShading() }}); new Setting('antialiasing', {category: 'preview', value: true}); new Setting('fov', {category: 'preview', value: 45, type: 'number', onChange(val) { Preview.all.forEach(preview => preview.setFOV(val)); }}); new Setting('camera_near_plane',{category: 'preview', value: 1, type: 'number', onChange(val) { Preview.all.forEach(preview => { preview.camPers.near = val; preview.camPers.updateProjectionMatrix(); }); }}); new Setting('render_sides', {category: 'preview', value: 'auto', type: 'select', options: { 'auto': tl('settings.render_sides.auto'), 'front': tl('settings.render_sides.front'), 'double': tl('settings.render_sides.double'), }, onChange() { Canvas.updateRenderSides(); }}); new Setting('background_rendering', {category: 'preview', value: true}); new Setting('texture_fps', {category: 'preview', value: 7, type: 'number', onChange() { TextureAnimator.updateSpeed() }}); new Setting('particle_tick_rate',{category: 'preview', value: 30, type: 'number', onChange() { WinterskyScene.global_options.tick_rate = this.value; }}); new Setting('volume', {category: 'preview', value: 80, type: 'number'}); new Setting('display_skin', {category: 'preview', value: false, type: 'click', condition: isApp, icon: 'icon-player', click: function() { changeDisplaySkin() }}); //Edit new Setting('undo_limit', {category: 'edit', value: 256, type: 'number'}); new Setting('canvas_unselect', {category: 'edit', value: false}); new Setting('highlight_cubes', {category: 'edit', value: true, onChange() { updateCubeHighlights(); }}); new Setting('allow_display_slot_mirror', {category: 'edit', value: false, onChange(value) { DisplayMode.vue.allow_mirroring = value; }}) new Setting('deactivate_size_limit',{category: 'edit', value: false}); new Setting('vertex_merge_distance',{category: 'edit', value: 0.1, step: 0.01, type: 'number'}); new Setting('preview_paste_behavior',{category: 'edit', value: 'always_ask', type: 'select', options: { 'always_ask': tl('settings.preview_paste_behavior.always_ask'), 'outliner': tl('menu.paste.outliner'), 'face': tl('menu.paste.face'), 'mesh_selection': tl('menu.paste.mesh_selection'), }}); //Grid new Setting('base_grid', {category: 'grid', value: true,}); new Setting('large_grid', {category: 'grid', value: true}); new Setting('full_grid', {category: 'grid', value: false}); new Setting('large_box', {category: 'grid', value: false}); new Setting('large_grid_size', {category: 'grid', value: 3, type: 'number'}); //new Setting('display_grid', {category: 'grid', value: false}); new Setting('painting_grid', {category: 'grid', value: true, onChange() { Canvas.updatePaintingGrid(); }}); new Setting('ground_plane', {category: 'grid', value: false, onChange() { Canvas.ground_plane.visible = this.value; }}); //Snapping new Setting('edit_size', {category: 'snapping', value: 16, type: 'number'}); new Setting('shift_size', {category: 'snapping', value: 64, type: 'number'}); new Setting('ctrl_size', {category: 'snapping', value: 160, type: 'number'}); new Setting('ctrl_shift_size', {category: 'snapping', value: 640, type: 'number'}); new Setting('negative_size',{category: 'snapping', value: false}); //Paint new Setting('sync_color', {category: 'paint', value: false}); new Setting('color_wheel', {category: 'paint', value: false, onChange(value) { Interface.Panels.color.vue.picker_type = value ? 'wheel' : 'box'; }}); new Setting('paint_side_restrict', {category: 'paint', value: true}); new Setting('brush_opacity_modifier', {category: 'paint', value: 'pressure', type: 'select', options: { 'pressure': tl('settings.brush_modifier.pressure'), 'tilt': tl('settings.brush_modifier.tilt'), 'none': tl('settings.brush_modifier.none'), }}); new Setting('brush_size_modifier', {category: 'paint', value: 'tilt', type: 'select', options: { 'pressure': tl('settings.brush_modifier.pressure'), 'tilt': tl('settings.brush_modifier.tilt'), 'none': tl('settings.brush_modifier.none'), }}); new Setting('image_editor', {category: 'paint', value: false, type: 'click', condition: isApp, icon: 'fas.fa-pen-square', click: function() {changeImageEditor(null, true) }}); //Defaults new Setting('autouv', {category: 'defaults', value: true}); new Setting('create_rename', {category: 'defaults', value: false}); new Setting('show_only_selected_uv', {category: 'defaults', value: false}); new Setting('default_path', {category: 'defaults', value: false, type: 'click', condition: isApp, icon: 'burst_mode', click: function() { openDefaultTexturePath() }}); new Setting('animation_snap', {category: 'defaults', value: 24, type: 'number'}); //Dialogs new Setting('dialog_larger_cubes', {category: 'dialogs', value: true, name: tl('message.model_clipping.title'), description: tl('settings.dialog.desc', [tl('message.model_clipping.title')])}); new Setting('dialog_rotation_limit', {category: 'dialogs', value: true, name: tl('message.rotation_limit.title'), description: tl('settings.dialog.desc', [tl('message.rotation_limit.title')])}); new Setting('dialog_loose_texture', {category: 'dialogs', value: true, name: tl('message.loose_texture.title'), description: tl('settings.dialog.desc', [tl('message.loose_texture.title')])}); new Setting('dialog_invalid_characters',{category: 'dialogs', value: true, name: tl('message.invalid_characters.title'), description: tl('settings.dialog.desc', [tl('message.invalid_characters.title')])}); //Application new Setting('recent_projects', {category: 'application', value: 32, max: 256, min: 0, type: 'number', condition: isApp}); new Setting('backup_interval', {category: 'application', value: 10, type: 'number', condition: isApp}); new Setting('backup_retain', {category: 'application', value: 30, type: 'number', condition: isApp}); new Setting('automatic_updates', {category: 'application', value: true, condition: isApp}); new Setting('update_to_prereleases', {category: 'application', value: false, condition: isApp, launch_setting: true}); new Setting('hardware_acceleration', {category: 'application', value: true, condition: isApp, launch_setting: true}); //Export new Setting('minifiedout', {category: 'export', value: false}); new Setting('minify_bbmodel', {category: 'export', value: true}); new Setting('export_empty_groups', {category: 'export', value: true}); new Setting('export_groups', {category: 'export', value: true}); new Setting('obj_face_export_mode', {category: 'export', value: 'both', type: 'select', options: { both: tl('settings.obj_face_export_mode.both'), tris: tl('settings.obj_face_export_mode.tris'), quads: tl('settings.obj_face_export_mode.quads'), }}); new Setting('animation_sample_rate',{category: 'export', value: 24, type: 'number'}); new Setting('sketchfab_token', {category: 'export', value: '', type: 'password'}); new Setting('credit', {category: 'export', value: 'Made with Blockbench', type: 'text'}); Blockbench.onUpdateTo('3.8', () => { settings.preview_checkerboard.value = true; settings.uv_checkerboard.value = true; }) }, addCategory(id, data = {}) { Settings.structure[id] = { name: data.name || tl('settings.category.'+id), open: data.open != undefined ? !!data.open : id === 'general', items: {} } Settings.dialog.sidebar.pages[id] = Settings.structure[id].name; Settings.dialog.sidebar.build(); }, saveLocalStorages() { var settings_copy = {} for (var key in settings) { settings_copy[key] = {value: settings[key].value} } localStorage.setItem('settings', JSON.stringify(settings_copy) ) if (window.canvas_scenes) { localStorage.setItem('canvas_scenes', JSON.stringify(canvas_scenes)) } if (window.ColorPanel) { ColorPanel.saveLocalStorages() } }, save() { Settings.saveLocalStorages() function hasSettingChanged(id) { return (settings[id].value !== Settings.old[id]) } updateSelection() for (var key in BarItems) { var action = BarItems[key] if (action.linked_setting) { if (settings[action.linked_setting] && action.value != settings[action.linked_setting].value) { action.value = settings[action.linked_setting].value; action.updateEnabledState(); } } } if (hasSettingChanged('base_grid') || hasSettingChanged('large_grid') || hasSettingChanged('full_grid') || hasSettingChanged('large_grid_size') ||hasSettingChanged('large_box') || hasSettingChanged('edit_size')) { Canvas.buildGrid() } Canvas.outlineMaterial.depthTest = !settings.seethrough_outline.value if (hasSettingChanged('brightness')) { Canvas.updateShading() } for (var id in settings) { var setting = settings[id]; if (!Condition(setting.condition)) continue; if (setting.onChange && hasSettingChanged(id)) { setting.onChange(setting.value); } if (isApp && setting.launch_setting && hasSettingChanged(id)) { ipcRenderer.send('edit-launch-setting', {key: id, value: setting.value}) } } Blockbench.dispatchEvent('update_settings'); }, import(file) { let data = JSON.parse(file.content); for (let key in settings) { let setting = settings[key]; if (setting instanceof Setting && data.settings[key] !== undefined) { setting.set(data.settings[key]); } } }, get(id) { if (id && settings[id]) { return settings[id].value; } }, openDialog(options = {}) { for (var sett in settings) { if (settings.hasOwnProperty(sett)) { Settings.old[sett] = settings[sett].value } } Settings.dialog.show(); if (options.search_term) Settings.dialog.content_vue.search_term = options.search_term; }, old: {} } Settings.setup() function updateStreamerModeNotification() { $('#start_screen section#streamer_mode').detach() if (settings.streamer_mode.value) { addStartScreenSection('streamer_mode', { graphic: {type: 'icon', icon: 'live_tv'}, color: 'var(--color-stream)', text_color: 'var(--color-light)', text: [ {type: 'h1', text: tl('interface.streamer_mode_on'), click() { Settings.openDialog({search_term: tl('settings.streamer_mode')}) }} ] }) } } BARS.defineActions(() => { new Action('settings_window', { icon: 'settings', category: 'blockbench', click: function () { for (var sett in settings) { if (settings.hasOwnProperty(sett)) { Settings.old[sett] = settings[sett].value } } Settings.dialog.show() document.querySelector('dialog#settings .search_bar > input').focus() } }) new Action('import_settings', { icon: 'folder', category: 'blockbench', click: function () { Blockbench.import({ resource_id: 'config', extensions: ['bbsettings'], type: 'Blockbench Settings' }, function(files) { Settings.import(files[0]); }) } }) new Action('export_settings', { icon: 'fas.fa-user-cog', category: 'blockbench', click: async function () { let private_data = []; var settings_copy = {} for (var key in settings) { settings_copy[key] = settings[key].value; if (settings[key].value && settings[key].type == 'password') { private_data.push(key); } } if (private_data.length) { let go_on = await new Promise((resolve, reject) => { Blockbench.showMessageBox({ title: 'dialog.export_private_settings.title', message: tl('dialog.export_private_settings.message', [private_data.map(key => settings[key].name).join(', ')]), buttons: ['dialog.export_private_settings.keep', 'dialog.export_private_settings.omit', 'dialog.cancel'] }, result => { if (result == 1) { private_data.forEach(key => { delete settings_copy[key]; }) } resolve(result !== 2); }) }); if (!go_on) return; } Blockbench.export({ resource_id: 'config', type: 'Blockbench Settings', extensions: ['bbsettings'], content: compileJSON({settings: settings_copy}) }) } }) BarItems.import_settings.toElement('#settings_title_bar') BarItems.export_settings.toElement('#settings_title_bar') }) onVueSetup(function() { for (var key in settings) { if (settings[key].condition == false) continue; var category = settings[key].category if (!category) category = 'general' if (!Settings.structure[category]) { Settings.structure[category] = { name: tl('settings.category.'+category), open: category === 'general', items: {} } } Settings.structure[category].items[key] = settings[key] } let sidebar_pages = {}; for (let key in Settings.structure) { sidebar_pages[key] = Settings.structure[key].name; } Settings.dialog = new Dialog({ id: 'settings', title: 'dialog.settings.settings', width: 920, singleButton: true, title_menu: new Menu([ 'settings_window', 'keybindings_window', 'theme_window', 'about_window', ]), sidebar: { pages: sidebar_pages, page: 'general', actions: [ 'import_settings', 'export_settings', ], onPageSwitch(page) { Settings.dialog.content_vue.open_category = page; Settings.dialog.content_vue.search_term = ''; } }, component: { data() {return { structure: Settings.structure, open_category: 'general', search_term: '', }}, methods: { saveSettings() { Settings.saveLocalStorages(); }, getIconNode: Blockbench.getIconNode, Condition }, computed: { list() { if (this.search_term) { var keywords = this.search_term.toLowerCase().replace(/_/g, ' ').split(' '); var items = {}; for (var key in settings) { var setting = settings[key]; if (Condition(setting.condition)) { var name = setting.name.toLowerCase(); var desc = setting.description.toLowerCase(); var missmatch = false; for (var word of keywords) { if ( !key.includes(word) && !name.includes(word) && !desc.includes(word) ) { missmatch = true; } } if (!missmatch) { items[key] = setting; } } } return items; } else { return this.structure[this.open_category].items; } }, title() { if (this.search_term) { return tl('dialog.settings.search_results'); } else { return this.structure[this.open_category].name; } } }, template: `

{{ title }}

` }, onButton() { Settings.save(); } }) })