4343 # track-selector { margin-bottom : 20px ; }
4444 .text-center { text-align : center; }
4545 .accordion-header i { transition : transform 0.2s ; }
46- input : checked ~ .accordion-header i { transition : transform 0.2s ; }
46+ input [ name = accordion-tracks ] : checked ~ .accordion-header i { transition : transform 0.2s ; }
4747 .accordion { margin-bottom : 20px ; }
4848 .accordion-item { position : relative; }
49- .accordion-header { cursor : pointer; padding : 10px ; background : # f8f9fa ; border-radius : 4px ; margin-bottom : 5px ; }
49+ .accordion-header { cursor : pointer; padding : 10px ; background : # f8f9fa ; border-radius : 4px ; }
50+ /* only show margin on expanded entries */
51+ input [name = accordion-tracks ]: checked ~ .accordion-header { margin-bottom : 5px ; }
52+ input .select-checkbox : checked ~ div > .accordion-body { background : # f0f8ff ; }
5053 .accordion-header : hover { background : # e9ecef ; }
5154 .accordion-body { overflow : visible; }
5255 .track-content-wrapper {
5861 .track-loading { text-align : center; padding : 20px ; color : # 666 ; }
5962 html , body { height : auto; min-height : 100% ; }
6063 body { margin : 0 ; padding-bottom : 50px ; }
64+ .vertical {
65+ display : flex;
66+ flex-direction : column;
67+ }
68+ .horizontal {
69+ display : flex;
70+ flex-direction : row;
71+ }
72+ .hidden {
73+ display : none;
74+ }
6175 </ style >
6276 </ head >
6377 < body >
393407}
394408
395409function saveGPX ( track , title ) {
396- if ( ! track ?. [ 0 ] ?. Time ) return showToast ( "Error in trackfile." , "error" ) ;
397410 track = filterGPSCoordinates ( track ) ;
411+ // check track validity after filtering
412+ if ( ! track ?. [ 0 ] ?. Time ) return showToast ( "Error in trackfile (no time or empty)." , "error" ) ;
398413 let lastTime = 0 ;
399414 const gpx = `<?xml version="1.0" encoding="UTF-8"?>
400415<gpx creator="Bangle.js" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
440455 return o ;
441456}
442457
443- function downloadTrack ( filename , callback ) {
444- function onData ( data ) {
458+ async function downloadTrack ( filename ) {
459+ function parse ( data ) {
445460 const lines = data . trim ( ) . split ( "\n" ) , headers = lines . shift ( ) . split ( "," ) ;
446- callback ( lines . map ( l => trackLineToObject ( headers , l ) ) . filter ( t => t . Time ) ) ;
461+ return lines . map ( l => trackLineToObject ( headers , l ) ) . filter ( t => t . Time ) ;
447462 }
448463 const data = fileCache . get ( filename ) ;
449- if ( data ) onData ( data ) ;
450- else {
451- Util . showModal ( `Downloading ${ filename } ...` ) ;
452- Util . readStorageFile ( filename , data => {
453- fileCache . set ( filename , data ) ;
454- onData ( data ) ;
455- Util . hideModal ( ) ;
456- } ) ;
464+ if ( data ) return parse ( data ) ;
465+
466+ Util . showModal ( `Downloading ${ filename } ...` ) ;
467+ try {
468+ const data = await new Promise ( resolve => Util . readStorageFile ( filename , resolve ) ) ;
469+ fileCache . set ( filename , data ) ;
470+ return parse ( data ) ;
471+ } finally {
472+ Util . hideModal ( ) ;
457473 }
458474}
459475
460- function downloadAll ( trackList , cb ) {
461- const tracks = [ ...trackList ] ;
462- const downloadOne = ( ) => {
463- const track = tracks . pop ( ) ;
464- if ( ! track ) return showToast ( "Finished downloading all." , "success" ) ;
465- downloadTrack ( track . filename , lines => {
466- cb ( lines , `Bangle.js Track ${ track . number } ` ) ;
467- downloadOne ( ) ;
476+ function isSelected ( track ) {
477+ const trackNumber = track . number ;
478+ return document . getElementById ( `track-download-${ trackNumber } ` ) . checked ;
479+ }
480+
481+ async function downloadTracks ( tracks , saveCb ) {
482+ for ( const track of tracks ) {
483+ const lines = await downloadTrack ( track . filename ) ;
484+ const title = `Bangle.js Track ${ track . number } ` ;
485+
486+ saveCb ( lines , title ) ;
487+ }
488+
489+ showToast ( "Finished downloading." , "success" ) ;
490+ }
491+
492+ async function deleteTrack ( filename ) {
493+ Util . showModal ( `Deleting ${ filename } ...` ) ;
494+
495+ try {
496+ await new Promise ( resolve => {
497+ Util . eraseStorageFile ( filename , ( ) => resolve ( ) ) ;
468498 } ) ;
469- } ;
470- downloadOne ( ) ;
499+ } finally {
500+ Util . hideModal ( ) ;
501+ }
471502}
472503
473504// ========================================
641672 }
642673}
643674
675+ function htmlCheckbox ( id , text = "" , extra = "" , type = "checkbox" ) {
676+ return `
677+ <label class="form-${ type } ">
678+ <input type="checkbox" id="${ id } " ${ extra } >
679+ <i class="form-icon"></i>
680+ ${ text }
681+ </label>
682+ ` ;
683+ }
684+
644685function getTrackList ( ) {
645686 Util . showModal ( "Loading Track List..." ) ;
646687 domTracks . innerHTML = "" ;
698739 <h2>GPS Tracks</h2>` ;
699740
700741 if ( trackList . length > 0 ) {
742+ html += htmlCheckbox ( "select-all" , "Select all" ) ;
701743 html += `<div class="accordion">` ;
702744 trackList . forEach ( ( track , index ) => {
703745 const trackData = trackLineToObject ( track . info . headers , track . info . l ) ;
@@ -708,20 +750,24 @@ <h2>GPS Tracks</h2>`;
708750
709751 html += `
710752 <div class="accordion-item">
711- <input type="checkbox" id="accordion-track-${ track . number } " name="accordion-tracks" hidden>
712- <label class="accordion-header" for="accordion-track-${ track . number } " data-track-index="${ index } ">
713- <i class="icon icon-arrow-right mr-1"></i>
714- <strong>Track ${ track . number } </strong> - ${ dateStr }
715- </label>
716- <div class="accordion-body" id="track-content-${ track . number } ">
717- <div class="track-loading">Click to load track data...</div>
753+ <div class="horizontal">
754+ ${ htmlCheckbox ( "track-download-" + track . number , undefined , "class='select-checkbox'" ) }
755+ <div class="vertical">
756+ <input type="checkbox" id="accordion-track-${ track . number } " name="accordion-tracks" hidden>
757+ <label class="accordion-header" for="accordion-track-${ track . number } " data-track-index="${ index } ">
758+ <i class="icon icon-arrow-right mr-1"></i>
759+ <strong>Track ${ track . number } </strong> - ${ dateStr }
760+ </label>
761+ <div class="accordion-body" id="track-content-${ track . number } ">
762+ <div class="track-loading">Click to load track data...</div>
763+ </div>
764+ </div>
718765 </div>
719766 </div>` ;
720767 } ) ;
721768
722769 html += `</div>` ;
723- }
724- if ( trackList . length == 0 ) {
770+ } else {
725771 html += `
726772 <div class="column col-12">
727773 <div class="card">
@@ -734,16 +780,21 @@ <h2>GPS Tracks</h2>`;
734780 }
735781
736782 html += `
737- <h2>Batch Operations</h2>
738- <div class="form-group">
739- ${ [ 'KML' , 'GPX' , 'CSV' ] . map ( fmt => `<button class="btn btn-primary" task="download${ fmt . toLowerCase ( ) } _all">Download all ${ fmt } </button>` ) . join ( ' ' ) }
783+ <div id="batch" class="hidden">
784+ <h2>Batch Operations</h2>
785+ <div class="form-group">
786+ ${ [ 'KML' , 'GPX' , 'CSV' ] . map ( fmt => `<button class="btn btn-primary" task="download${ fmt . toLowerCase ( ) } _selected">Download selected ${ fmt } </button>` ) . join ( ' ' ) }
787+ <button class="btn btn-primary" id="delete-selected">Delete selected</button>
788+ </div>
740789 </div>
741790 <h2>Settings</h2>
742791 <div class="form-group">
743- <label class="form-switch">
744- <input type="checkbox" id="settings-allow-no-gps" ${ localStorage . getItem ( "recorder-allow-no-gps" ) == "true" ? "checked" : "" } >
745- <i class="form-icon"></i> Include GPX/KML entries even when there's no GPS info
746- </label>
792+ ${ htmlCheckbox (
793+ "settings-allow-no-gps" ,
794+ "Include GPX/KML entries even when there's no GPS info" ,
795+ localStorage . getItem ( "recorder-allow-no-gps" ) == "true" ? "checked" : "" ,
796+ "switch" ,
797+ ) }
747798 </div>
748799 <div class="form-group">
749800 <label class="form-label">Units</label>
@@ -790,7 +841,7 @@ <h2>Settings</h2>
790841 trackContainer . dataset . loaded = 'true' ;
791842 attachTrackButtonListeners ( trackContainer ) ;
792843
793- downloadTrack ( track . filename , fullTrack => {
844+ downloadTrack ( track . filename ) . then ( fullTrack => {
794845 if ( trackData . Latitude ) {
795846 const coordinates = fullTrack
796847 . filter ( hasValidGPS )
@@ -815,11 +866,35 @@ <h2>Settings</h2>
815866 } ) ;
816867 }
817868
869+ async function confirmDelete ( button , filenames ) {
870+ if ( button . dataset . confirmDelete === "true" ) {
871+ // Second click - proceed with deletion
872+ for ( const filename of filenames )
873+ await deleteTrack ( filename ) ;
874+ getTrackList ( ) ;
875+ } else {
876+ // First click - change to confirm state
877+ const originalText = button . textContent ;
878+ button . textContent = "Confirm Delete" ;
879+ button . classList . add ( "btn-error" ) ;
880+ button . dataset . confirmDelete = "true" ;
881+
882+ // Reset after 3 seconds
883+ setTimeout ( ( ) => {
884+ if ( button . dataset . confirmDelete === "true" ) {
885+ button . textContent = originalText ;
886+ button . classList . remove ( "btn-error" ) ;
887+ delete button . dataset . confirmDelete ;
888+ }
889+ } , 3000 ) ;
890+ }
891+ }
892+
818893 function attachTrackButtonListeners ( container ) {
819894 const buttons = container . querySelectorAll ( "button[task]" ) ;
820895
821896 buttons . forEach ( button => {
822- button . addEventListener ( "click" , event => {
897+ button . addEventListener ( "click" , async event => {
823898 const button = event . currentTarget ;
824899 const filename = button . getAttribute ( "filename" ) ;
825900 const trackid = button . getAttribute ( "trackid" ) ;
@@ -829,45 +904,35 @@ <h2>Settings</h2>
829904
830905 switch ( task ) {
831906 case "delete" :
832- if ( button . dataset . confirmDelete === "true" ) {
833- // Second click - proceed with deletion
834- Util . showModal ( `Deleting ${ filename } ...` ) ;
835- Util . eraseStorageFile ( filename , ( ) => {
836- Util . hideModal ( ) ;
837- getTrackList ( ) ;
838- } ) ;
839- } else {
840- // First click - change to confirm state
841- const originalText = button . textContent ;
842- button . textContent = "Confirm Delete" ;
843- button . classList . add ( "btn-error" ) ;
844- button . dataset . confirmDelete = "true" ;
845-
846- // Reset after 3 seconds
847- setTimeout ( ( ) => {
848- if ( button . dataset . confirmDelete === "true" ) {
849- button . textContent = originalText ;
850- button . classList . remove ( "btn-error" ) ;
851- delete button . dataset . confirmDelete ;
852- }
853- } , 3000 ) ;
854- }
907+ await confirmDelete ( button , [ filename ] ) ;
855908 break ;
856909 case "downloadkml" :
857- downloadTrack ( filename , track => saveKML ( track , `Bangle.js Track ${ trackid } ` ) ) ;
910+ await downloadTracks ( [ filename ] , track => saveKML ( track , `Bangle.js Track ${ trackid } ` ) ) ;
858911 break ;
859912 case "downloadgpx" :
860- downloadTrack ( filename , track => saveGPX ( track , `Bangle.js Track ${ trackid } ` ) ) ;
913+ await downloadTracks ( [ filename ] , track => saveGPX ( track , `Bangle.js Track ${ trackid } ` ) ) ;
861914 break ;
862915 case "downloadcsv" :
863- downloadTrack ( filename , track => saveCSV ( track , `Bangle.js Track ${ trackid } ` ) ) ;
916+ await downloadTracks ( [ filename ] , track => saveCSV ( track , `Bangle.js Track ${ trackid } ` ) ) ;
864917 break ;
865918 }
866919 } ) ;
867920 } ) ;
868921 }
869922
870923 if ( trackList . length > 0 ) {
924+ const selectAll = document . querySelector ( "#select-all" ) ;
925+ const showOrHideBatch = checkboxes => {
926+ const batch = document . querySelector ( "#batch" ) ;
927+ batch . classList . toggle ( "hidden" , ! checkboxes . some ( b => b . checked ) ) ;
928+ } ;
929+ const handleCheckboxCheck = ( ) => {
930+ const checkboxes = [ ...document . querySelectorAll ( ".select-checkbox" ) ] ;
931+
932+ selectAll . checked = checkboxes . every ( b => b . checked ) ;
933+ showOrHideBatch ( checkboxes ) ;
934+ } ;
935+
871936 document . querySelectorAll ( '.accordion-header' ) . forEach ( header => {
872937 header . addEventListener ( 'click' , e => {
873938 const trackIndex = parseInt ( header . dataset . trackIndex ) ;
@@ -876,6 +941,19 @@ <h2>Settings</h2>
876941 setTimeout ( ( ) => displayTrack ( trackIndex , trackNumber ) , 10 ) ;
877942 }
878943 } ) ;
944+
945+ header . closest ( ".horizontal" )
946+ . querySelector ( ".select-checkbox" )
947+ . addEventListener ( 'click' , handleCheckboxCheck ) ;
948+ } ) ;
949+
950+ selectAll . addEventListener ( "click" , e => {
951+ const checkboxes = [ ...document . querySelectorAll ( ".select-checkbox" ) ] ;
952+ const { checked } = e . target ;
953+
954+ for ( const b of checkboxes )
955+ b . checked = checked ;
956+ showOrHideBatch ( checkboxes ) ;
879957 } ) ;
880958 }
881959
@@ -898,12 +976,19 @@ <h2>Settings</h2>
898976 getTrackList ( ) ;
899977 } ) ;
900978 Util . hideModal ( ) ;
901- domTracks . querySelectorAll ( "button[task$='_all ']" ) . forEach ( button => {
902- button . addEventListener ( "click" , e => {
979+ domTracks . querySelectorAll ( "button[task$='_selected ']" ) . forEach ( button => {
980+ button . addEventListener ( "click" , async e => {
903981 const task = e . currentTarget . getAttribute ( "task" ) ;
904- downloadAll ( trackList , task . includes ( 'kml' ) ? saveKML : task . includes ( 'gpx' ) ? saveGPX : saveCSV ) ;
982+ await downloadTracks (
983+ trackList . filter ( isSelected ) ,
984+ task . includes ( 'kml' ) ? saveKML : task . includes ( 'gpx' ) ? saveGPX : saveCSV
985+ ) ;
905986 } ) ;
906987 } ) ;
988+ domTracks . querySelector ( "button#delete-selected" ) . addEventListener ( "click" , e => {
989+ const filenames = trackList . filter ( isSelected ) . map ( track => track . filename ) ;
990+ confirmDelete ( e . target , filenames ) ;
991+ } ) ;
907992 } ) ;
908993 } ) ;
909994}
0 commit comments