@@ -830,22 +830,95 @@ function configureUseButton(id, { active = false, disabled = false, text = '', t
830830 button . title = title || '' ;
831831}
832832
833+ function setActionButtonDisabled ( button , disabled ) {
834+ const nextDisabled = Boolean ( disabled ) ;
835+ if ( button . disabled !== nextDisabled ) {
836+ button . disabled = nextDisabled ;
837+ }
838+ button . __galgameActionDesiredDisabled = nextDisabled ;
839+ }
840+
841+ function syncActionButtonElement ( current , next ) {
842+ const nextDisabled = next . hasAttribute ( 'disabled' ) ;
843+ const attributeNames = new Set ( [
844+ ...Array . from ( current . attributes , ( attr ) => attr . name ) ,
845+ ...Array . from ( next . attributes , ( attr ) => attr . name ) ,
846+ ] ) ;
847+ attributeNames . forEach ( ( name ) => {
848+ if ( name === 'disabled' ) {
849+ return ;
850+ }
851+ const nextValue = next . getAttribute ( name ) ;
852+ if ( nextValue == null ) {
853+ current . removeAttribute ( name ) ;
854+ } else if ( current . getAttribute ( name ) !== nextValue ) {
855+ current . setAttribute ( name , nextValue ) ;
856+ }
857+ } ) ;
858+ if ( current . textContent !== next . textContent ) {
859+ current . textContent = next . textContent ;
860+ }
861+ setActionButtonDisabled ( current , nextDisabled ) ;
862+ }
863+
864+ function canPatchActionButtons ( currentChildren , nextChildren ) {
865+ return currentChildren . length === nextChildren . length
866+ && nextChildren . every ( ( next , index ) => {
867+ const current = currentChildren [ index ] ;
868+ return current
869+ && current . tagName === next . tagName
870+ && ( current . id || '' ) === ( next . id || '' )
871+ && ( current . getAttribute ( 'data-primary-action' ) || '' ) === ( next . getAttribute ( 'data-primary-action' ) || '' ) ;
872+ } ) ;
873+ }
874+
875+ function syncActionButtons ( actions , html ) {
876+ const nextHtml = html || '' ;
877+ if ( ! actions || actions . dataset . renderedHtml === nextHtml ) {
878+ return ;
879+ }
880+ const template = document . createElement ( 'template' ) ;
881+ template . innerHTML = nextHtml . trim ( ) ;
882+ const currentChildren = Array . from ( actions . children ) ;
883+ const nextChildren = Array . from ( template . content . children ) ;
884+ if ( ! canPatchActionButtons ( currentChildren , nextChildren ) ) {
885+ actions . innerHTML = nextHtml ;
886+ actions . dataset . renderedHtml = nextHtml ;
887+ Array . from ( actions . children ) . forEach ( ( button ) => {
888+ if ( button instanceof HTMLButtonElement ) {
889+ setActionButtonDisabled ( button , button . disabled ) ;
890+ }
891+ } ) ;
892+ return ;
893+ }
894+ currentChildren . forEach ( ( current , index ) => {
895+ syncActionButtonElement ( current , nextChildren [ index ] ) ;
896+ } ) ;
897+ actions . dataset . renderedHtml = nextHtml ;
898+ }
899+
833900function rebindCardButton ( id , handler ) {
834901 const button = document . getElementById ( id ) ;
835902 if ( ! button ) {
836903 return ;
837904 }
838- const clone = button . cloneNode ( true ) ;
839- button . parentNode . replaceChild ( clone , button ) ;
840- clone . addEventListener ( 'click' , async ( ) => {
841- if ( clone . disabled ) {
905+ button . __galgameCardClickHandler = handler ;
906+ if ( button . __galgameCardClickBound ) {
907+ return ;
908+ }
909+ button . __galgameCardClickBound = true ;
910+ button . addEventListener ( 'click' , async ( ) => {
911+ if ( button . disabled ) {
842912 return ;
843913 }
844- clone . disabled = true ;
914+ button . disabled = true ;
845915 try {
846- await handler ( ) ;
916+ const currentHandler = button . __galgameCardClickHandler ;
917+ if ( typeof currentHandler === 'function' ) {
918+ await currentHandler ( ) ;
919+ }
847920 } finally {
848- clone . disabled = false ;
921+ button . disabled = Boolean ( button . __galgameActionDesiredDisabled ) ;
849922 }
850923 } ) ;
851924}
@@ -1474,11 +1547,11 @@ function renderPrimaryDiagnosis(status = {}) {
14741547 kicker . textContent = uiT ( 'ui.diag.kicker' , '运行诊断' ) ;
14751548 title . textContent = diagnosis . title ;
14761549 body . textContent = diagnosis . body ;
1477- actions . innerHTML = ( diagnosis . actions || [ ] ) . map ( ( action , index ) => `
1550+ syncActionButtons ( actions , ( diagnosis . actions || [ ] ) . map ( ( action , index ) => `
14781551 <button class="${ index === 0 ? 'primary' : 'secondary' } " data-primary-action="${ escapeHtml ( action . id ) } ">
14791552 ${ escapeHtml ( primaryActionLabel ( action . id , action . label ) ) }
14801553 </button>
1481- ` ) . join ( '' ) ;
1554+ ` ) . join ( '' ) ) ;
14821555}
14831556
14841557function buildFirstRunStepsLegacy ( status = { } ) {
@@ -3355,7 +3428,7 @@ function renderInstallTaskState(kind) {
33553428 card . style . display = '' ;
33563429 if ( button ) {
33573430 button . hidden = false ;
3358- button . disabled = false ;
3431+ setActionButtonDisabled ( button , false ) ;
33593432 }
33603433 statusText . textContent = uiTf ( 'ui.install.task.waiting' , '等待 {label} 安装任务' , { label } ) ;
33613434 percentText . textContent = '0%' ;
@@ -3396,15 +3469,15 @@ function renderInstallTaskState(kind) {
33963469 if ( button ) {
33973470 const terminalCompleted = ! rapidocrModelsStillMissing ;
33983471 button . hidden = terminalCompleted ;
3399- button . disabled = terminalCompleted ;
3472+ setActionButtonDisabled ( button , terminalCompleted ) ;
34003473 if ( rapidocrModelsStillMissing ) {
34013474 button . textContent = getInstallConfig ( kind ) . retryText ;
34023475 }
34033476 }
34043477 } else if ( state . status === 'failed' ) {
34053478 if ( button ) {
34063479 button . hidden = false ;
3407- button . disabled = false ;
3480+ setActionButtonDisabled ( button , false ) ;
34083481 button . textContent = getInstallConfig ( kind ) . retryText ;
34093482 }
34103483 }
@@ -3465,7 +3538,7 @@ function renderPluginUnavailable(error) {
34653538 if ( chip ) chip . textContent = pluginNotStarted ;
34663539 if ( desc ) desc . textContent = uiT ( 'ui.install.plugin_unavailable_body' , '当前无法读取插件运行状态。请先启动或重载 galgame_plugin,启动完成后这里会显示安装和运行时状态。' ) ;
34673540 if ( meta ) meta . textContent = `${ PROMPT_LABELS [ kind ] } · ${ message } ` ;
3468- if ( actions ) actions . innerHTML = '' ;
3541+ if ( actions ) syncActionButtons ( actions , '' ) ;
34693542 if ( kind === 'rapidocr' ) {
34703543 const card = document . getElementById ( 'rapidocrInstallCard' ) ;
34713544 if ( card ) {
@@ -4960,7 +5033,7 @@ function renderRapidOcr(status) {
49605033 chip . textContent = chipText ;
49615034 desc . textContent = descText ;
49625035 meta . textContent = metaText ;
4963- actions . innerHTML = buttons . join ( '' ) ;
5036+ syncActionButtons ( actions , buttons . join ( '' ) ) ;
49645037 renderInstallTaskState ( 'rapidocr_models' ) ;
49655038 rebindCardButton ( 'rapidocrUseBtn' , ( ) => setOcrBackendSelection ( { backendSelection : 'rapidocr' } ) ) ;
49665039 rebindCardButton ( 'ocrBackendAutoBtn' , ( ) => setOcrBackendSelection ( { backendSelection : 'auto' } ) ) ;
@@ -4992,14 +5065,14 @@ function renderDxcam(status) {
49925065 ? selectedCaptureBackend === 'mss' || selectedCaptureBackend === 'imagegrab'
49935066 : selectedCaptureBackend === value
49945067 ) ;
4995- actions . innerHTML = [
5068+ syncActionButtons ( actions , [
49965069 `<button id="smartCaptureUseBtn" class="secondary" ${ captureActive ( 'smart' ) ? 'disabled' : '' } >${ escapeHtml ( captureActive ( 'smart' ) ? uiT ( 'ui.install.smart.using' , '正在使用 Smart' ) : uiT ( 'ui.install.smart.use' , '使用 Smart' ) ) } </button>` ,
49975070 `<button id="dxcamUseBtn" class="secondary" ${ ( ! installed || captureActive ( 'dxcam' ) ) ? 'disabled' : '' } >${ escapeHtml ( captureActive ( 'dxcam' ) ? uiT ( 'ui.install.dxcam.using' , '正在使用 DXcam' ) : uiT ( 'ui.install.dxcam.use' , '使用 DXcam' ) ) } </button>` ,
49985071 `<button id="captureBackendAutoBtn" class="ghost" ${ captureActive ( 'auto' ) ? 'disabled' : '' } >${ escapeHtml ( captureActive ( 'auto' ) ? uiT ( 'ui.install.capture_auto.using' , '截图自动选择中' ) : uiT ( 'ui.install.capture_auto' , '截图自动' ) ) } </button>` ,
49995072 `<button id="mssUseBtn" class="ghost" ${ captureActive ( 'mss' ) ? 'disabled' : '' } >${ escapeHtml ( captureActive ( 'mss' ) ? uiT ( 'ui.install.mss.using' , '正在使用 MSS' ) : uiT ( 'ui.install.mss.use' , '使用 MSS' ) ) } </button>` ,
50005073 `<button id="pyautoguiUseBtn" class="ghost" ${ captureActive ( 'pyautogui' ) ? 'disabled' : '' } >${ escapeHtml ( captureActive ( 'pyautogui' ) ? uiT ( 'ui.install.pyautogui.using' , '正在使用 PyAutoGUI' ) : uiT ( 'ui.install.pyautogui.use' , '使用 PyAutoGUI' ) ) } </button>` ,
50015074 `<button id="printwindowUseBtn" class="ghost" ${ captureActive ( 'printwindow' ) ? 'disabled' : '' } >${ escapeHtml ( captureActive ( 'printwindow' ) ? uiT ( 'ui.install.printwindow.using' , '正在使用 PrintWindow' ) : uiT ( 'ui.install.printwindow.use' , '使用 PrintWindow' ) ) } </button>` ,
5002- ] . join ( '' ) ;
5075+ ] . join ( '' ) ) ;
50035076
50045077 if ( ! dxcam . install_supported ) {
50055078 card . className = 'install-card neutral' ;
@@ -5094,7 +5167,7 @@ function renderTesseract(status) {
50945167 chip . textContent = chipText ;
50955168 desc . textContent = descText ;
50965169 meta . textContent = metaText ;
5097- actions . innerHTML = buttons . join ( '' ) ;
5170+ syncActionButtons ( actions , buttons . join ( '' ) ) ;
50985171 if ( installed ) {
50995172 const nodes = getInstallNodes ( 'tesseract' ) ;
51005173 if ( nodes . card ) nodes . card . hidden = true ;
@@ -5160,7 +5233,7 @@ function renderTextractor(status) {
51605233 chip . textContent = chipText ;
51615234 desc . textContent = descText ;
51625235 meta . textContent = metaText ;
5163- actions . innerHTML = installButtonHtml ( 'textractor' , installable , installed ) ;
5236+ syncActionButtons ( actions , installButtonHtml ( 'textractor' , installable , installed ) ) ;
51645237 if ( installed ) {
51655238 const nodes = getInstallNodes ( 'textractor' ) ;
51665239 if ( nodes . card ) nodes . card . hidden = true ;
0 commit comments