Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at lambda-fork/main 156 lines 5.8 kB view raw
1/* 2 * Copyright (C) 2023-2025 Yomitan Authors 3 * Copyright (C) 2020-2022 Yomichan Authors 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19import {EventListenerCollection} from '../core/event-listener-collection.js'; 20import {generateId} from '../core/utilities.js'; 21import {PanelElement} from '../dom/panel-element.js'; 22import {querySelectorNotNull} from '../dom/query-selector.js'; 23 24export class DisplayProfileSelection { 25 /** 26 * @param {import('./display.js').Display} display 27 */ 28 constructor(display) { 29 /** @type {import('./display.js').Display} */ 30 this._display = display; 31 /** @type {HTMLElement} */ 32 this._profileList = querySelectorNotNull(document, '#profile-list'); 33 /** @type {HTMLButtonElement} */ 34 this._profileButton = querySelectorNotNull(document, '#profile-button'); 35 /** @type {HTMLElement} */ 36 const profilePanelElement = querySelectorNotNull(document, '#profile-panel'); 37 /** @type {PanelElement} */ 38 this._profilePanel = new PanelElement(profilePanelElement, 375); // Milliseconds; includes buffer 39 /** @type {boolean} */ 40 this._profileListNeedsUpdate = false; 41 /** @type {EventListenerCollection} */ 42 this._eventListeners = new EventListenerCollection(); 43 /** @type {string} */ 44 this._source = generateId(16); 45 /** @type {HTMLElement} */ 46 this._profileName = querySelectorNotNull(document, '#profile-name'); 47 } 48 49 /** */ 50 async prepare() { 51 this._display.application.on('optionsUpdated', this._onOptionsUpdated.bind(this)); 52 this._profileButton.addEventListener('click', this._onProfileButtonClick.bind(this), false); 53 this._profileListNeedsUpdate = true; 54 await this._updateCurrentProfileName(); 55 } 56 57 // Private 58 59 /** 60 * @param {{source: string}} details 61 */ 62 async _onOptionsUpdated({source}) { 63 if (source === this._source) { return; } 64 this._profileListNeedsUpdate = true; 65 if (this._profilePanel.isVisible()) { 66 void this._updateProfileList(); 67 } 68 await this._updateCurrentProfileName(); 69 } 70 71 /** 72 * @param {MouseEvent} e 73 */ 74 _onProfileButtonClick(e) { 75 e.preventDefault(); 76 e.stopPropagation(); 77 this._setProfilePanelVisible(!this._profilePanel.isVisible()); 78 } 79 80 /** 81 * @param {boolean} visible 82 */ 83 _setProfilePanelVisible(visible) { 84 this._profilePanel.setVisible(visible); 85 this._profileButton.classList.toggle('sidebar-button-highlight', visible); 86 document.documentElement.dataset.profilePanelVisible = `${visible}`; 87 if (visible && this._profileListNeedsUpdate) { 88 void this._updateProfileList(); 89 } 90 } 91 92 /** */ 93 async _updateCurrentProfileName() { 94 const {profileCurrent, profiles} = await this._display.application.api.optionsGetFull(); 95 if (profiles.length === 1) { 96 this._profileButton.style.display = 'none'; 97 return; 98 } 99 const currentProfile = profiles[profileCurrent]; 100 this._profileName.textContent = currentProfile.name; 101 } 102 103 /** */ 104 async _updateProfileList() { 105 this._profileListNeedsUpdate = false; 106 const options = await this._display.application.api.optionsGetFull(); 107 108 this._eventListeners.removeAllEventListeners(); 109 const displayGenerator = this._display.displayGenerator; 110 111 const {profileCurrent, profiles} = options; 112 const fragment = document.createDocumentFragment(); 113 for (let i = 0, ii = profiles.length; i < ii; ++i) { 114 const {name} = profiles[i]; 115 const entry = displayGenerator.createProfileListItem(); 116 /** @type {HTMLInputElement} */ 117 const radio = querySelectorNotNull(entry, '.profile-entry-is-default-radio'); 118 radio.checked = (i === profileCurrent); 119 /** @type {Element} */ 120 const nameNode = querySelectorNotNull(entry, '.profile-list-item-name'); 121 nameNode.textContent = name; 122 fragment.appendChild(entry); 123 this._eventListeners.addEventListener(radio, 'change', this._onProfileRadioChange.bind(this, i), false); 124 } 125 this._profileList.textContent = ''; 126 this._profileList.appendChild(fragment); 127 } 128 129 /** 130 * @param {number} index 131 * @param {Event} e 132 */ 133 _onProfileRadioChange(index, e) { 134 const element = /** @type {HTMLInputElement} */ (e.currentTarget); 135 if (element.checked) { 136 void this._setProfileCurrent(index); 137 } 138 } 139 140 /** 141 * @param {number} index 142 */ 143 async _setProfileCurrent(index) { 144 /** @type {import('settings-modifications').ScopedModificationSet} */ 145 const modification = { 146 action: 'set', 147 path: 'profileCurrent', 148 value: index, 149 scope: 'global', 150 optionsContext: null, 151 }; 152 await this._display.application.api.modifySettings([modification], this._source); 153 this._setProfilePanelVisible(false); 154 await this._updateCurrentProfileName(); 155 } 156}