/* * Copyright (C) 2020-2021 Yomichan Authors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /* global * ProfileConditionsUI */ class ProfileController { constructor(settingsController, modalController) { this._settingsController = settingsController; this._modalController = modalController; this._profileConditionsUI = new ProfileConditionsUI(settingsController); this._profileConditionsIndex = null; this._profileActiveSelect = null; this._profileTargetSelect = null; this._profileCopySourceSelect = null; this._removeProfileNameElement = null; this._profileAddButton = null; this._profileRemoveConfirmButton = null; this._profileCopyConfirmButton = null; this._profileEntryListContainer = null; this._profileConditionsProfileName = null; this._profileRemoveModal = null; this._profileCopyModal = null; this._profileConditionsModal = null; this._profileEntriesSupported = false; this._profileEntryList = []; this._profiles = []; this._profileCurrent = 0; } get profileCount() { return this._profiles.length; } get profileCurrentIndex() { return this._profileCurrent; } async prepare() { const {platform: {os}} = await yomichan.api.getEnvironmentInfo(); this._profileConditionsUI.os = os; this._profileActiveSelect = document.querySelector('#profile-active-select'); this._profileTargetSelect = document.querySelector('#profile-target-select'); this._profileCopySourceSelect = document.querySelector('#profile-copy-source-select'); this._removeProfileNameElement = document.querySelector('#profile-remove-name'); this._profileAddButton = document.querySelector('#profile-add-button'); this._profileRemoveConfirmButton = document.querySelector('#profile-remove-confirm-button'); this._profileCopyConfirmButton = document.querySelector('#profile-copy-confirm-button'); this._profileEntryListContainer = document.querySelector('#profile-entry-list'); this._profileConditionsProfileName = document.querySelector('#profile-conditions-profile-name'); this._profileRemoveModal = this._modalController.getModal('profile-remove'); this._profileCopyModal = this._modalController.getModal('profile-copy'); this._profileConditionsModal = this._modalController.getModal('profile-conditions'); this._profileEntriesSupported = (this._profileEntryListContainer !== null); if (this._profileActiveSelect !== null) { this._profileActiveSelect.addEventListener('change', this._onProfileActiveChange.bind(this), false); } if (this._profileTargetSelect !== null) { this._profileTargetSelect.addEventListener('change', this._onProfileTargetChange.bind(this), false); } if (this._profileAddButton !== null) { this._profileAddButton.addEventListener('click', this._onAdd.bind(this), false); } if (this._profileRemoveConfirmButton !== null) { this._profileRemoveConfirmButton.addEventListener('click', this._onDeleteConfirm.bind(this), false); } if (this._profileCopyConfirmButton !== null) { this._profileCopyConfirmButton.addEventListener('click', this._onCopyConfirm.bind(this), false); } this._profileConditionsUI.on('conditionGroupCountChanged', this._onConditionGroupCountChanged.bind(this)); this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); this._onOptionsChanged(); } async moveProfile(profileIndex, offset) { if (this._getProfile(profileIndex) === null) { return; } const profileIndexNew = Math.max(0, Math.min(this._profiles.length - 1, profileIndex + offset)); if (profileIndex === profileIndexNew) { return; } await this.swapProfiles(profileIndex, profileIndexNew); } async setProfileName(profileIndex, value) { const profile = this._getProfile(profileIndex); if (profile === null) { return; } profile.name = value; this._updateSelectName(profileIndex, value); const profileEntry = this._getProfileEntry(profileIndex); if (profileEntry !== null) { profileEntry.setName(value); } await this._settingsController.setGlobalSetting(`profiles[${profileIndex}].name`, value); } async setDefaultProfile(profileIndex) { const profile = this._getProfile(profileIndex); if (profile === null) { return; } this._profileActiveSelect.value = `${profileIndex}`; this._profileCurrent = profileIndex; const profileEntry = this._getProfileEntry(profileIndex); if (profileEntry !== null) { profileEntry.setIsDefault(true); } await this._settingsController.setGlobalSetting('profileCurrent', profileIndex); } async copyProfile(sourceProfileIndex, destinationProfileIndex) { const sourceProfile = this._getProfile(sourceProfileIndex); if (sourceProfile === null || !this._getProfile(destinationProfileIndex)) { return; } const options = clone(sourceProfile.options); this._profiles[destinationProfileIndex].options = options; this._updateProfileSelectOptions(); const destinationProfileEntry = this._getProfileEntry(destinationProfileIndex); if (destinationProfileEntry !== null) { destinationProfileEntry.updateState(); } await this._settingsController.modifyGlobalSettings([{ action: 'set', path: `profiles[${destinationProfileIndex}].options`, value: options }]); await this._settingsController.refresh(); } async duplicateProfile(profileIndex) { const profile = this._getProfile(profileIndex); if (this.profile === null) { return; } // Create new profile const newProfile = clone(profile); newProfile.name = this._createCopyName(profile.name, this._profiles, 100); // Update state const index = this._profiles.length; this._profiles.push(newProfile); if (this._profileEntriesSupported) { this._addProfileEntry(index); } this._updateProfileSelectOptions(); // Modify settings await this._settingsController.modifyGlobalSettings([{ action: 'splice', path: 'profiles', start: index, deleteCount: 0, items: [newProfile] }]); // Update profile index this._settingsController.profileIndex = index; } async deleteProfile(profileIndex) { const profile = this._getProfile(profileIndex); if (profile === null || this.profileCount <= 1) { return; } // Get indices let profileCurrentNew = this._profileCurrent; const settingsProfileIndex = this._settingsController.profileIndex; // Construct settings modifications const modifications = [{ action: 'splice', path: 'profiles', start: profileIndex, deleteCount: 1, items: [] }]; if (profileCurrentNew >= profileIndex) { profileCurrentNew = Math.min(profileCurrentNew - 1, this._profiles.length - 1); modifications.push({ action: 'set', path: 'profileCurrent', value: profileCurrentNew }); } // Update state this._profileCurrent = profileCurrentNew; this._profiles.splice(profileIndex, 1); if (profileIndex < this._profileEntryList.length) { const profileEntry = this._profileEntryList[profileIndex]; profileEntry.cleanup(); this._profileEntryList.splice(profileIndex, 1); for (let i = profileIndex, ii = this._profileEntryList.length; i < ii; ++i) { this._profileEntryList[i].index = i; } } const profileEntry2 = this._getProfileEntry(profileCurrentNew); if (profileEntry2 !== null) { profileEntry2.setIsDefault(true); } this._updateProfileSelectOptions(); // Modify settings await this._settingsController.modifyGlobalSettings(modifications); // Update profile index if (settingsProfileIndex === profileIndex) { this._settingsController.profileIndex = profileCurrentNew; } } async swapProfiles(index1, index2) { const profile1 = this._getProfile(index1); const profile2 = this._getProfile(index2); if (profile1 === null || profile2 === null || index1 === index2) { return; } // Get swapped indices const profileCurrent = this._profileCurrent; const profileCurrentNew = this._getSwappedValue(profileCurrent, index1, index2); const settingsProfileIndex = this._settingsController.profileIndex; const settingsProfileIndexNew = this._getSwappedValue(settingsProfileIndex, index1, index2); // Construct settings modifications const modifications = [{ action: 'swap', path1: `profiles[${index1}]`, path2: `profiles[${index2}]` }]; if (profileCurrentNew !== profileCurrent) { modifications.push({ action: 'set', path: 'profileCurrent', value: profileCurrentNew }); } // Update state this._profileCurrent = profileCurrentNew; this._profiles[index1] = profile2; this._profiles[index2] = profile1; const entry1 = this._getProfileEntry(index1); const entry2 = this._getProfileEntry(index2); if (entry1 !== null && entry2 !== null) { entry1.index = index2; entry2.index = index1; this._swapDomNodes(entry1.node, entry2.node); this._profileEntryList[index1] = entry2; this._profileEntryList[index2] = entry1; } this._updateProfileSelectOptions(); // Modify settings await this._settingsController.modifyGlobalSettings(modifications); // Update profile index if (settingsProfileIndex !== settingsProfileIndexNew) { this._settingsController.profileIndex = settingsProfileIndexNew; } } openDeleteProfileModal(profileIndex) { const profile = this._getProfile(profileIndex); if (profile === null || this.profileCount <= 1) { return; } this._removeProfileNameElement.textContent = profile.name; this._profileRemoveModal.node.dataset.profileIndex = `${profileIndex}`; this._profileRemoveModal.setVisible(true); } openCopyProfileModal(profileIndex) { const profile = this._getProfile(profileIndex); if (profile === null || this.profileCount <= 1) { return; } let copyFromIndex = this._profileCurrent; if (copyFromIndex === profileIndex) { if (profileIndex !== 0) { copyFromIndex = 0; } else if (this.profileCount > 1) { copyFromIndex = 1; } } const profileIndexString = `${profileIndex}`; for (const option of this._profileCopySourceSelect.querySelectorAll('option')) { const {value} = option; option.disabled = (value === profileIndexString); } this._profileCopySourceSelect.value = `${copyFromIndex}`; this._profileCopyModal.node.dataset.profileIndex = `${profileIndex}`; this._profileCopyModal.setVisible(true); } openProfileConditionsModal(profileIndex) { const profile = this._getProfile(profileIndex); if (profile === null) { return; } if (this._profileConditionsModal === null) { return; } this._profileConditionsModal.setVisible(true); this._profileConditionsUI.cleanup(); this._profileConditionsIndex = profileIndex; this._profileConditionsUI.prepare(profileIndex); if (this._profileConditionsProfileName !== null) { this._profileConditionsProfileName.textContent = profile.name; } } // Private async _onOptionsChanged() { // Update state const {profiles, profileCurrent} = await this._settingsController.getOptionsFull(); this._profiles = profiles; this._profileCurrent = profileCurrent; const settingsProfileIndex = this._settingsController.profileIndex; // Udpate UI this._updateProfileSelectOptions(); this._profileActiveSelect.value = `${profileCurrent}`; this._profileTargetSelect.value = `${settingsProfileIndex}`; // Update profile conditions this._profileConditionsUI.cleanup(); const conditionsProfile = this._getProfile(this._profileConditionsIndex !== null ? this._profileConditionsIndex : settingsProfileIndex); if (conditionsProfile !== null) { this._profileConditionsUI.prepare(settingsProfileIndex); } // Udpate profile entries for (const entry of this._profileEntryList) { entry.cleanup(); } this._profileEntryList = []; if (this._profileEntriesSupported) { for (let i = 0, ii = profiles.length; i < ii; ++i) { this._addProfileEntry(i); } } } _onProfileActiveChange(e) { const value = this._tryGetValidProfileIndex(e.currentTarget.value); if (value === null) { return; } this.setDefaultProfile(value); } _onProfileTargetChange(e) { const value = this._tryGetValidProfileIndex(e.currentTarget.value); if (value === null) { return; } this._settingsController.profileIndex = value; } _onAdd() { this.duplicateProfile(this._settingsController.profileIndex); } _onDeleteConfirm() { const modal = this._profileRemoveModal; modal.setVisible(false); const {node} = modal; let profileIndex = node.dataset.profileIndex; delete node.dataset.profileIndex; profileIndex = this._tryGetValidProfileIndex(profileIndex); if (profileIndex === null) { return; } this.deleteProfile(profileIndex); } _onCopyConfirm() { const modal = this._profileCopyModal; modal.setVisible(false); const {node} = modal; let destinationProfileIndex = node.dataset.profileIndex; delete node.dataset.profileIndex; destinationProfileIndex = this._tryGetValidProfileIndex(destinationProfileIndex); if (destinationProfileIndex === null) { return; } const sourceProfileIndex = this._tryGetValidProfileIndex(this._profileCopySourceSelect.value); if (sourceProfileIndex === null) { return; } this.copyProfile(sourceProfileIndex, destinationProfileIndex); } _onConditionGroupCountChanged({count, profileIndex}) { if (profileIndex >= 0 && profileIndex < this._profileEntryList.length) { const profileEntry = this._profileEntryList[profileIndex]; profileEntry.setConditionGroupsCount(count); } } _addProfileEntry(profileIndex) { const profile = this._profiles[profileIndex]; const node = this._settingsController.instantiateTemplate('profile-entry'); const entry = new ProfileEntry(this, node); this._profileEntryList.push(entry); entry.prepare(profile, profileIndex); this._profileEntryListContainer.appendChild(node); } _updateProfileSelectOptions() { for (const select of this._getAllProfileSelects()) { const fragment = document.createDocumentFragment(); for (let i = 0; i < this._profiles.length; ++i) { const profile = this._profiles[i]; const option = document.createElement('option'); option.value = `${i}`; option.textContent = profile.name; fragment.appendChild(option); } select.textContent = ''; select.appendChild(fragment); } } _updateSelectName(index, name) { const optionValue = `${index}`; for (const select of this._getAllProfileSelects()) { for (const option of select.querySelectorAll('option')) { if (option.value === optionValue) { option.textContent = name; } } } } _getAllProfileSelects() { return [ this._profileActiveSelect, this._profileTargetSelect, this._profileCopySourceSelect ]; } _tryGetValidProfileIndex(stringValue) { if (typeof stringValue !== 'string') { return null; } const intValue = parseInt(stringValue, 10); return ( Number.isFinite(intValue) && intValue >= 0 && intValue < this.profileCount ? intValue : null ); } _createCopyName(name, profiles, maxUniqueAttempts) { let space, index, prefix, suffix; const match = /^([\w\W]*\(Copy)((\s+)(\d+))?(\)\s*)$/.exec(name); if (match === null) { prefix = `${name} (Copy`; space = ''; index = ''; suffix = ')'; } else { prefix = match[1]; suffix = match[5]; if (typeof match[2] === 'string') { space = match[3]; index = parseInt(match[4], 10) + 1; } else { space = ' '; index = 2; } } let i = 0; while (true) { const newName = `${prefix}${space}${index}${suffix}`; if (i++ >= maxUniqueAttempts || profiles.findIndex((profile) => profile.name === newName) < 0) { return newName; } if (typeof index !== 'number') { index = 2; space = ' '; } else { ++index; } } } _getSwappedValue(currentValue, value1, value2) { if (currentValue === value1) { return value2; } if (currentValue === value2) { return value1; } return currentValue; } _getProfile(profileIndex) { return (profileIndex >= 0 && profileIndex < this._profiles.length ? this._profiles[profileIndex] : null); } _getProfileEntry(profileIndex) { return (profileIndex >= 0 && profileIndex < this._profileEntryList.length ? this._profileEntryList[profileIndex] : null); } _swapDomNodes(node1, node2) { const parent1 = node1.parentNode; const parent2 = node2.parentNode; const next1 = node1.nextSibling; const next2 = node2.nextSibling; if (node2 !== next1) { parent1.insertBefore(node2, next1); } if (node1 !== next2) { parent2.insertBefore(node1, next2); } } } class ProfileEntry { constructor(profileController, node) { this._profileController = profileController; this._node = node; this._profile = null; this._index = 0; this._isDefaultRadio = null; this._nameInput = null; this._countLink = null; this._countText = null; this._menuButton = null; this._eventListeners = new EventListenerCollection(); } get index() { return this._index; } set index(value) { this._index = value; } get node() { return this._node; } prepare(profile, index) { this._profile = profile; this._index = index; const node = this._node; this._isDefaultRadio = node.querySelector('.profile-entry-is-default-radio'); this._nameInput = node.querySelector('.profile-entry-name-input'); this._countLink = node.querySelector('.profile-entry-condition-count-link'); this._countText = node.querySelector('.profile-entry-condition-count'); this._menuButton = node.querySelector('.profile-entry-menu-button'); this.updateState(); this._eventListeners.addEventListener(this._isDefaultRadio, 'change', this._onIsDefaultRadioChange.bind(this), false); this._eventListeners.addEventListener(this._nameInput, 'input', this._onNameInputInput.bind(this), false); this._eventListeners.addEventListener(this._countLink, 'click', this._onConditionsCountLinkClick.bind(this), false); this._eventListeners.addEventListener(this._menuButton, 'menuOpen', this._onMenuOpen.bind(this), false); this._eventListeners.addEventListener(this._menuButton, 'menuClose', this._onMenuClose.bind(this), false); } cleanup() { this._eventListeners.removeAllEventListeners(); if (this._node.parentNode !== null) { this._node.parentNode.removeChild(this._node); } } setName(value) { if (this._nameInput.value === value) { return; } this._nameInput.value = value; } setIsDefault(value) { this._isDefaultRadio.checked = value; } updateState() { this._nameInput.value = this._profile.name; this._countText.textContent = `${this._profile.conditionGroups.length}`; this._isDefaultRadio.checked = (this._index === this._profileController.profileCurrentIndex); } setConditionGroupsCount(count) { this._countText.textContent = `${count}`; } // Private _onIsDefaultRadioChange(e) { if (!e.currentTarget.checked) { return; } this._profileController.setDefaultProfile(this._index); } _onNameInputInput(e) { const name = e.currentTarget.value; this._profileController.setProfileName(this._index, name); } _onConditionsCountLinkClick() { this._profileController.openProfileConditionsModal(this._index); } _onMenuOpen(e) { const bodyNode = e.detail.menu.bodyNode; const count = this._profileController.profileCount; this._setMenuActionEnabled(bodyNode, 'moveUp', this._index > 0); this._setMenuActionEnabled(bodyNode, 'moveDown', this._index < count - 1); this._setMenuActionEnabled(bodyNode, 'copyFrom', count > 1); this._setMenuActionEnabled(bodyNode, 'delete', count > 1); } _onMenuClose(e) { switch (e.detail.action) { case 'moveUp': this._profileController.moveProfile(this._index, -1); break; case 'moveDown': this._profileController.moveProfile(this._index, 1); break; case 'copyFrom': this._profileController.openCopyProfileModal(this._index); break; case 'editConditions': this._profileController.openProfileConditionsModal(this._index); break; case 'duplicate': this._profileController.duplicateProfile(this._index); break; case 'delete': this._profileController.openDeleteProfileModal(this._index); break; } } _setMenuActionEnabled(menu, action, enabled) { const element = menu.querySelector(`[data-menu-action="${action}"]`); if (element === null) { return; } element.disabled = !enabled; } }