11<!DOCTYPE html>
22< html lang ="en ">
3+
34< head >
45 < meta charset ="UTF-8 ">
56 < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
1112 --background-color : # 000000 ;
1213 --text-color : # ffffff ;
1314 --border-radius : 4px ;
14- --box-shadow : 0 2px 4px rgba (0 , 0 , 0 , 0.3 );
15+ --box-shadow : 0 2px 4px rgba (0 , 0 , 0 , 0.3 );
1516 }
1617
1718 * {
212213 border : none;
213214 }
214215
215- .quality-value {
216+ input [type = "range" ]: disabled {
217+ background : color-mix (in srgb, var (--background-color ), white 25% );
218+ ;
219+ }
220+
221+ input [type = "range" ]: disabled ::-webkit-slider-thumb {
222+ background : color-mix (in srgb, var (--primary-color ), black 75% );
223+ ;
224+ cursor : not - allowed;
225+ }
226+
227+ input [type = "range" ]: disabled ::-moz-range-thumb {
228+ background : color-mix (in srgb, var (--primary-color ), black 75% );
229+ ;
230+ cursor : not - allowed;
231+ }
232+
233+ .quality-value ,
234+ .ledBrightness-value ,
235+ .ledEffectSpeed-value {
216236 text-align : center;
217237 font-size : 0.9rem ;
218238 color : var (--primary-color );
431451 color : var (--primary-color );
432452 }
433453
434- .form-select {
454+ .form-select ,
455+ .form-text {
435456 width : 100% ;
436457 padding : 8px ;
437458 border : 1px solid var (--primary-color );
442463 cursor : pointer;
443464 }
444465
445- .form-select : hover {
446- background : rgba (0 , 255 , 0 , 0.1 );
466+ .form-select : hover ,
467+ .form-text : hover {
468+ background : rgba (153 , 0 , 255 , 0.1 );
447469 }
448470
449- .form-select : focus {
471+ .form-select : focus ,
472+ .form-text : focus {
450473 outline : none;
451474 border-color : var (--primary-color );
452475 box-shadow : 0 0 10px var (--primary-color );
480503 }
481504 </ style >
482505</ head >
506+
483507< body >
484508 < div class ="container ">
485509 < header class ="header ">
@@ -498,19 +522,30 @@ <h1>Bruce Theme Builder</h1>
498522
499523 < div class ="controls-section ">
500524 < div class ="control-group ">
525+ < div class ="control-item ">
526+ < label for ="themeNameInput "> Theme Name:</ label >
527+ < input type ="text " id ="themeNameInput " class ="form-text " />
528+ </ div >
501529 < div class ="control-item ">
502530 < label for ="heightInput "> Target Device:</ label >
503- < select id ="heightInput " class ="form-select ">
504- < option value ="0 "> All devices</ option >
531+ < select id ="heightInput " class ="form-select "
532+ onchange ="document.getElementById('heightInputCustom').parentElement.style.display = this.value === 'CUSTOM' ? 'block' : 'none'; ">
533+ < option value ="105,140,180,192 "> All devices</ option >
505534 < option value ="105 "> StickCPlus and Cardputer (105px)</ option >
506535 < option value ="140 "> Lilygo T-Embed (140px)</ option >
507536 < option value ="180 "> CYD, Core, T-Deck (180px)</ option >
508- < option value ="222 "> T-LoRa Pager (222px)</ option >
537+ < option value ="192 "> T-LoRa-Pager (192px)</ option >
538+ < option value ="CUSTOM "> Custom</ option >
509539 </ select >
510540 </ div >
541+ < div class ="control-item " style ="display: none; ">
542+ < label for ="heightInputCustom "> Custom Height(s):</ label >
543+ < input type ="text " id ="heightInputCustom " class ="form-text " value ="105,140,180,222 " />
544+ </ div >
511545 < div class ="control-item ">
512546 < label for ="qualityInput "> Quality:</ label >
513- < input type ="range " id ="qualityInput " min ="0 " max ="1 " step ="0.1 " value ="0.8 " oninput ="updateQualityValue(this.value) ">
547+ < input type ="range " id ="qualityInput " min ="0 " max ="1 " step ="0.1 " value ="0.8 "
548+ oninput ="updateQualityValue(this.value) ">
514549 < div id ="qualityValue " class ="quality-value "> 0.8</ div >
515550 </ div >
516551 </ div >
@@ -541,6 +576,49 @@ <h1>Bruce Theme Builder</h1>
541576 </ div >
542577 </ div >
543578
579+ < div class ="controls-section ">
580+ < div class ="control-group ">
581+ < div class ="control-item ">
582+ < label for ="ledColor "> LED Color:</ label >
583+ < input type ="color " id ="ledColor " value ="#ad007b ">
584+ </ div >
585+ < div class ="control-item ">
586+ < label for ="ledBrightnessInput "> LED Brightness:</ label >
587+ < input type ="range " id ="ledBrightnessInput " min ="0 " max ="100 " step ="5 " value ="50 "
588+ oninput ="updateLEDBrightnessValue(this.value) ">
589+ < div id ="ledBrightnessValue " class ="ledBrightness-value "> 50</ div >
590+ </ div >
591+ </ div >
592+ < div class ="control-group ">
593+ < div class ="control-item ">
594+
595+ < label for ="ledEffectInput "> LED Effect:</ label >
596+ < select id ="ledEffectInput " class ="form-select " onchange ="updateLEDEffectControls(this.value) ">
597+ < option value ="0 "> Solid Color</ option >
598+ < option value ="1 "> Breathe</ option >
599+ < option value ="2 "> Color Cycle</ option >
600+ < option value ="3 "> Color Wheel</ option >
601+ < option value ="4 "> Chase</ option >
602+ < option value ="5 "> Chase Tail</ option >
603+ </ select >
604+ </ div >
605+ < div class ="control-item ">
606+ < label for ="ledEffectSpeedInput "> Effect Speed:</ label >
607+ < input type ="range " id ="ledEffectSpeedInput " min ="0 " max ="10 " step ="1 " value ="5 "
608+ oninput ="updateLEDEffectSpeedValue(this.value) " disabled >
609+ < div id ="ledEffectSpeedValue " class ="ledEffectSpeed-value "> 5</ div >
610+ </ div >
611+ < div class ="border-control ">
612+ < input type ="checkbox " id ="ledEffectDirection " disabled >
613+ < label for ="ledEffectDirection "> Reverse Effect Direction</ label >
614+ </ div >
615+ < div class ="border-control ">
616+ < input type ="checkbox " id ="ledSyncToEncoder " disabled >
617+ < label for ="ledSyncToEncoder "> Sync Effect to Encoder Wheel (where available)</ label >
618+ </ div >
619+ </ div >
620+ </ div >
621+
544622 < div class ="bulk-upload ">
545623 < div class ="bulk-drop-zone " id ="bulkDropZone ">
546624 < div class ="bulk-upload-text ">
@@ -553,12 +631,12 @@ <h1>Bruce Theme Builder</h1>
553631
554632 < div class ="image-inputs " id ="imageInputs "> </ div >
555633
556- < button onclick ="resizeImages () "> Resize and Download</ button >
634+ < button onclick ="generateAndDownload () "> Generate and Download</ button >
557635 </ div >
558636
559637 < script src ="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js "> </ script >
560638 < script >
561- const fileNames = [ "wifi" , "ble" , "rf" , "rfid" , "ir" , "fm" , "files" , "gps" , "nrf" , "interpreter" , "others" , "clock" , "connect" , "config" ] ;
639+ const fileNames = [ "wifi" , "ble" , "ethernet" , " rf", "rfid" , "ir" , "fm" , "files" , "gps" , "nrf" , "interpreter" , "others" , "clock" , "connect" , "config" ] ;
562640 //const fileNames = ["wifi", "ble", "rf"];
563641 const selectedImages = { } ;
564642 const placeholderUrl = "https://via.placeholder.com/150" ;
@@ -685,104 +763,140 @@ <h1>Bruce Theme Builder</h1>
685763 } ) ;
686764 }
687765
688- function rgbToRGB565 ( hex ) {
766+ function rgbToRGB565 ( hex ) {
689767 let r = parseInt ( hex . substring ( 1 , 3 ) , 16 ) >> 3 ;
690768 let g = parseInt ( hex . substring ( 3 , 5 ) , 16 ) >> 2 ;
691769 let b = parseInt ( hex . substring ( 5 , 7 ) , 16 ) >> 3 ;
692770 return ( ( r << 11 ) | ( g << 5 ) | b ) . toString ( 16 ) . padStart ( 4 , '0' ) ;
693771 }
694772
695- function resizeImages ( ) {
773+ async function downloadZip ( zip , themeName ) {
774+ const content = await zip . generateAsync ( { type : "blob" } ) ;
775+ const link = document . createElement ( 'a' ) ;
776+ link . href = URL . createObjectURL ( content ) ;
777+ link . download = `Theme_${ themeName } .zip` ;
778+ document . body . appendChild ( link ) ;
779+ link . click ( ) ;
780+ document . body . removeChild ( link ) ;
781+ }
782+
783+ function getCommonMapping ( ) {
784+ return {
785+ priColor : rgbToRGB565 ( document . getElementById ( 'priColor' ) . value ) ,
786+ secColor : rgbToRGB565 ( document . getElementById ( 'secColor' ) . value ) ,
787+ bgColor : rgbToRGB565 ( document . getElementById ( 'bgColor' ) . value ) ,
788+ ledBright : document . getElementById ( 'ledBrightnessInput' ) . value ,
789+ ledColor : document . getElementById ( 'ledColor' ) . value . substring ( 0 , 7 ) ,
790+ ledEffect : document . getElementById ( 'ledEffectInput' ) . value ,
791+ ledEffectSpeed : document . getElementById ( 'ledSyncToEncoder' ) . checked ? 11 : document . getElementById ( 'ledEffectSpeedInput' ) . value ,
792+ ledEffectDirection : document . getElementById ( 'ledEffectDirection' ) . checked ? - 1 : 1
793+ } ;
794+ }
795+
796+ async function generateAndDownload ( ) {
696797 const zip = new JSZip ( ) ;
697- const randomId = Math . floor ( 100 + Math . random ( ) * 900 ) ;
698- const targetHeight = parseInt ( document . getElementById ( 'heightInput' ) . value ) ;
798+ const themeName = document . getElementById ( "themeNameInput" ) . value . replace ( " " , "_" ) || "Theme_" + Math . floor ( 100 + Math . random ( ) * 900 ) ;
799+
800+ const heightInput = document . getElementById ( 'heightInput' ) . value . trim ( ) ;
801+ const customHeights = document . getElementById ( 'heightInputCustom' ) . value . trim ( ) ;
802+ const heightsToUse = heightInput === 'CUSTOM' ? customHeights : heightInput ;
803+ const heights = heightsToUse . split ( ',' ) . map ( h => parseInt ( h . trim ( ) ) ) . filter ( h => ! isNaN ( h ) && h > 0 ) ;
804+
699805 const quality = parseFloat ( document . getElementById ( 'qualityInput' ) . value ) ;
806+
700807 const hasLabels = document . getElementById ( 'labels' ) . checked ;
701808 const hasBorder = document . getElementById ( 'border' ) . checked ;
809+ const totalToProcess = heights . length * Object . keys ( selectedImages ) . length ;
810+
811+ if ( totalToProcess === 0 ) {
812+ zip . file ( `Theme_${ themeName } .json` , JSON . stringify ( getCommonMapping ( ) , null , 2 ) ) ;
813+ await downloadZip ( zip , themeName ) ;
814+ return ;
815+ }
702816
703- // Define as alturas que precisamos processar
704- const heights = targetHeight === 0 ? [ 105 , 140 , 180 ] : [ targetHeight ] ;
705817 let totalProcessed = 0 ;
706- const totalToProcess = heights . length * Object . keys ( selectedImages ) . length ;
707818
708- heights . forEach ( height => {
709- const folderName = `Theme_${ randomId } /${ height } px` ;
819+ for ( const height of heights ) {
820+ const folderName = `Theme_${ themeName } /${ height } px` ;
710821 const imageFolder = zip . folder ( folderName ) ;
711822 const jsonMapping = { } ;
712823
713- fileNames . forEach ( name => {
824+ await Promise . all ( fileNames . map ( async name => {
714825 if ( ! selectedImages [ name ] ) return ;
715826
716827 const file = selectedImages [ name ] ;
717828 const img = new Image ( ) ;
718- img . onload = function ( ) {
719- const canvas = document . createElement ( 'canvas' ) ;
720- const ctx = canvas . getContext ( '2d' ) ;
721-
722- // Ajusta a altura baseado no checkbox Labels
723- const effectiveHeight = hasLabels ? height - 40 : height ;
724-
725- // Só redimensiona se a imagem for maior que o alvo
726- let newWidth , newHeight ;
727- if ( img . height > effectiveHeight ) {
728- const aspectRatio = img . width / img . height ;
729- newWidth = Math . round ( effectiveHeight * aspectRatio ) ;
730- newHeight = effectiveHeight ;
731- } else {
732- newWidth = img . width ;
733- newHeight = img . height ;
734- }
829+ await new Promise ( resolve => {
830+ img . onload = async function ( ) {
831+ const canvas = document . createElement ( 'canvas' ) ;
832+ const ctx = canvas . getContext ( '2d' ) ;
833+ const effectiveHeight = hasLabels ? height - 40 : height ;
834+ let newWidth , newHeight ;
835+ if ( img . height > effectiveHeight ) {
836+ const aspectRatio = img . width / img . height ;
837+ newWidth = Math . round ( effectiveHeight * aspectRatio ) ;
838+ newHeight = effectiveHeight ;
839+ } else {
840+ newWidth = img . width ;
841+ newHeight = img . height ;
842+ }
843+ canvas . width = newWidth ;
844+ canvas . height = newHeight ;
845+ ctx . drawImage ( img , 0 , 0 , newWidth , newHeight ) ;
846+
847+ let fileType = file . type ;
848+ let fileExtension = fileType . split ( '/' ) [ 1 ] ;
849+ const baseName = file . name . substring ( 0 , file . name . lastIndexOf ( "." ) ) || file . name ;
850+ let imageDataUrl = canvas . toDataURL ( fileType , quality ) ;
851+
852+ const blob = await fetch ( imageDataUrl ) . then ( res => res . blob ( ) ) ;
853+ const fileName = `${ baseName } .${ fileExtension } ` ;
854+ jsonMapping [ name ] = fileName ;
855+ imageFolder . file ( fileName , blob ) ;
856+ totalProcessed ++ ;
857+ resolve ( ) ;
858+ } ;
859+ img . src = URL . createObjectURL ( file ) ;
860+ } ) ;
861+ } ) ) ;
862+
863+ if ( Object . keys ( selectedImages ) . length === Object . keys ( jsonMapping ) . length ) {
864+ Object . assign ( jsonMapping , {
865+ border : hasBorder ? 1 : 0 ,
866+ label : hasLabels ? 1 : 0 ,
867+ ...getCommonMapping ( )
868+ } ) ;
869+ imageFolder . file ( `Theme_${ themeName } .json` , JSON . stringify ( jsonMapping , null , 2 ) ) ;
870+ }
871+ }
735872
736- canvas . width = newWidth ;
737- canvas . height = newHeight ;
738- ctx . drawImage ( img , 0 , 0 , newWidth , newHeight ) ;
739-
740- let fileType = file . type ;
741- let fileExtension = fileType . split ( '/' ) [ 1 ] ;
742- const baseName = file . name . substring ( 0 , file . name . lastIndexOf ( "." ) ) || file . name ;
743- let imageDataUrl = canvas . toDataURL ( fileType , quality ) ;
744-
745- fetch ( imageDataUrl )
746- . then ( res => res . blob ( ) )
747- . then ( blob => {
748- const fileName = `${ baseName } .${ fileExtension } ` ;
749- jsonMapping [ name ] = fileName ;
750- imageFolder . file ( fileName , blob ) ;
751- totalProcessed ++ ;
752-
753- // Verifica se é o último arquivo desta altura
754- if ( Object . keys ( selectedImages ) . length === Object . keys ( jsonMapping ) . length ) {
755- jsonMapping [ "priColor" ] = rgbToRGB565 ( document . getElementById ( 'priColor' ) . value ) ;
756- jsonMapping [ "secColor" ] = rgbToRGB565 ( document . getElementById ( 'secColor' ) . value ) ;
757- jsonMapping [ "bgColor" ] = rgbToRGB565 ( document . getElementById ( 'bgColor' ) . value ) ;
758- jsonMapping [ "border" ] = hasBorder ? 1 : 0 ;
759- jsonMapping [ "label" ] = hasLabels ? 1 : 0 ;
760- imageFolder . file ( `Theme_${ randomId } .json` , JSON . stringify ( jsonMapping , null , 2 ) ) ;
761- }
762-
763- // Verifica se é o último arquivo de todos
764- if ( totalProcessed === totalToProcess ) {
765- zip . generateAsync ( { type : "blob" } ) . then ( content => {
766- // Cria um link temporário para download
767- const link = document . createElement ( 'a' ) ;
768- link . href = URL . createObjectURL ( content ) ;
769- link . download = 'theme.zip' ;
770- document . body . appendChild ( link ) ;
771- link . click ( ) ;
772- document . body . removeChild ( link ) ;
773- } ) ;
774- }
775- } ) ;
776- } ;
777- img . src = URL . createObjectURL ( file ) ;
778- } ) ;
779- } ) ;
873+ if ( totalProcessed === totalToProcess ) {
874+ await downloadZip ( zip , themeName ) ;
875+ }
780876 }
781877
782878 function updateQualityValue ( value ) {
783879 document . getElementById ( 'qualityValue' ) . textContent = value ;
784880 }
785881
882+ function updateLEDBrightnessValue ( value ) {
883+ document . getElementById ( 'ledBrightnessValue' ) . textContent = value ;
884+ }
885+
886+ function updateLEDEffectSpeedValue ( value ) {
887+ document . getElementById ( 'ledEffectSpeedValue' ) . textContent = value ;
888+ }
889+
890+ function updateLEDEffectControls ( value ) {
891+ const speedInput = document . getElementById ( 'ledEffectSpeedInput' ) ;
892+ const directionCheckbox = document . getElementById ( 'ledEffectDirection' ) ;
893+ const syncCheckbox = document . getElementById ( 'ledSyncToEncoder' ) ;
894+
895+ speedInput . disabled = value === "0" ;
896+ directionCheckbox . disabled = value === "0" ;
897+ syncCheckbox . disabled = value === "0" ;
898+ }
899+
786900 function setupDragAndDrop ( ) {
787901 // Setup para área de upload em massa
788902 const bulkDropZone = document . getElementById ( 'bulkDropZone' ) ;
@@ -901,4 +1015,5 @@ <h1>Bruce Theme Builder</h1>
9011015 updateColorDisplay ( ) ;
9021016 </ script >
9031017</ body >
1018+
9041019</ html >
0 commit comments