diff options
Diffstat (limited to 'ext/js/settings/profile-controller.js')
| -rw-r--r-- | ext/js/settings/profile-controller.js | 698 | 
1 files changed, 698 insertions, 0 deletions
diff --git a/ext/js/settings/profile-controller.js b/ext/js/settings/profile-controller.js new file mode 100644 index 00000000..914fc679 --- /dev/null +++ b/ext/js/settings/profile-controller.js @@ -0,0 +1,698 @@ +/* + * 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 + * api + */ + +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._profileNameInput = null; +        this._removeProfileNameElement = null; +        this._profileAddButton = null; +        this._profileRemoveButton = null; +        this._profileRemoveConfirmButton = null; +        this._profileCopyButton = null; +        this._profileCopyConfirmButton = null; +        this._profileMoveUpButton = null; +        this._profileMoveDownButton = 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 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._profileNameInput = document.querySelector('#profile-name-input'); +        this._removeProfileNameElement = document.querySelector('#profile-remove-name'); +        this._profileAddButton = document.querySelector('#profile-add-button'); +        this._profileRemoveButton = document.querySelector('#profile-remove-button'); +        this._profileRemoveConfirmButton = document.querySelector('#profile-remove-confirm-button'); +        this._profileCopyButton = document.querySelector('#profile-copy-button'); +        this._profileCopyConfirmButton = document.querySelector('#profile-copy-confirm-button'); +        this._profileMoveUpButton = document.querySelector('#profile-move-up-button'); +        this._profileMoveDownButton = document.querySelector('#profile-move-down-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._profileNameInput !== null) { this._profileNameInput.addEventListener('change', this._onNameChanged.bind(this), false); } +        if (this._profileAddButton !== null) { this._profileAddButton.addEventListener('click', this._onAdd.bind(this), false); } +        if (this._profileRemoveButton !== null) { this._profileRemoveButton.addEventListener('click', this._onDelete.bind(this), false); } +        if (this._profileRemoveConfirmButton !== null) { this._profileRemoveConfirmButton.addEventListener('click', this._onDeleteConfirm.bind(this), false); } +        if (this._profileCopyButton !== null) { this._profileCopyButton.addEventListener('click', this._onCopy.bind(this), false); } +        if (this._profileCopyConfirmButton !== null) { this._profileCopyConfirmButton.addEventListener('click', this._onCopyConfirm.bind(this), false); } +        if (this._profileMoveUpButton !== null) { this._profileMoveUpButton.addEventListener('click', this._onMove.bind(this, -1), false); } +        if (this._profileMoveDownButton !== null) { this._profileMoveDownButton.addEventListener('click', this._onMove.bind(this, 1), 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; +        const settingsProfile = this._getProfile(settingsProfileIndex); + +        // Udpate UI +        this._updateProfileSelectOptions(); + +        this._profileActiveSelect.value = `${profileCurrent}`; +        this._profileTargetSelect.value = `${settingsProfileIndex}`; + +        if (this._profileRemoveButton !== null) { this._profileRemoveButton.disabled = (profiles.length <= 1); } +        if (this._profileCopyButton !== null) { this._profileCopyButton.disabled = (profiles.length <= 1); } +        if (this._profileMoveUpButton !== null) { this._profileMoveUpButton.disabled = (settingsProfileIndex <= 0); } +        if (this._profileMoveDownButton !== null) { this._profileMoveDownButton.disabled = (settingsProfileIndex >= profiles.length - 1); } + +        if (this._profileNameInput !== null && settingsProfile !== null) { this._profileNameInput.value = settingsProfile.name; } + +        // 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; +    } + +    _onNameChanged(e) { +        this.setProfileName(this._settingsController.profileIndex, e.currentTarget.value); +    } + +    _onAdd() { +        this.duplicateProfile(this._settingsController.profileIndex); +    } + +    _onDelete(e) { +        const profileIndex = this._settingsController.profileIndex; +        if (e.shiftKey) { +            this.deleteProfile(profileIndex); +        } else { +            this.openDeleteProfileModal(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); +    } + +    _onCopy() { +        this.openCopyProfileModal(this._settingsController.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); +    } + +    _onMove(offset) { +        this.moveProfile(this._settingsController.profileIndex, offset); +    } + +    _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; +    } +}  |