yomichan/ext/js/pages/settings/profile-controller.js
2021-04-03 13:32:53 -04:00

654 lines
24 KiB
JavaScript

/*
* 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;
}
}