2025-01-11 01:59:38 +01:00
import { Popup } from '../../../../popup.js' ;
2023-12-20 13:40:44 +00:00
import { getSortableDelay } from '../../../../utils.js' ;
2023-12-25 13:07:06 +00:00
import { log , warn } from '../../index.js' ;
import { QuickReply } from '../QuickReply.js' ;
2023-12-20 13:40:44 +00:00
import { QuickReplySet } from '../QuickReplySet.js' ;
// eslint-disable-next-line no-unused-vars
import { QuickReplySettings } from '../QuickReplySettings.js' ;
export class SettingsUi {
/**@type {QuickReplySettings}*/ settings ;
/**@type {HTMLElement}*/ template ;
/**@type {HTMLElement}*/ dom ;
/**@type {HTMLInputElement}*/ isEnabled ;
/**@type {HTMLInputElement}*/ isCombined ;
2024-08-20 23:06:36 +03:00
/**@type {HTMLInputElement}*/ showPopoutButton ;
2023-12-20 13:40:44 +00:00
/**@type {HTMLElement}*/ globalSetList ;
/**@type {HTMLElement}*/ chatSetList ;
/**@type {QuickReplySet}*/ currentQrSet ;
/**@type {HTMLInputElement}*/ disableSend ;
/**@type {HTMLInputElement}*/ placeBeforeInput ;
/**@type {HTMLInputElement}*/ injectInput ;
2024-07-13 14:45:35 -04:00
/**@type {HTMLInputElement}*/ color ;
/**@type {HTMLInputElement}*/ onlyBorderColor ;
2023-12-20 13:40:44 +00:00
/**@type {HTMLSelectElement}*/ currentSet ;
constructor ( /**@type {QuickReplySettings}*/ settings ) {
this . settings = settings ;
2023-12-20 15:49:27 +00:00
settings . onRequestEditSet = ( qrs ) => this . selectQrSet ( qrs ) ;
2023-12-20 13:40:44 +00:00
}
rerender ( ) {
if ( ! this . dom ) return ;
const content = this . dom . querySelector ( '.inline-drawer-content' ) ;
content . innerHTML = '' ;
// @ts-ignore
Array . from ( this . template . querySelector ( '.inline-drawer-content' ) . cloneNode ( true ) . children ) . forEach ( el => {
content . append ( el ) ;
} ) ;
this . prepareDom ( ) ;
}
unrender ( ) {
this . dom ? . remove ( ) ;
this . dom = null ;
}
async render ( ) {
if ( ! this . dom ) {
const response = await fetch ( '/scripts/extensions/quick-reply/html/settings.html' , { cache : 'no-store' } ) ;
if ( response . ok ) {
this . template = document . createRange ( ) . createContextualFragment ( await response . text ( ) ) . querySelector ( '#qr--settings' ) ;
// @ts-ignore
this . dom = this . template . cloneNode ( true ) ;
this . prepareDom ( ) ;
} else {
warn ( 'failed to fetch settings template' ) ;
}
}
return this . dom ;
}
prepareGeneralSettings ( ) {
// general settings
this . isEnabled = this . dom . querySelector ( '#qr--isEnabled' ) ;
this . isEnabled . checked = this . settings . isEnabled ;
this . isEnabled . addEventListener ( 'click' , ( ) => this . onIsEnabled ( ) ) ;
this . isCombined = this . dom . querySelector ( '#qr--isCombined' ) ;
this . isCombined . checked = this . settings . isCombined ;
this . isCombined . addEventListener ( 'click' , ( ) => this . onIsCombined ( ) ) ;
2024-08-20 23:06:36 +03:00
this . showPopoutButton = this . dom . querySelector ( '#qr--showPopoutButton' ) ;
this . showPopoutButton . checked = this . settings . showPopoutButton ;
this . showPopoutButton . addEventListener ( 'click' , ( ) => this . onShowPopoutButton ( ) ) ;
2023-12-20 13:40:44 +00:00
}
prepareGlobalSetList ( ) {
2023-12-20 17:56:08 +00:00
const dom = this . template . querySelector ( '#qr--global' ) ;
const clone = dom . cloneNode ( true ) ;
// @ts-ignore
this . settings . config . renderSettingsInto ( clone ) ;
this . dom . querySelector ( '#qr--global' ) . replaceWith ( clone ) ;
2023-12-20 13:40:44 +00:00
}
prepareChatSetList ( ) {
2023-12-20 17:56:08 +00:00
const dom = this . template . querySelector ( '#qr--chat' ) ;
const clone = dom . cloneNode ( true ) ;
2023-12-20 15:49:27 +00:00
if ( this . settings . chatConfig ) {
2023-12-20 17:56:08 +00:00
// @ts-ignore
this . settings . chatConfig . renderSettingsInto ( clone ) ;
2023-12-20 15:49:27 +00:00
} else {
const info = document . createElement ( 'div' ) ; {
2023-12-20 13:40:44 +00:00
info . textContent = 'No active chat.' ;
2023-12-20 17:56:08 +00:00
// @ts-ignore
clone . append ( info ) ;
2023-12-20 13:40:44 +00:00
}
}
2023-12-20 17:56:08 +00:00
this . dom . querySelector ( '#qr--chat' ) . replaceWith ( clone ) ;
2023-12-20 13:40:44 +00:00
}
prepareQrEditor ( ) {
// qr editor
2025-01-11 01:41:46 +01:00
this . dom . querySelector ( '#qr--set-rename' ) . addEventListener ( 'click' , async ( ) => this . renameQrSet ( ) ) ;
2023-12-20 14:04:28 +00:00
this . dom . querySelector ( '#qr--set-new' ) . addEventListener ( 'click' , async ( ) => this . addQrSet ( ) ) ;
2023-12-25 13:07:06 +00:00
/**@type {HTMLInputElement}*/
const importFile = this . dom . querySelector ( '#qr--set-importFile' ) ;
importFile . addEventListener ( 'change' , async ( ) => {
await this . importQrSet ( importFile . files ) ;
importFile . value = null ;
} ) ;
this . dom . querySelector ( '#qr--set-import' ) . addEventListener ( 'click' , ( ) => importFile . click ( ) ) ;
this . dom . querySelector ( '#qr--set-export' ) . addEventListener ( 'click' , async ( ) => this . exportQrSet ( ) ) ;
this . dom . querySelector ( '#qr--set-delete' ) . addEventListener ( 'click' , async ( ) => this . deleteQrSet ( ) ) ;
2023-12-20 13:40:44 +00:00
this . dom . querySelector ( '#qr--set-add' ) . addEventListener ( 'click' , async ( ) => {
this . currentQrSet . addQuickReply ( ) ;
} ) ;
2024-07-10 17:34:48 -04:00
this . dom . querySelector ( '#qr--set-paste' ) . addEventListener ( 'click' , async ( ) => {
const text = await navigator . clipboard . readText ( ) ;
2024-07-18 19:47:35 -04:00
this . currentQrSet . addQuickReplyFromText ( text ) ;
2024-07-10 17:34:48 -04:00
} ) ;
this . dom . querySelector ( '#qr--set-importQr' ) . addEventListener ( 'click' , async ( ) => {
const inp = document . createElement ( 'input' ) ; {
inp . type = 'file' ;
inp . accept = '.json' ;
inp . addEventListener ( 'change' , async ( ) => {
if ( inp . files . length > 0 ) {
for ( const file of inp . files ) {
const text = await file . text ( ) ;
this . currentQrSet . addQuickReply ( JSON . parse ( text ) ) ;
}
}
} ) ;
inp . click ( ) ;
}
} ) ;
2023-12-20 13:40:44 +00:00
this . qrList = this . dom . querySelector ( '#qr--set-qrList' ) ;
this . currentSet = this . dom . querySelector ( '#qr--set' ) ;
this . currentSet . addEventListener ( 'change' , ( ) => this . onQrSetChange ( ) ) ;
2024-07-16 11:00:59 -04:00
QuickReplySet . list . toSorted ( ( a , b ) => a . name . toLowerCase ( ) . localeCompare ( b . name . toLowerCase ( ) ) ) . forEach ( qrs => {
2023-12-20 13:40:44 +00:00
const opt = document . createElement ( 'option' ) ; {
opt . value = qrs . name ;
opt . textContent = qrs . name ;
this . currentSet . append ( opt ) ;
}
} ) ;
this . disableSend = this . dom . querySelector ( '#qr--disableSend' ) ;
this . disableSend . addEventListener ( 'click' , ( ) => {
const qrs = this . currentQrSet ;
qrs . disableSend = this . disableSend . checked ;
qrs . save ( ) ;
} ) ;
this . placeBeforeInput = this . dom . querySelector ( '#qr--placeBeforeInput' ) ;
this . placeBeforeInput . addEventListener ( 'click' , ( ) => {
const qrs = this . currentQrSet ;
qrs . placeBeforeInput = this . placeBeforeInput . checked ;
qrs . save ( ) ;
} ) ;
this . injectInput = this . dom . querySelector ( '#qr--injectInput' ) ;
this . injectInput . addEventListener ( 'click' , ( ) => {
const qrs = this . currentQrSet ;
qrs . injectInput = this . injectInput . checked ;
qrs . save ( ) ;
} ) ;
2024-07-15 18:07:36 -04:00
let initialColorChange = true ;
2024-07-13 14:45:35 -04:00
this . color = this . dom . querySelector ( '#qr--color' ) ;
2024-07-16 15:39:07 -04:00
this . color . color = this . currentQrSet ? . color ? ? 'transparent' ;
2024-07-13 14:45:35 -04:00
this . color . addEventListener ( 'change' , ( evt ) => {
2024-07-16 15:39:07 -04:00
if ( ! this . dom . closest ( 'body' ) ) return ;
2024-07-13 14:45:35 -04:00
const qrs = this . currentQrSet ;
2024-07-15 18:07:36 -04:00
if ( initialColorChange ) {
initialColorChange = false ;
this . color . color = qrs . color ;
return ;
}
2024-07-13 14:45:35 -04:00
qrs . color = evt . detail . rgb ;
qrs . save ( ) ;
this . currentQrSet . updateColor ( ) ;
} ) ;
2024-07-14 14:13:57 -04:00
this . dom . querySelector ( '#qr--colorClear' ) . addEventListener ( 'click' , ( evt ) => {
const qrs = this . currentQrSet ;
this . color . color = 'transparent' ;
qrs . save ( ) ;
this . currentQrSet . updateColor ( ) ;
} ) ;
2024-07-13 14:45:35 -04:00
this . onlyBorderColor = this . dom . querySelector ( '#qr--onlyBorderColor' ) ;
this . onlyBorderColor . addEventListener ( 'click' , ( ) => {
const qrs = this . currentQrSet ;
qrs . onlyBorderColor = this . onlyBorderColor . checked ;
qrs . save ( ) ;
this . currentQrSet . updateColor ( ) ;
} ) ;
2023-12-20 13:40:44 +00:00
this . onQrSetChange ( ) ;
}
onQrSetChange ( ) {
2024-06-23 01:34:10 +03:00
this . currentQrSet = QuickReplySet . get ( this . currentSet . value ) ? ? new QuickReplySet ( ) ;
2023-12-20 13:40:44 +00:00
this . disableSend . checked = this . currentQrSet . disableSend ;
this . placeBeforeInput . checked = this . currentQrSet . placeBeforeInput ;
this . injectInput . checked = this . currentQrSet . injectInput ;
2024-07-16 15:39:07 -04:00
this . color . color = this . currentQrSet . color ? ? 'transparent' ;
2024-07-13 14:45:35 -04:00
this . onlyBorderColor . checked = this . currentQrSet . onlyBorderColor ;
2023-12-20 13:40:44 +00:00
this . qrList . innerHTML = '' ;
const qrsDom = this . currentQrSet . renderSettings ( ) ;
this . qrList . append ( qrsDom ) ;
// @ts-ignore
$ ( qrsDom ) . sortable ( {
delay : getSortableDelay ( ) ,
2024-01-06 18:21:08 +00:00
handle : '.drag-handle' ,
2023-12-20 13:40:44 +00:00
stop : ( ) => this . onQrListSort ( ) ,
} ) ;
}
prepareDom ( ) {
this . prepareGeneralSettings ( ) ;
this . prepareGlobalSetList ( ) ;
this . prepareChatSetList ( ) ;
this . prepareQrEditor ( ) ;
}
async onIsEnabled ( ) {
this . settings . isEnabled = this . isEnabled . checked ;
this . settings . save ( ) ;
}
async onIsCombined ( ) {
this . settings . isCombined = this . isCombined . checked ;
this . settings . save ( ) ;
}
2024-08-20 23:06:36 +03:00
async onShowPopoutButton ( ) {
this . settings . showPopoutButton = this . showPopoutButton . checked ;
this . settings . save ( ) ;
}
2023-12-20 13:40:44 +00:00
async onGlobalSetListSort ( ) {
this . settings . config . setList = Array . from ( this . globalSetList . children ) . map ( ( it , idx ) => {
const set = this . settings . config . setList [ Number ( it . getAttribute ( 'data-order' ) ) ] ;
it . setAttribute ( 'data-order' , String ( idx ) ) ;
return set ;
} ) ;
this . settings . save ( ) ;
}
async onChatSetListSort ( ) {
this . settings . chatConfig . setList = Array . from ( this . chatSetList . children ) . map ( ( it , idx ) => {
const set = this . settings . chatConfig . setList [ Number ( it . getAttribute ( 'data-order' ) ) ] ;
it . setAttribute ( 'data-order' , String ( idx ) ) ;
return set ;
} ) ;
this . settings . save ( ) ;
}
updateOrder ( list ) {
Array . from ( list . children ) . forEach ( ( it , idx ) => {
it . setAttribute ( 'data-order' , idx ) ;
} ) ;
}
async onQrListSort ( ) {
this . currentQrSet . qrList = Array . from ( this . qrList . querySelectorAll ( '.qr--set-item' ) ) . map ( ( it , idx ) => {
const qr = this . currentQrSet . qrList . find ( qr => qr . id == Number ( it . getAttribute ( 'data-id' ) ) ) ;
it . setAttribute ( 'data-order' , String ( idx ) ) ;
return qr ;
} ) ;
this . currentQrSet . save ( ) ;
}
2023-12-20 14:04:28 +00:00
2023-12-20 15:49:27 +00:00
async deleteQrSet ( ) {
2025-01-11 01:59:38 +01:00
const confirmed = await Popup . show . confirm ( 'Delete Quick Reply Set' , ` Are you sure you want to delete the Quick Reply Set " ${ this . currentQrSet . name } "?<br>This cannot be undone. ` ) ;
2023-12-20 15:49:27 +00:00
if ( confirmed ) {
2023-12-20 18:12:54 +00:00
await this . doDeleteQrSet ( this . currentQrSet ) ;
this . rerender ( ) ;
}
}
async doDeleteQrSet ( qrs ) {
await qrs . delete ( ) ;
//TODO (HACK) should just bubble up from QuickReplySet.delete() but that would require proper or at least more comples onDelete listeners
for ( let i = this . settings . config . setList . length - 1 ; i >= 0 ; i -- ) {
if ( this . settings . config . setList [ i ] . set == qrs ) {
this . settings . config . setList . splice ( i , 1 ) ;
2023-12-20 18:05:14 +00:00
}
2023-12-20 18:12:54 +00:00
}
if ( this . settings . chatConfig ) {
for ( let i = this . settings . chatConfig . setList . length - 1 ; i >= 0 ; i -- ) {
2023-12-26 12:16:45 +00:00
if ( this . settings . chatConfig . setList [ i ] . set == qrs ) {
this . settings . chatConfig . setList . splice ( i , 1 ) ;
2023-12-20 18:05:14 +00:00
}
}
2023-12-20 15:49:27 +00:00
}
2023-12-20 18:12:54 +00:00
this . settings . save ( ) ;
2023-12-20 15:49:27 +00:00
}
2025-01-11 01:41:46 +01:00
async renameQrSet ( ) {
2025-01-11 01:59:38 +01:00
const newName = await Popup . show . input ( 'Rename Quick Reply Set' , 'Enter a new name:' , this . currentQrSet . name ) ;
2025-01-11 01:41:46 +01:00
if ( newName && newName . length > 0 ) {
const existingSet = QuickReplySet . get ( newName ) ;
if ( existingSet ) {
toastr . error ( ` A Quick Reply Set named " ${ newName } " already exists. ` ) ;
return ;
}
const oldName = this . currentQrSet . name ;
this . currentQrSet . name = newName ;
await this . currentQrSet . save ( ) ;
// Update the option in all select dropdowns
/** @type {NodeListOf<HTMLOptionElement>} */
const options = this . dom . querySelectorAll ( ` #qr--set option[value=" ${ oldName } "], select.qr--set option[value=" ${ oldName } "] ` ) ;
options . forEach ( option => {
option . value = newName ;
option . textContent = newName ;
} ) ;
// Update in in both set lists
this . settings . config . setList . forEach ( set => {
if ( set . set . name === oldName ) {
set . set . name = newName ;
}
} ) ;
this . settings . chatConfig ? . setList . forEach ( set => {
if ( set . set . name === oldName ) {
set . set . name = newName ;
}
} ) ;
this . settings . save ( ) ;
this . currentSet . value = newName ;
console . info ( ` Quick Reply Set renamed from "" ${ oldName } " to " ${ newName } ". ` ) ;
}
}
2023-12-20 14:04:28 +00:00
async addQrSet ( ) {
2025-01-11 01:59:38 +01:00
const name = await Popup . show . input ( 'Create a new World Info' , 'Enter a name for the new Quick Reply Set:' ) ;
2023-12-20 14:04:28 +00:00
if ( name && name . length > 0 ) {
const oldQrs = QuickReplySet . get ( name ) ;
if ( oldQrs ) {
2025-01-11 01:59:38 +01:00
const replace = Popup . show . confirm ( 'Replace existing World Info' , ` A Quick Reply Set named " ${ name } " already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone. ` ) ;
2023-12-20 14:04:28 +00:00
if ( replace ) {
2023-12-20 18:12:54 +00:00
const idx = QuickReplySet . list . indexOf ( oldQrs ) ;
await this . doDeleteQrSet ( oldQrs ) ;
2023-12-20 14:04:28 +00:00
const qrs = new QuickReplySet ( ) ;
qrs . name = name ;
qrs . addQuickReply ( ) ;
2023-12-20 18:12:54 +00:00
QuickReplySet . list . splice ( idx , 0 , qrs ) ;
this . rerender ( ) ;
2023-12-20 14:04:28 +00:00
this . currentSet . value = name ;
this . onQrSetChange ( ) ;
2023-12-20 17:56:08 +00:00
this . prepareGlobalSetList ( ) ;
this . prepareChatSetList ( ) ;
2023-12-20 14:04:28 +00:00
}
} else {
const qrs = new QuickReplySet ( ) ;
qrs . name = name ;
qrs . addQuickReply ( ) ;
2024-07-16 11:00:59 -04:00
const idx = QuickReplySet . list . findIndex ( it => it . name . toLowerCase ( ) . localeCompare ( name . toLowerCase ( ) ) == 1 ) ;
2023-12-20 17:56:08 +00:00
if ( idx > - 1 ) {
QuickReplySet . list . splice ( idx , 0 , qrs ) ;
} else {
QuickReplySet . list . push ( qrs ) ;
}
2023-12-20 14:04:28 +00:00
const opt = document . createElement ( 'option' ) ; {
opt . value = qrs . name ;
opt . textContent = qrs . name ;
2023-12-20 17:56:08 +00:00
if ( idx > - 1 ) {
this . currentSet . children [ idx ] . insertAdjacentElement ( 'beforebegin' , opt ) ;
} else {
this . currentSet . append ( opt ) ;
}
2023-12-20 14:04:28 +00:00
}
this . currentSet . value = name ;
this . onQrSetChange ( ) ;
2023-12-20 17:56:08 +00:00
this . prepareGlobalSetList ( ) ;
this . prepareChatSetList ( ) ;
2023-12-20 14:04:28 +00:00
}
}
}
2023-12-20 15:49:27 +00:00
2023-12-25 13:07:06 +00:00
async importQrSet ( /**@type {FileList}*/ files ) {
for ( let i = 0 ; i < files . length ; i ++ ) {
await this . importSingleQrSet ( files . item ( i ) ) ;
}
}
async importSingleQrSet ( /**@type {File}*/ file ) {
log ( 'FILE' , file ) ;
try {
const text = await file . text ( ) ;
const props = JSON . parse ( text ) ;
if ( ! Number . isInteger ( props . version ) || typeof props . name != 'string' ) {
toastr . error ( ` The file " ${ file . name } " does not appear to be a valid quick reply set. ` ) ;
warn ( ` The file " ${ file . name } " does not appear to be a valid quick reply set. ` ) ;
} else {
/**@type {QuickReplySet}*/
const qrs = QuickReplySet . from ( JSON . parse ( JSON . stringify ( props ) ) ) ;
qrs . qrList = props . qrList . map ( it => QuickReply . from ( it ) ) ;
qrs . init ( ) ;
const oldQrs = QuickReplySet . get ( props . name ) ;
if ( oldQrs ) {
2025-01-11 01:59:38 +01:00
const replace = Popup . show . confirm ( 'Replace existing World Info' , ` A Quick Reply Set named " ${ name } " already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone. ` ) ;
2023-12-25 13:07:06 +00:00
if ( replace ) {
const idx = QuickReplySet . list . indexOf ( oldQrs ) ;
await this . doDeleteQrSet ( oldQrs ) ;
QuickReplySet . list . splice ( idx , 0 , qrs ) ;
await qrs . save ( ) ;
this . rerender ( ) ;
this . currentSet . value = qrs . name ;
this . onQrSetChange ( ) ;
this . prepareGlobalSetList ( ) ;
this . prepareChatSetList ( ) ;
}
} else {
2024-07-16 11:00:59 -04:00
const idx = QuickReplySet . list . findIndex ( it => it . name . toLowerCase ( ) . localeCompare ( qrs . name . toLowerCase ( ) ) == 1 ) ;
2023-12-25 13:07:06 +00:00
if ( idx > - 1 ) {
QuickReplySet . list . splice ( idx , 0 , qrs ) ;
} else {
QuickReplySet . list . push ( qrs ) ;
}
await qrs . save ( ) ;
const opt = document . createElement ( 'option' ) ; {
opt . value = qrs . name ;
opt . textContent = qrs . name ;
if ( idx > - 1 ) {
this . currentSet . children [ idx ] . insertAdjacentElement ( 'beforebegin' , opt ) ;
} else {
this . currentSet . append ( opt ) ;
}
}
this . currentSet . value = qrs . name ;
this . onQrSetChange ( ) ;
this . prepareGlobalSetList ( ) ;
this . prepareChatSetList ( ) ;
}
}
} catch ( ex ) {
warn ( ex ) ;
toastr . error ( ` Failed to import " ${ file . name } ": \n \n ${ ex . message } ` ) ;
}
}
exportQrSet ( ) {
const blob = new Blob ( [ JSON . stringify ( this . currentQrSet ) ] , { type : 'application/json' } ) ;
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ; {
a . href = url ;
a . download = ` ${ this . currentQrSet . name } .json ` ;
a . click ( ) ;
}
2024-05-22 01:36:38 +03:00
URL . revokeObjectURL ( url ) ;
2023-12-25 13:07:06 +00:00
}
2023-12-20 15:49:27 +00:00
selectQrSet ( qrs ) {
this . currentSet . value = qrs . name ;
this . onQrSetChange ( ) ;
}
2023-12-20 13:40:44 +00:00
}