├── icon-256.png
├── ubuntu.mono.ttf
├── map
├── images
│ ├── layers.png
│ ├── layers-2x.png
│ ├── marker-icon.png
│ ├── marker-icon-2x.png
│ └── marker-shadow.png
├── uiMap.js
└── leaflet.css
├── resources
├── icon-256.png
├── ubuntu.mono.ttf
└── geo.svg
├── .gitignore
├── VerticalTabs
├── verticalTabs.scss
├── verticalTabs.css.map
└── verticalTabs.css
├── ui.css
├── .eslintrc.js
├── uiNight.css
├── map.html
├── ariaSpeak.js
├── LICENSE
├── nightMode.js
├── settings
├── sliderControl.js
├── uiAdvanced.js
├── advanced.html
├── uiAddons.js
├── settings.html
└── durationInput.js
├── about.js
├── index.css
├── README.md
├── processing
├── uiCommon.js
├── output.html
├── summarise.html
├── uiInput.js
├── uiOutput.js
├── uiSummarise.js
├── aligning.html
└── uiSplit.js
├── about.html
├── schedule
├── schedule.js
├── dateInput.js
├── calculateSunriseSunset.js
├── schedule.html
├── scheduleEditor.js
├── uiScheduleEditor.js
├── roundingInput.js
└── sunIntervalInput.js
├── versionChecker.js
├── timeZoneSelection.html
├── package.json
├── ui.js
├── timeHandler.js
├── scheduleBar.js
└── constants.js
/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/icon-256.png
--------------------------------------------------------------------------------
/ubuntu.mono.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/ubuntu.mono.ttf
--------------------------------------------------------------------------------
/map/images/layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/layers.png
--------------------------------------------------------------------------------
/resources/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/resources/icon-256.png
--------------------------------------------------------------------------------
/map/images/layers-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/layers-2x.png
--------------------------------------------------------------------------------
/resources/ubuntu.mono.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/resources/ubuntu.mono.ttf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.log
3 | dist/
4 | package-lock.json
5 | .DS_Store
6 | .vscode/tasks.json
7 | package-lock.json
8 |
--------------------------------------------------------------------------------
/map/images/marker-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/marker-icon.png
--------------------------------------------------------------------------------
/map/images/marker-icon-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/marker-icon-2x.png
--------------------------------------------------------------------------------
/map/images/marker-shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenAcousticDevices/AudioMoth-Configuration-App/HEAD/map/images/marker-shadow.png
--------------------------------------------------------------------------------
/VerticalTabs/verticalTabs.scss:
--------------------------------------------------------------------------------
1 | $horizontal-tabs-min: 350px;
2 |
3 | $vertical-tabs-min: 400px;
4 |
5 | $fixed-tab-size: 92px;
6 |
7 | $left-tabs-text-align: left;
8 | $right-tabs-text-align: right;
9 |
10 | @import "../node_modules/bootstrap-5-vertical-tabs/scss/responsive-vertical-tabs";
11 |
--------------------------------------------------------------------------------
/ui.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #FFFFFF;
3 | }
4 |
5 | .slider-selection {
6 | background: rgb(173, 189, 241);
7 | }
8 |
9 | input {
10 | border: thin solid #cfcfcf;
11 | }
12 |
13 | .tooltip .tooltip-inner {
14 | max-width: none;
15 | white-space: pre-wrap;
16 | text-align: left;
17 | }
18 |
19 | .greyStart::first-letter, .allGrey {
20 | color: #f0f0f0;
21 | }
22 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'standard',
3 | rules: {
4 | semi: [2, 'always'],
5 | indent: ['error', 4],
6 | 'padded-blocks': ['error', 'always'],
7 | 'no-useless-escape': 0,
8 | 'object-curly-spacing': ['error', 'never'],
9 | 'no-extend-native': ['error', {exceptions: ['Array']}],
10 | 'standard/no-callback-literal': 0,
11 | 'no-throw-literal': 0
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/resources/geo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/uiNight.css:
--------------------------------------------------------------------------------
1 | .table>tbody>tr>td,
2 | .table>tbody>tr>th {
3 | background: #000000;
4 | color: #FFFFFF;
5 | }
6 |
7 | body {
8 | background: #000000;
9 | color: #FFFFFF;
10 | }
11 |
12 | input {
13 | background: #000000;
14 | color: #FFFFFF;
15 | border: thin solid #FFFFFF;
16 | }
17 |
18 | select {
19 | background: #000000;
20 | color: #FFFFFF;
21 | }
22 |
23 | .info-display-input {
24 | background-color: #000000;
25 | }
26 |
27 | #time-canvas {
28 | border: #FFFFFF;
29 | }
30 |
31 | .slider-horizontal .slider-selection {
32 | background: rgb(64, 100, 216);
33 | }
34 |
35 | .grey {
36 | color: #808080;
37 | }
38 |
39 | .greyStart::first-letter, .allGrey {
40 | color: #404040;
41 | }
42 |
43 | .tab-pane {
44 | background: #000000;
45 | color: #FFFFFF;
46 | }
47 |
--------------------------------------------------------------------------------
/map.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | No folder selected.
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | Writing summary file to source folder.
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/settings/uiAddons.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * uiAddons.js
3 | * openacousticdevices.info
4 | * October 2024
5 | *****************************************************************************/
6 |
7 | const {dialog} = require('@electron/remote');
8 |
9 | const magneticDelayCheckbox = document.getElementById('magnetic-delay-checkbox');
10 | const gpsFixTimeCheckbox = document.getElementById('gps-time-checkbox');
11 | const gpsBeforeAfterSelect = document.getElementById('gps-before-after-select');
12 | const gpsBeforeAfterLabel = document.getElementById('gps-before-after-label');
13 | const gpsFixTimeSelect = document.getElementById('gps-time-select');
14 | const gpsFixTimeLabel = document.getElementById('gps-time-label');
15 |
16 | let gpsHardwareWarningDisplayed = false;
17 | let magneticSwitchHardwareWarningDisplayed = false;
18 |
19 | exports.isMagneticSwitchEnabled = () => {
20 |
21 | return magneticDelayCheckbox.checked;
22 |
23 | };
24 |
25 | exports.isTimeSettingFromGPSEnabled = () => {
26 |
27 | return gpsFixTimeCheckbox.checked;
28 |
29 | };
30 |
31 | exports.gpsFixesBeforeAfterSetting = () => {
32 |
33 | return gpsBeforeAfterSelect.value;
34 |
35 | };
36 |
37 | exports.getGpsFixTime = () => {
38 |
39 | return parseInt(gpsFixTimeSelect.value);
40 |
41 | };
42 |
43 | function updateGpsUI () {
44 |
45 | gpsBeforeAfterSelect.disabled = !gpsFixTimeCheckbox.checked;
46 | gpsBeforeAfterSelect.style.color = gpsFixTimeCheckbox.checked ? '' : 'grey';
47 | gpsBeforeAfterLabel.style.color = gpsFixTimeCheckbox.checked ? '' : 'grey';
48 |
49 | gpsFixTimeSelect.disabled = !gpsFixTimeCheckbox.checked;
50 | gpsFixTimeSelect.style.color = gpsFixTimeCheckbox.checked ? '' : 'grey';
51 | gpsFixTimeLabel.style.color = gpsFixTimeCheckbox.checked ? '' : 'grey';
52 |
53 | }
54 |
55 | exports.fillUI = (settings) => {
56 |
57 | magneticDelayCheckbox.checked = settings.magneticSwitchEnabled;
58 |
59 | gpsFixTimeCheckbox.checked = settings.timeSettingFromGPSEnabled;
60 | gpsBeforeAfterSelect.value = settings.acquireGpsFixBeforeAfter;
61 |
62 | const gpsFixTimeSelectValues = [...gpsFixTimeSelect.options].map(o => o.value);
63 |
64 | const gpsFixTimeString = settings.gpsFixTime.toString();
65 |
66 | /* Convert GPS fix time value to a string and check it against available values in dropdown */
67 |
68 | gpsFixTimeSelect.value = gpsFixTimeSelectValues.includes(gpsFixTimeString) ? gpsFixTimeString : '2';
69 |
70 | updateGpsUI();
71 |
72 | };
73 |
74 | exports.prepareUI = (changeFunction) => {
75 |
76 | gpsFixTimeCheckbox.addEventListener('change', changeFunction);
77 | gpsBeforeAfterSelect.addEventListener('change', changeFunction);
78 | gpsFixTimeSelect.addEventListener('change', changeFunction);
79 |
80 | };
81 |
82 | function displayAdditionalHardwareWarning (featureString) {
83 |
84 | dialog.showMessageBox({
85 | type: 'warning',
86 | buttons: ['OK'],
87 | title: 'Additional hardware required',
88 | message: 'Additional hardware is required to use the ' + featureString + ' feature. Do not use these settings if this hardware is not present.'
89 | });
90 |
91 | }
92 |
93 | function displayGpsHardwareWarning () {
94 |
95 | if (gpsHardwareWarningDisplayed) {
96 |
97 | return;
98 |
99 | }
100 |
101 | displayAdditionalHardwareWarning('GPS time setting');
102 |
103 | gpsHardwareWarningDisplayed = true;
104 |
105 | }
106 |
107 | exports.displayGpsHardwareWarning = displayGpsHardwareWarning;
108 |
109 | function displayMagneticSwitchHardwareWarning () {
110 |
111 | if (magneticSwitchHardwareWarningDisplayed) {
112 |
113 | return;
114 |
115 | }
116 |
117 | displayAdditionalHardwareWarning('magnetic switch');
118 |
119 | magneticSwitchHardwareWarningDisplayed = true;
120 |
121 | }
122 |
123 | exports.displayMagneticSwitchHardwareWarning = displayMagneticSwitchHardwareWarning;
124 |
125 | gpsFixTimeCheckbox.addEventListener('change', () => {
126 |
127 | if (gpsFixTimeCheckbox.checked) {
128 |
129 | displayGpsHardwareWarning();
130 |
131 | }
132 |
133 | updateGpsUI();
134 |
135 | });
136 |
137 | magneticDelayCheckbox.addEventListener('change', () => {
138 |
139 | if (magneticDelayCheckbox.checked) {
140 |
141 | displayMagneticSwitchHardwareWarning();
142 |
143 | }
144 |
145 | });
146 |
--------------------------------------------------------------------------------
/ui.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * ui.js
3 | * openacousticdevices.info
4 | * July 2017
5 | *****************************************************************************/
6 |
7 | 'use strict';
8 |
9 | /* global document */
10 |
11 | const electron = require('electron');
12 |
13 | const strftime = require('strftime');
14 |
15 | const timeHandler = require('./timeHandler.js');
16 | const scheduleBar = require('./scheduleBar.js');
17 | const nightMode = require('./nightMode.js');
18 | const constants = require('./constants.js');
19 |
20 | /* UI components */
21 |
22 | const timeZoneLabel = document.getElementById('time-zone-label');
23 |
24 | const timeDisplay = document.getElementById('time-display');
25 |
26 | let timeZoneMode = constants.TIME_ZONE_MODE_UTC;
27 |
28 | let deviceDate;
29 |
30 | /* Date formatting functions */
31 |
32 | function formatDateString (date) {
33 |
34 | const year = ('0000' + date.getUTCFullYear()).slice(-4);
35 | const month = ('00' + (date.getUTCMonth() + 1)).slice(-2);
36 | const day = ('00' + date.getUTCDate()).slice(-2);
37 |
38 | return year + '-' + month + '-' + day;
39 |
40 | }
41 |
42 | exports.formatDateString = formatDateString;
43 |
44 | function extractDateComponents (dateString) {
45 |
46 | const dateArray = dateString.split('-');
47 |
48 | const year = parseInt(dateArray[0]);
49 | const month = parseInt(dateArray[1]);
50 | const day = parseInt(dateArray[2]);
51 |
52 | return {
53 | year,
54 | month,
55 | day
56 | };
57 |
58 | }
59 |
60 | exports.extractDateComponents = extractDateComponents;
61 |
62 | /* Time zone mode function */
63 |
64 | function getTimeZoneMode () {
65 |
66 | return timeZoneMode;
67 |
68 | }
69 |
70 | exports.getTimeZoneMode = getTimeZoneMode;
71 |
72 | /* Generate and display current time in either UTC or local time */
73 |
74 | function showTime () {
75 |
76 | if (deviceDate) {
77 |
78 | const timeZoneOffset = timeHandler.getTimeZoneOffset();
79 |
80 | const strftimeUTC = strftime.timezone(timeZoneOffset);
81 |
82 | if (timeDisplay) {
83 |
84 | timeDisplay.textContent = strftimeUTC('%H:%M:%S %d/%m/%Y', deviceDate);
85 |
86 | }
87 |
88 | } else {
89 |
90 | timeDisplay.textContent = '--:--:-- --/--/----';
91 |
92 | }
93 |
94 | }
95 |
96 | exports.showTime = showTime;
97 |
98 | /* Run all UI update functions */
99 |
100 | function update () {
101 |
102 | scheduleBar.updateCanvas();
103 |
104 | if (timeDisplay) {
105 |
106 | showTime();
107 |
108 | }
109 |
110 | }
111 |
112 | exports.update = update;
113 |
114 | exports.updateDate = (date) => {
115 |
116 | deviceDate = date;
117 |
118 | };
119 |
120 | exports.disableTimeDisplay = () => {
121 |
122 | if (timeDisplay) {
123 |
124 | timeDisplay.classList.add('grey');
125 |
126 | timeZoneLabel.classList.add('grey');
127 |
128 | }
129 |
130 | };
131 |
132 | exports.enableTimeDisplay = () => {
133 |
134 | if (timeDisplay) {
135 |
136 | timeDisplay.classList.remove('grey');
137 |
138 | timeZoneLabel.classList.remove('grey');
139 |
140 | }
141 |
142 | };
143 |
144 | /* Switch between time zone modes (UTC, local, and custom) */
145 |
146 | function setTimeZoneStatus (mode) {
147 |
148 | timeZoneMode = mode;
149 |
150 | }
151 |
152 | exports.setTimeZoneStatus = setTimeZoneStatus;
153 |
154 | function updateTimeZoneUI () {
155 |
156 | const timeZoneText = timeHandler.getTimeZoneText();
157 |
158 | timeZoneLabel.innerHTML = timeZoneText;
159 |
160 | scheduleBar.clearSelectedPeriod();
161 |
162 | update();
163 |
164 | }
165 |
166 | exports.updateTimeZoneUI = updateTimeZoneUI;
167 |
168 | function setNightMode (nm) {
169 |
170 | nightMode.setNightMode(nm);
171 |
172 | scheduleBar.updateCanvas();
173 | scheduleBar.drawTimeLabels();
174 |
175 | }
176 |
177 | exports.setNightMode = setNightMode;
178 |
179 | function toggleNightMode () {
180 |
181 | nightMode.toggle();
182 |
183 | scheduleBar.updateCanvas();
184 | scheduleBar.drawTimeLabels();
185 |
186 | }
187 |
188 | exports.toggleNightMode = toggleNightMode;
189 |
190 | exports.isNightMode = nightMode.isEnabled;
191 |
192 | electron.ipcRenderer.on('poll-night-mode', () => {
193 |
194 | electron.ipcRenderer.send('night-mode-poll-reply', nightMode.isEnabled());
195 |
196 | });
197 |
198 | exports.setSunriseSunsetEnabled = scheduleBar.setSunriseSunsetEnabled;
199 | exports.isSunriseSunsetEnabled = scheduleBar.isSunriseSunsetEnabled;
200 | exports.setSunriseSunset = scheduleBar.setSunriseSunset;
201 | exports.getSunrise = scheduleBar.getSunrise;
202 | exports.getSunset = scheduleBar.getSunset;
203 |
--------------------------------------------------------------------------------
/processing/uiInput.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * uiInput.js
3 | * openacousticdevices.info
4 | * September 2023
5 | *****************************************************************************/
6 |
7 | const {dialog, getCurrentWindow} = require('@electron/remote');
8 | const currentWindow = getCurrentWindow();
9 |
10 | const path = require('path');
11 | const fs = require('fs');
12 |
13 | const selectionRadios = document.getElementsByName('selection-radio');
14 |
15 | let previousSelection = [];
16 |
17 | function getSelectedRadioValue (radioName) {
18 |
19 | return parseInt(document.querySelector('input[name="' + radioName + '"]:checked').value);
20 |
21 | }
22 |
23 | /* Open dialog and set files to be processed */
24 |
25 | exports.selectRecordings = (fileRegex) => {
26 |
27 | let folderContents, filePath, fileName, recordings;
28 |
29 | const selectionTypes = ['openFile', 'openDirectory'];
30 | const selectionType = getSelectedRadioValue('selection-radio');
31 | const properties = [selectionTypes[selectionType], 'multiSelections'];
32 |
33 | /* If files are being selected, limit selection to .wav files */
34 |
35 | const filters = (selectionType === 0) ? [{name: 'wav', extensions: ['wav']}] : [];
36 |
37 | const openPath = previousSelection.length > 0 ? path.dirname(previousSelection[0]) : '';
38 |
39 | const selection = dialog.showOpenDialogSync(currentWindow, {
40 | title: 'Select recording file or folder containing recordings',
41 | nameFieldLabel: 'Recordings',
42 | properties,
43 | filters,
44 | defaultPath: openPath
45 | });
46 |
47 | if (selection) {
48 |
49 | previousSelection = selection;
50 |
51 | recordings = [];
52 |
53 | if (selectionType === 0) {
54 |
55 | for (let i = 0; i < selection.length; i++) {
56 |
57 | filePath = selection[i];
58 | fileName = path.basename(filePath);
59 |
60 | /* Check if wav files match a given regex */
61 |
62 | if (fileName.charAt(0) !== '.' && fileRegex.test(fileName.toUpperCase())) {
63 |
64 | recordings.push(filePath);
65 |
66 | }
67 |
68 | }
69 |
70 | } else {
71 |
72 | for (let i = 0; i < selection.length; i++) {
73 |
74 | folderContents = fs.readdirSync(selection[i]);
75 |
76 | for (let j = 0; j < folderContents.length; j++) {
77 |
78 | filePath = folderContents[j];
79 |
80 | if (filePath.charAt(0) !== '.' && fileRegex.test(filePath.toUpperCase())) {
81 |
82 | recordings.push(path.join(selection[i], filePath));
83 |
84 | }
85 |
86 | }
87 |
88 | }
89 |
90 | }
91 |
92 | return recordings;
93 |
94 | }
95 |
96 | };
97 |
98 | function walk (dir) {
99 |
100 | let results = [];
101 |
102 | /* Check directory is not hidden */
103 |
104 | const dirName = path.basename(dir);
105 |
106 | if (dirName.charAt(0) === '.') return results;
107 |
108 | /* Try to read the directory contents */
109 |
110 | let list;
111 |
112 | try {
113 |
114 | list = fs.readdirSync(dir);
115 |
116 | } catch (e) {
117 |
118 | return results;
119 |
120 | }
121 |
122 | /* Process each file in the directory */
123 |
124 | list.forEach((file) => {
125 |
126 | const filePath = path.join(dir, file);
127 |
128 | const stat = fs.statSync(filePath);
129 |
130 | if (stat && stat.isDirectory()) {
131 |
132 | results = results.concat(walk(filePath));
133 |
134 | } else {
135 |
136 | if (file.charAt(0) !== '.') {
137 |
138 | results.push(filePath);
139 |
140 | }
141 |
142 | }
143 |
144 | });
145 |
146 | return results;
147 |
148 | }
149 |
150 | exports.selectAllFilesInFolder = () => {
151 |
152 | const selection = dialog.showOpenDialogSync(currentWindow, {
153 | title: 'Select folder containing files',
154 | nameFieldLabel: 'Recordings',
155 | properties: ['openDirectory'],
156 | filters: []
157 | });
158 |
159 | return updateFilesInFolder(selection);
160 |
161 | };
162 |
163 | function updateFilesInFolder (selection) {
164 |
165 | let files;
166 |
167 | if (selection && selection[0]) {
168 |
169 | if (!fs.existsSync(selection[0])) {
170 |
171 | return;
172 |
173 | }
174 |
175 | files = [];
176 |
177 | for (let i = 0; i < selection.length; i++) {
178 |
179 | const folderContents = walk(selection[i]);
180 |
181 | files = files.concat(folderContents);
182 |
183 | }
184 |
185 | return {
186 | selection,
187 | folder: selection[0],
188 | files
189 | };
190 |
191 | }
192 |
193 | }
194 |
195 | exports.updateFilesInFolder = updateFilesInFolder;
196 |
197 | function resetPreviousSelection () {
198 |
199 | previousSelection = [];
200 |
201 | }
202 |
203 | if (selectionRadios.length > 0) {
204 |
205 | selectionRadios[0].addEventListener('change', resetPreviousSelection);
206 | selectionRadios[1].addEventListener('change', resetPreviousSelection);
207 |
208 | }
209 |
--------------------------------------------------------------------------------
/processing/uiOutput.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * uiOutput.js
3 | * openacousticdevices.info
4 | * June 2022
5 | *****************************************************************************/
6 |
7 | 'use strict';
8 |
9 | /* global document */
10 |
11 | const {dialog} = require('@electron/remote');
12 |
13 | const outputCheckbox = document.getElementById('output-checkbox');
14 | const outputButton = document.getElementById('output-button');
15 | const outputLabel = document.getElementById('output-label');
16 |
17 | const subdirectoriesLabel = document.getElementById('subdirectories-label');
18 | const subdirectoriesCheckbox = document.getElementById('subdirectories-checkbox');
19 |
20 | let outputDir = '';
21 |
22 | const prefixInput = document.getElementById('prefix-input');
23 | const prefixCheckbox = document.getElementById('prefix-checkbox');
24 | const prefixLabel = document.getElementById('prefix-label');
25 |
26 | const selectionRadios = document.getElementsByName('selection-radio');
27 |
28 | function getSelectedRadioValue (radioName) {
29 |
30 | return parseInt(document.querySelector('input[name="' + radioName + '"]:checked').value);
31 |
32 | }
33 |
34 | /* Update label to notify user if a custom output directtory is being used */
35 |
36 | function updateOutputLabel () {
37 |
38 | if (outputDir === '' || !outputCheckbox.checked) {
39 |
40 | outputLabel.textContent = 'Writing WAV files to source folder.';
41 |
42 | } else {
43 |
44 | outputLabel.textContent = 'Writing WAV files to destination folder.';
45 |
46 | }
47 |
48 | };
49 |
50 | function updateSubdirectoriesCheckbox () {
51 |
52 | const selectionType = getSelectedRadioValue('selection-radio');
53 |
54 | if (outputCheckbox.checked && outputDir !== '' && selectionType === 1) {
55 |
56 | subdirectoriesCheckbox.disabled = false;
57 | subdirectoriesLabel.classList.remove('grey');
58 |
59 | } else {
60 |
61 | subdirectoriesCheckbox.disabled = true;
62 | subdirectoriesLabel.classList.add('grey');
63 |
64 | }
65 |
66 | }
67 |
68 | /* Add listener which handles enabling/disabling custom output directory UI */
69 |
70 | outputCheckbox.addEventListener('change', () => {
71 |
72 | updateOutputLabel();
73 |
74 | updateSubdirectoriesCheckbox();
75 |
76 | outputButton.disabled = !outputCheckbox.checked;
77 |
78 | });
79 |
80 | /* Select a custom output directory. If Cancel is pressed, assume no custom direcotry is wantted */
81 |
82 | outputButton.addEventListener('click', () => {
83 |
84 | const destinationName = dialog.showOpenDialogSync({
85 | title: 'Select Destination',
86 | nameFieldLabel: 'Destination',
87 | multiSelections: false,
88 | properties: ['openDirectory'],
89 | defaultPath: outputDir
90 | });
91 |
92 | if (destinationName !== undefined) {
93 |
94 | outputDir = destinationName[0];
95 |
96 | }
97 |
98 | updateOutputLabel();
99 |
100 | updateSubdirectoriesCheckbox();
101 |
102 | });
103 |
104 | /* Remove all characters which aren't A-Z, a-z, 0-9, and _ */
105 |
106 | prefixInput.addEventListener('keydown', (e) => {
107 |
108 | if (prefixInput.disabled) {
109 |
110 | e.preventDefault();
111 | return;
112 |
113 | }
114 |
115 | const reg = /[^A-Za-z-_0-9]{1}/g;
116 |
117 | if (reg.test(e.key)) {
118 |
119 | e.preventDefault();
120 |
121 | }
122 |
123 | });
124 |
125 | prefixInput.addEventListener('paste', (e) => {
126 |
127 | e.stopPropagation();
128 | e.preventDefault();
129 |
130 | if (prefixInput.disabled) {
131 |
132 | return;
133 |
134 | }
135 |
136 | /* Read text from clipboard */
137 |
138 | const clipboardData = e.clipboardData || window.clipboardData;
139 | const pastedData = clipboardData.getData('Text');
140 |
141 | /* Perform paste, but remove all unsupported characters */
142 |
143 | prefixInput.value += pastedData.replace(/[^A-Za-z_0-9]{1}/g, '');
144 |
145 | /* Limit max number of characters */
146 |
147 | prefixInput.value = prefixInput.value.substring(0, prefixInput.maxLength);
148 |
149 | });
150 |
151 | /* Add listener to handle enabling/disabling prefix UI */
152 |
153 | prefixCheckbox.addEventListener('change', () => {
154 |
155 | if (prefixCheckbox.checked) {
156 |
157 | prefixLabel.classList.remove('grey');
158 | prefixInput.classList.remove('grey');
159 | prefixInput.disabled = false;
160 |
161 | } else {
162 |
163 | prefixLabel.classList.add('grey');
164 | prefixInput.classList.add('grey');
165 | prefixInput.disabled = true;
166 |
167 | }
168 |
169 | });
170 |
171 | exports.disableOutputCheckbox = () => {
172 |
173 | outputCheckbox.disabled = true;
174 |
175 | };
176 |
177 | exports.disableOutputButton = () => {
178 |
179 | outputButton.disabled = true;
180 |
181 | };
182 |
183 | exports.enableOutputCheckbox = () => {
184 |
185 | outputCheckbox.disabled = false;
186 |
187 | };
188 |
189 | exports.enableOutputButton = () => {
190 |
191 | if (outputCheckbox.checked) {
192 |
193 | outputButton.disabled = false;
194 |
195 | }
196 |
197 | };
198 |
199 | exports.isCustomDestinationEnabled = () => {
200 |
201 | return outputCheckbox.checked;
202 |
203 | };
204 |
205 | exports.getOutputDir = () => {
206 |
207 | return outputDir;
208 |
209 | };
210 |
211 | exports.isCreateSubdirectoriesEnabled = () => {
212 |
213 | return subdirectoriesCheckbox.checked;
214 |
215 | };
216 |
217 | selectionRadios[0].addEventListener('change', updateSubdirectoriesCheckbox);
218 | selectionRadios[1].addEventListener('change', updateSubdirectoriesCheckbox);
219 |
--------------------------------------------------------------------------------
/timeHandler.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * timeHandler.js
3 | * openacousticdevices.info
4 | * November 2019
5 | *****************************************************************************/
6 |
7 | const {ipcRenderer} = require('electron');
8 |
9 | const constants = require('./constants.js');
10 | const ui = require('./ui.js');
11 |
12 | let storedTimeZoneOffset = 0;
13 |
14 | function sortPeriods (periods) {
15 |
16 | const sortedPeriods = periods.sort((a, b) => {
17 |
18 | return a.startMins - b.startMins;
19 |
20 | });
21 |
22 | return sortedPeriods;
23 |
24 | }
25 |
26 | exports.sortPeriods = sortPeriods;
27 |
28 | function storeTimeZoneOffset () {
29 |
30 | storedTimeZoneOffset = 0;
31 |
32 | if (ui.getTimeZoneMode() === constants.TIME_ZONE_MODE_LOCAL) {
33 |
34 | const currentDate = new Date();
35 |
36 | storedTimeZoneOffset = -currentDate.getTimezoneOffset();
37 |
38 | }
39 |
40 | if (ui.getTimeZoneMode() === constants.TIME_ZONE_MODE_CUSTOM) {
41 |
42 | storedTimeZoneOffset = ipcRenderer.sendSync('request-custom-time-zone');
43 |
44 | }
45 |
46 | }
47 |
48 | exports.storeTimeZoneOffset = storeTimeZoneOffset;
49 |
50 | function getTimeZoneOffset () {
51 |
52 | return storedTimeZoneOffset;
53 |
54 | }
55 |
56 | exports.getTimeZoneOffset = getTimeZoneOffset;
57 |
58 | /* ------------------------------------------------- Time zone conversion functions ------------------------------------------------- */
59 |
60 | function shiftTime (time, toUTC) {
61 |
62 | /* Offset is given as UTC - local time in minutes */
63 |
64 | let timeZoneOffset = storedTimeZoneOffset;
65 |
66 | timeZoneOffset = toUTC ? timeZoneOffset * -1 : timeZoneOffset;
67 |
68 | time = (time + timeZoneOffset) % constants.MINUTES_IN_DAY;
69 |
70 | /* If time zone offset move time over midnight */
71 |
72 | if (time < 0) {
73 |
74 | time += constants.MINUTES_IN_DAY;
75 |
76 | }
77 |
78 | return time;
79 |
80 | }
81 |
82 | function shiftTimePeriod (timePeriod, toUTC) {
83 |
84 | const startMins = shiftTime(timePeriod.startMins, toUTC);
85 | const endMins = shiftTime(timePeriod.endMins, toUTC);
86 |
87 | return {
88 | startMins,
89 | endMins
90 | };
91 |
92 | }
93 |
94 | exports.shiftTimePeriod = shiftTimePeriod;
95 |
96 | /* Convert a list of time periods from UTC to local */
97 |
98 | function shiftTimePeriods (tps, toUTC) {
99 |
100 | let shiftedTimePeriods = [];
101 |
102 | for (let i = 0; i < tps.length; i++) {
103 |
104 | const timePeriod = tps[i];
105 | const shiftedTimePeriod = shiftTimePeriod(timePeriod, toUTC);
106 |
107 | shiftedTimePeriods.push({
108 | startMins: shiftedTimePeriod.startMins,
109 | endMins: shiftedTimePeriod.endMins
110 | });
111 |
112 | }
113 |
114 | shiftedTimePeriods = sortPeriods(shiftedTimePeriods);
115 |
116 | shiftedTimePeriods = checkTimePeriodsForOverlaps(shiftedTimePeriods);
117 |
118 | return shiftedTimePeriods;
119 |
120 | }
121 |
122 | exports.shiftTimePeriods = shiftTimePeriods;
123 |
124 | /* ------------------------------------------------- Time period checks ------------------------------------------------- */
125 |
126 | /* See if any newly created time periods overlap and can be merged */
127 |
128 | function checkTimePeriodsForOverlaps (timePeriods) {
129 |
130 | for (let i = 0; i < timePeriods.length; i++) {
131 |
132 | for (let j = 0; j < timePeriods.length; j++) {
133 |
134 | if (timePeriods[i].startMins === timePeriods[j].startMins && timePeriods[i].endMins === timePeriods[j].endMins) {
135 |
136 | continue;
137 |
138 | }
139 |
140 | if (timePeriods[i].endMins === timePeriods[j].startMins) {
141 |
142 | timePeriods[i].endMins = timePeriods[j].endMins;
143 | timePeriods.splice(j, 1);
144 |
145 | return checkTimePeriodsForOverlaps(timePeriods);
146 |
147 | }
148 |
149 | }
150 |
151 | }
152 |
153 | return timePeriods;
154 |
155 | }
156 |
157 | exports.checkTimePeriodsForOverlaps = checkTimePeriodsForOverlaps;
158 |
159 | /* ------------------------------------------------- Other functions ------------------------------------------------- */
160 |
161 | /* Get the text representation of the current timeZone */
162 |
163 | function getTimeZoneText () {
164 |
165 | let timeZoneText = 'UTC';
166 |
167 | if (storedTimeZoneOffset === 0) return timeZoneText;
168 |
169 | const timeZoneOffsetHours = storedTimeZoneOffset < 0 ? Math.ceil(storedTimeZoneOffset / constants.MINUTES_IN_HOUR) : Math.floor(storedTimeZoneOffset / constants.MINUTES_IN_HOUR);
170 |
171 | const timeZoneOffsetMins = Math.abs(storedTimeZoneOffset % constants.MINUTES_IN_HOUR);
172 |
173 | timeZoneText += storedTimeZoneOffset > 0 ? '+' : '-';
174 |
175 | timeZoneText += Math.abs(timeZoneOffsetHours);
176 |
177 | if (timeZoneOffsetMins > 0) timeZoneText += ':' + ('00' + timeZoneOffsetMins).slice(-2);
178 |
179 | return timeZoneText;
180 |
181 | }
182 |
183 | exports.getTimeZoneText = getTimeZoneText;
184 |
185 | /* Pad the left of each time with zeroes */
186 |
187 | function pad (n) {
188 |
189 | return (n < 10) ? ('0' + n) : n;
190 |
191 | }
192 |
193 | /* Convert the number of minutes through a day to a HH:MM formatted string */
194 |
195 | function minsToTimeString (mins) {
196 |
197 | const timeHours = Math.floor(mins / constants.MINUTES_IN_HOUR);
198 |
199 | return pad(timeHours) + ':' + pad((mins - (timeHours * constants.MINUTES_IN_HOUR)));
200 |
201 | }
202 |
203 | exports.minsToTimeString = minsToTimeString;
204 |
--------------------------------------------------------------------------------
/schedule/scheduleEditor.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * scheduleEditor.js
3 | * openacousticdevices.info
4 | * November 2019
5 | *****************************************************************************/
6 |
7 | 'use strict';
8 |
9 | const ui = require('../ui.js');
10 | const timeHandler = require('../timeHandler.js');
11 | const constants = require('../constants.js');
12 |
13 | /* Remove a time from the recording period data structure and update UI to reflect change */
14 |
15 | function removeTime (timePeriod, tps) {
16 |
17 | const startMins = timePeriod.startMins;
18 |
19 | for (let i = 0; i < tps.length; i++) {
20 |
21 | if (tps[i].startMins === startMins) {
22 |
23 | tps.splice(i, 1);
24 |
25 | }
26 |
27 | }
28 |
29 | return tps;
30 |
31 | }
32 |
33 | exports.removeTime = removeTime;
34 |
35 | /* Check to see if two periods of time overlap */
36 |
37 | function isSubset (startTime1, endTime1, startTime2, endTime2) {
38 |
39 | const a = startTime1 < endTime1 && startTime2 < endTime2 && startTime2 >= startTime1 && endTime2 <= endTime1;
40 | const b = startTime1 > endTime1 && startTime2 < endTime2 && startTime2 >= startTime1 && (endTime2 >= startTime1 || endTime2 <= endTime1);
41 | const c = startTime1 > endTime1 && startTime2 < endTime2 && startTime2 <= endTime1 && endTime2 <= endTime1;
42 | const d = startTime1 > endTime1 && startTime2 > endTime2 && startTime2 > startTime1 && endTime2 < endTime1;
43 | const e = startTime1 === endTime1;
44 |
45 | return a || b || c || d || e;
46 |
47 | }
48 |
49 | function isSuperset (startTime1, endTime1, startTime2, endTime2) {
50 |
51 | return isSubset(startTime2, endTime2, startTime1, endTime1);
52 |
53 | }
54 |
55 | function startEndOverlaps (startTime1, endTime1, startTime2, endTime2) {
56 |
57 | const a = startTime1 < endTime1 && startTime2 > endTime2 && endTime2 >= startTime1 && startTime2 <= endTime1;
58 | const b = startTime1 > endTime1 && startTime2 < endTime2 && startTime2 <= endTime1 && endTime2 >= startTime1;
59 | const c = startTime1 > endTime1 && startTime2 > endTime2 && endTime2 >= startTime1 && startTime2 >= endTime1;
60 | const d = startTime1 > endTime1 && startTime2 > endTime2 && endTime2 <= startTime1 && startTime2 <= endTime1;
61 |
62 | return a || b || c || d;
63 |
64 | }
65 |
66 | function startOverlaps (startTime1, endTime1, startTime2, endTime2) {
67 |
68 | const a = startTime1 < endTime1 && startTime2 >= startTime1 && startTime2 <= endTime1;
69 | const b = startTime1 > endTime1 && startTime2 >= startTime1;
70 | const c = startTime1 > endTime1 && startTime2 <= endTime1;
71 |
72 | return a || b || c;
73 |
74 | }
75 |
76 | function endOverlaps (startTime1, endTime1, startTime2, endTime2) {
77 |
78 | const a = startTime1 < endTime1 && endTime2 >= startTime1 && endTime2 <= endTime1;
79 | const b = startTime1 > endTime1 && endTime2 >= startTime1;
80 | const c = startTime1 > endTime1 && endTime2 <= endTime1;
81 |
82 | return a || b || c;
83 |
84 | }
85 |
86 | /* Add a new recording period to the data structure and update UI */
87 |
88 | function addTime (startMins, endMins, timePeriods) {
89 |
90 | endMins = endMins === constants.MINUTES_IN_DAY ? 0 : endMins;
91 |
92 | let newStart, newEnd;
93 |
94 | if (startMins === endMins) {
95 |
96 | timePeriods = [];
97 |
98 | timePeriods.push({
99 | startMins,
100 | endMins
101 | });
102 |
103 | return timePeriods;
104 |
105 | }
106 |
107 | for (let i = 0; i < timePeriods.length; i++) {
108 |
109 | const existingStartMins = timePeriods[i].startMins;
110 | let existingEndMins = timePeriods[i].endMins;
111 | existingEndMins = existingEndMins === constants.MINUTES_IN_DAY ? 0 : existingEndMins;
112 |
113 | /* Check if the new period is just a time inside an existing period */
114 |
115 | if (isSubset(existingStartMins, existingEndMins, startMins, endMins)) {
116 |
117 | console.log('Subset');
118 |
119 | return timePeriods;
120 |
121 | }
122 |
123 | if (isSuperset(existingStartMins, existingEndMins, startMins, endMins)) {
124 |
125 | console.log('Superset');
126 |
127 | timePeriods = removeTime(timePeriods[i], timePeriods);
128 |
129 | timePeriods = addTime(startMins, endMins, timePeriods);
130 |
131 | return timePeriods;
132 |
133 | }
134 |
135 | if (startEndOverlaps(existingStartMins, existingEndMins, startMins, endMins)) {
136 |
137 | console.log('Start and end overlaps');
138 |
139 | timePeriods = removeTime(timePeriods[i], timePeriods);
140 |
141 | timePeriods = addTime(startMins, startMins, timePeriods);
142 |
143 | return timePeriods;
144 |
145 | }
146 |
147 | if (startOverlaps(existingStartMins, existingEndMins, startMins, endMins)) {
148 |
149 | console.log('Start overlaps');
150 |
151 | newStart = existingStartMins;
152 | newEnd = endMins;
153 |
154 | timePeriods = removeTime(timePeriods[i], timePeriods);
155 |
156 | timePeriods = addTime(newStart, newEnd, timePeriods);
157 |
158 | return timePeriods;
159 |
160 | }
161 |
162 | if (endOverlaps(existingStartMins, existingEndMins, startMins, endMins)) {
163 |
164 | console.log('End overlaps');
165 |
166 | newStart = startMins;
167 | newEnd = existingEndMins;
168 |
169 | timePeriods = removeTime(timePeriods[i], timePeriods);
170 |
171 | timePeriods = addTime(newStart, newEnd, timePeriods);
172 |
173 | return timePeriods;
174 |
175 | }
176 |
177 | }
178 |
179 | timePeriods.push({
180 | startMins,
181 | endMins
182 | });
183 |
184 | return timePeriods;
185 |
186 | }
187 |
188 | function formatAndAddTime (startTimestamp, endTimestamp, timePeriods) {
189 |
190 | let utcPeriod;
191 |
192 | const timePeriod = {
193 | startMins: startTimestamp,
194 | endMins: endTimestamp
195 | };
196 |
197 | const timeZoneMode = ui.getTimeZoneMode();
198 |
199 | if (timeZoneMode === constants.TIME_ZONE_MODE_UTC) {
200 |
201 | utcPeriod = {
202 | startMins: (timePeriod.startMins % constants.MINUTES_IN_DAY),
203 | endMins: (timePeriod.endMins % constants.MINUTES_IN_DAY)
204 | };
205 |
206 | } else {
207 |
208 | utcPeriod = timeHandler.shiftTimePeriod(timePeriod, true);
209 |
210 | }
211 |
212 | startTimestamp = utcPeriod.startMins;
213 | endTimestamp = utcPeriod.endMins;
214 |
215 | timePeriods = addTime(startTimestamp, endTimestamp, timePeriods);
216 |
217 | return timePeriods;
218 |
219 | }
220 |
221 | exports.addTime = addTime;
222 | exports.formatAndAddTime = formatAndAddTime;
223 |
--------------------------------------------------------------------------------
/schedule/uiScheduleEditor.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * uiScheduleEditor.js
3 | * openacousticdevices.info
4 | * November 2019
5 | *****************************************************************************/
6 |
7 | /* global Event */
8 |
9 | const electron = require('electron');
10 |
11 | const ariaSpeak = require('../ariaSpeak.js');
12 |
13 | const schedule = require('../schedule/schedule.js');
14 | const scheduleEditor = require('./scheduleEditor.js');
15 | const scheduleBar = require('../scheduleBar.js');
16 | const timeHandler = require('../timeHandler.js');
17 | const ui = require('../ui.js');
18 | const timeInput = require('./timeInput.js');
19 | const constants = require('../constants.js');
20 |
21 | /* UI components */
22 |
23 | const timeList = document.getElementById('time-list');
24 | const addTimeButton = document.getElementById('add-time-button');
25 | const removeTimeButton = document.getElementById('remove-time-button');
26 | const clearTimeButton = document.getElementById('clear-time-button');
27 |
28 | const startTimeInput = timeInput.create('start-time-input', true);
29 | const endTimeInput = timeInput.create('end-time-input', true);
30 |
31 | const endTimeTextInput = timeInput.getTextInput(endTimeInput);
32 | timeInput.setNextElements(startTimeInput, [endTimeTextInput]);
33 | timeInput.setNextElements(endTimeInput, [addTimeButton, timeList]);
34 |
35 | /* Function which uses changed schedule to update life approximation */
36 |
37 | let updateLifeDisplayOnChange;
38 |
39 | /* Pad the left of each time with zeroes */
40 |
41 | function pad (n) {
42 |
43 | return (n < 10) ? ('0' + n) : n;
44 |
45 | }
46 |
47 | /* Obtain time periods from UI and add to data structure in response to button press */
48 |
49 | function addTimeOnClick () {
50 |
51 | const startTimeSplit = timeInput.getValue(startTimeInput).split(':');
52 | const endTimeSplit = timeInput.getValue(endTimeInput).split(':');
53 |
54 | const startHours = parseInt(startTimeSplit[0], 10);
55 | const startMins = parseInt(startTimeSplit[1], 10);
56 | const endHours = parseInt(endTimeSplit[0], 10);
57 | const endMins = parseInt(endTimeSplit[1], 10);
58 |
59 | const startTimestamp = (startHours * constants.MINUTES_IN_HOUR) + startMins;
60 | let endTimestamp = (endHours * constants.MINUTES_IN_HOUR) + endMins;
61 |
62 | endTimestamp = (endTimestamp > 0) ? endTimestamp : 0;
63 |
64 | ariaSpeak.speak(pad(startHours) + ':' + pad(startMins) + ' to ' + pad(endHours) + ':' + pad(endMins));
65 |
66 | let timePeriods = schedule.getTimePeriods();
67 | timePeriods = scheduleEditor.formatAndAddTime(startTimestamp, endTimestamp, timePeriods);
68 | schedule.setTimePeriods(timePeriods);
69 |
70 | scheduleBar.updateCanvas();
71 | updateTimeList();
72 | scheduleBar.clearSelectedPeriod();
73 |
74 | }
75 |
76 | function getTimePeriodFromList () {
77 |
78 | const values = timeList.value.split(',');
79 |
80 | const timePeriod = {
81 | startMins: parseInt(values[0], 10),
82 | endMins: parseInt(values[1], 10)
83 | };
84 |
85 | /* Allows time list to display 24:00 as 0:00 */
86 | timePeriod.endMins = (timePeriod.endMins === 0) ? constants.MINUTES_IN_DAY : 0;
87 |
88 | return timePeriod;
89 |
90 | }
91 |
92 | function removeTimeOnClick () {
93 |
94 | let timePeriods = schedule.getTimePeriods();
95 |
96 | const timePeriod = getTimePeriodFromList();
97 |
98 | const timeZoneMode = ui.getTimeZoneMode();
99 |
100 | if (timeZoneMode === constants.TIME_ZONE_MODE_UTC) {
101 |
102 | timePeriods = scheduleEditor.removeTime(timePeriod, timePeriods);
103 |
104 | } else {
105 |
106 | let ltps = timeHandler.shiftTimePeriods(timePeriods, false);
107 | ltps = scheduleEditor.removeTime(timePeriod, ltps);
108 |
109 | timePeriods = timeHandler.shiftTimePeriods(ltps, true);
110 |
111 | }
112 |
113 | schedule.setTimePeriods(timePeriods);
114 |
115 | scheduleBar.updateCanvas();
116 | updateTimeList();
117 | removeTimeButton.disabled = true;
118 | scheduleBar.clearSelectedPeriod();
119 |
120 | }
121 |
122 | /* Fill UI list with time periods from data structure */
123 |
124 | function updateTimeList () {
125 |
126 | let tp;
127 |
128 | const timePeriods = schedule.getTimePeriods();
129 |
130 | const timeZoneMode = ui.getTimeZoneMode();
131 |
132 | if (timeZoneMode === constants.TIME_ZONE_MODE_UTC) {
133 |
134 | tp = timePeriods;
135 |
136 | } else {
137 |
138 | tp = timeHandler.shiftTimePeriods(timePeriods, false);
139 |
140 | }
141 |
142 | timeList.options.length = 0;
143 |
144 | /* Sort recording periods in order of occurrence */
145 |
146 | tp = timeHandler.sortPeriods(tp);
147 |
148 | for (let i = 0; i < tp.length; i += 1) {
149 |
150 | const startMins = tp[i].startMins;
151 | let endMins = tp[i].endMins;
152 | endMins = endMins === 0 ? constants.MINUTES_IN_DAY : endMins;
153 |
154 | const timeZoneText = '(' + timeHandler.getTimeZoneText() + ')';
155 |
156 | const option = document.createElement('option');
157 | option.text = timeHandler.minsToTimeString(startMins) + ' - ' + timeHandler.minsToTimeString(endMins) + ' ' + timeZoneText;
158 | option.value = [startMins, endMins];
159 | timeList.add(option);
160 |
161 | }
162 |
163 | /* Disable or enable action buttons in response to number time periods entered */
164 |
165 | addTimeButton.disabled = (tp.length >= constants.MAX_PERIODS);
166 | clearTimeButton.disabled = (tp.length === 0);
167 |
168 | updateLifeDisplayOnChange();
169 |
170 | }
171 |
172 | exports.updateTimeList = updateTimeList;
173 |
174 | electron.ipcRenderer.on('update-schedule', updateTimeList);
175 |
176 | function clearTimesOnClick () {
177 |
178 | schedule.setTimePeriods([]);
179 | scheduleBar.updateCanvas();
180 | updateTimeList();
181 | removeTimeButton.disabled = true;
182 | scheduleBar.clearSelectedPeriod();
183 |
184 | }
185 |
186 | exports.disableRemoveTimeButton = () => {
187 |
188 | removeTimeButton.disabled = true;
189 |
190 | };
191 |
192 | exports.prepareUI = (changeFunction) => {
193 |
194 | updateLifeDisplayOnChange = changeFunction;
195 |
196 | addTimeButton.addEventListener('click', addTimeOnClick);
197 | removeTimeButton.addEventListener('click', removeTimeOnClick);
198 | clearTimeButton.addEventListener('click', clearTimesOnClick);
199 |
200 | timeList.addEventListener('change', () => {
201 |
202 | removeTimeButton.disabled = (timeList.value === null || timeList.value === '');
203 |
204 | if (timeList.value !== null && timeList.value !== '') {
205 |
206 | const valueSplit = timeList.value.split(',');
207 | const selectedTimePeriod = {startMins: parseInt(valueSplit[0]), endMins: parseInt(valueSplit[1])};
208 | scheduleBar.setSelectedPeriod(selectedTimePeriod);
209 |
210 | } else {
211 |
212 | scheduleBar.clearSelectedPeriod();
213 |
214 | }
215 |
216 | });
217 |
218 | timeInput.setValue(endTimeInput, 24, 0);
219 |
220 | };
221 |
222 | scheduleBar.prepareScheduleCanvas((selectedIndex) => {
223 |
224 | timeList.options.selectedIndex = selectedIndex;
225 | timeList.focus();
226 | timeList.dispatchEvent(new Event('change'));
227 |
228 | });
229 |
--------------------------------------------------------------------------------
/processing/uiSummarise.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * uiSummarise.js
3 | * openacousticdevices.info
4 | * September 2023
5 | *****************************************************************************/
6 |
7 | const electron = require('electron');
8 | const {dialog, shell} = require('@electron/remote');
9 |
10 | /* Get common functions to add night mode functionality */
11 | require('./uiCommon.js');
12 |
13 | const uiInput = require('./uiInput.js');
14 |
15 | const audiomothUtils = require('audiomoth-utils');
16 |
17 | const path = require('path');
18 |
19 | const fileLabel = document.getElementById('file-label');
20 | const fileButton = document.getElementById('file-button');
21 |
22 | const outputCheckbox = document.getElementById('output-checkbox');
23 | const outputButton = document.getElementById('output-button');
24 | const outputLabel = document.getElementById('output-label');
25 |
26 | const summariseButton = document.getElementById('summarise-button');
27 |
28 | let files = [];
29 | let outputDir = '';
30 | let sourceDir = '';
31 | let selection = [];
32 |
33 | let summarising = false;
34 |
35 | /* Disable UI elements in main window while progress bar is open and downsample is in progress */
36 |
37 | function disableUI () {
38 |
39 | fileButton.disabled = true;
40 | summariseButton.disabled = true;
41 |
42 | outputButton.disabled = true;
43 |
44 | }
45 |
46 | function enableUI () {
47 |
48 | fileButton.disabled = false;
49 | summariseButton.disabled = false;
50 |
51 | if (outputCheckbox.checked) {
52 |
53 | outputButton.disabled = false;
54 |
55 | }
56 |
57 | summarising = false;
58 |
59 | }
60 |
61 | /* Update label to reflect new file/folder selection */
62 |
63 | function updateInputDirectoryDisplay (directoryArray) {
64 |
65 | if (!directoryArray) {
66 |
67 | fileLabel.innerHTML = 'No folder selected.';
68 | summariseButton.disabled = true;
69 |
70 | } else if (directoryArray.length === 0) {
71 |
72 | fileLabel.innerHTML = 'No files found.';
73 | summariseButton.disabled = true;
74 |
75 | } else {
76 |
77 | fileLabel.innerHTML = 'Found ';
78 | fileLabel.innerHTML += directoryArray.length + ' file';
79 | fileLabel.innerHTML += (directoryArray.length === 1 ? '' : 's');
80 | fileLabel.innerHTML += '.';
81 | summariseButton.disabled = false;
82 |
83 | }
84 |
85 | }
86 |
87 | /* Summary file output */
88 |
89 | /* Update label to notify user if a custom output directtory is being used */
90 |
91 | function updateOutputLabel () {
92 |
93 | if (outputDir === '' || !outputCheckbox.checked) {
94 |
95 | outputLabel.textContent = 'Writing summary file to source folder.';
96 |
97 | } else {
98 |
99 | outputLabel.textContent = 'Writing summary file to destination folder.';
100 |
101 | }
102 |
103 | };
104 |
105 | /* Add listener which handles enabling/disabling custom output directory UI */
106 |
107 | outputCheckbox.addEventListener('change', () => {
108 |
109 | updateOutputLabel();
110 |
111 | outputButton.disabled = !outputCheckbox.checked;
112 |
113 | });
114 |
115 | outputButton.addEventListener('click', () => {
116 |
117 | const destinationName = dialog.showOpenDialogSync({
118 | title: 'Select Destination',
119 | nameFieldLabel: 'Destination',
120 | multiSelections: false,
121 | properties: ['openDirectory']
122 | });
123 |
124 | if (destinationName !== undefined) {
125 |
126 | outputDir = destinationName[0];
127 |
128 | }
129 |
130 | updateOutputLabel();
131 |
132 | });
133 |
134 | /* Select/process file(s) buttons */
135 |
136 | fileButton.addEventListener('click', () => {
137 |
138 | const response = uiInput.selectAllFilesInFolder();
139 |
140 | if (response) {
141 |
142 | selection = response.selection;
143 |
144 | files = response.files;
145 |
146 | sourceDir = response.folder;
147 |
148 | }
149 |
150 | updateInputDirectoryDisplay(files);
151 |
152 | });
153 |
154 | /* Summarise functionality */
155 |
156 | function summariseFiles () {
157 |
158 | if (!files) {
159 |
160 | return;
161 |
162 | }
163 |
164 | let successCount = 0;
165 |
166 | audiomothUtils.summariser.initialise();
167 |
168 | for (let i = 0; i < files.length; i++) {
169 |
170 | /* If progress bar is closed, the summarise task is considered cancelled. This will contact the main thread and ask if that has happened */
171 |
172 | const cancelled = electron.ipcRenderer.sendSync('poll-summarise-cancelled');
173 |
174 | if (cancelled) {
175 |
176 | console.log('Summary cancelled.');
177 | enableUI();
178 | return;
179 |
180 | }
181 |
182 | /* Let the main thread know what value to set the progress bar to */
183 |
184 | electron.ipcRenderer.send('set-summarise-bar-progress', i, 0);
185 |
186 | console.log('Summarising:', files[i]);
187 | console.log('-');
188 |
189 | const summariseSuccess = audiomothUtils.summariser.summarise(sourceDir, files[i], (progress) => {
190 |
191 | electron.ipcRenderer.send('set-summarise-bar-progress', i, progress);
192 | electron.ipcRenderer.send('set-summarise-bar-file', i, path.basename(files[i]));
193 |
194 | });
195 |
196 | if (summariseSuccess) {
197 |
198 | successCount++;
199 |
200 | }
201 |
202 | }
203 |
204 | /* Attempt to write summary file */
205 |
206 | /* If no output path given, use the containing directory of the input files */
207 |
208 | const outputPath = outputCheckbox.checked && outputDir !== '' ? outputDir : sourceDir;
209 |
210 | const finaliseResult = audiomothUtils.summariser.finalise(outputPath);
211 |
212 | /* Notify main thread that summarise is complete so progress bar is closed */
213 |
214 | electron.ipcRenderer.send('set-summarise-bar-completed', successCount, finaliseResult);
215 |
216 | /* Show summarise file location */
217 |
218 | try {
219 |
220 | shell.showItemInFolder(path.join(outputPath, 'SUMMARY.CSV'));
221 |
222 | } catch (error) {
223 |
224 | console.error('Failed to show summary file in file explorer');
225 |
226 | }
227 |
228 | }
229 |
230 | summariseButton.addEventListener('click', () => {
231 |
232 | if (summarising) {
233 |
234 | return;
235 |
236 | }
237 |
238 | const response = uiInput.updateFilesInFolder(selection);
239 |
240 | if (response) {
241 |
242 | files = response.files;
243 | sourceDir = response.folder;
244 |
245 | updateInputDirectoryDisplay(files);
246 |
247 | summarising = true;
248 | disableUI();
249 |
250 | electron.ipcRenderer.send('start-summarise-bar', files.length);
251 | setTimeout(summariseFiles, 2000);
252 |
253 | } else {
254 |
255 | dialog.showMessageBoxSync({
256 | type: 'error',
257 | title: 'Folder Not Found',
258 | message: 'Selected folder no longer exists. Select a new location and try again.'
259 | });
260 |
261 | files = [];
262 | sourceDir = '';
263 | selection = [];
264 |
265 | updateInputDirectoryDisplay(files);
266 |
267 | }
268 |
269 | });
270 |
271 | /* When the progress bar is complete and the summary window at the end has been displayed for a fixed amount of time, it will close and this re-enables the UI */
272 |
273 | electron.ipcRenderer.on('summarise-summary-closed', enableUI);
274 |
--------------------------------------------------------------------------------
/VerticalTabs/verticalTabs.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Responsive vertical tabs for Bootstrap 5
3 | *
4 | * Main stylesheet. @import "custom-variables.sccss" before
5 | * this file to compile into final CSS
6 | *
7 | * Copyright (c) 2020-2021 Tromgy (tromgy@yahoo.com)
8 | *
9 | * This software is supplied under the terms of the MIT License, a
10 | * copy of which should be located in the distribution where this
11 | * file was obtained (LICENSE.txt). A copy of the license may also be
12 | * found online at https://opensource.org/licenses/MIT.
13 | *
14 | */
15 | .right-tabs.sideways-tabs, .left-tabs.sideways-tabs {
16 | margin-top: 5rem;
17 | border: none;
18 | position: relative;
19 | margin-bottom: 0;
20 | }
21 |
22 | .right-tabs.sideways-tabs, .left-tabs.sideways-tabs, .right-tabs.nav-tabs, .left-tabs.nav-tabs {
23 | height: 100%;
24 | flex-direction: column;
25 | }
26 |
27 | .ellipsis {
28 | white-space: nowrap;
29 | overflow: hidden;
30 | text-overflow: ellipsis;
31 | padding-right: 0.5rem;
32 | }
33 |
34 | .vtabs .tab-clickable {
35 | cursor: pointer;
36 | }
37 |
38 | .left-tabs.nav-tabs {
39 | border-right: var(--bs-border-width) solid var(--bs-border-color);
40 | border-bottom: none;
41 | }
42 |
43 | .left-tabs .nav-link {
44 | border-top-left-radius: var(--bs-border-radius);
45 | border-bottom-left-radius: var(--bs-border-radius);
46 | border-bottom-right-radius: 0;
47 | border-top-right-radius: 0;
48 | margin-right: -var(--bs-border-width);
49 | text-align: left;
50 | }
51 |
52 | .left-tabs .nav-link:hover {
53 | border-right: var(--bs-border-width) solid transparent;
54 | }
55 |
56 | .left-tabs .nav-link.active {
57 | border-top: var(--bs-border-width) solid var(--bs-border-color);
58 | border-right: var(--bs-border-width) solid transparent;
59 | border-bottom: var(--bs-border-width) solid var(--bs-border-color);
60 | border-left: var(--bs-border-width) solid var(--bs-border-color);
61 | }
62 |
63 | .right-tabs.nav-tabs {
64 | border-left: var(--bs-border-width) solid var(--bs-border-color);
65 | border-bottom: none;
66 | }
67 |
68 | .right-tabs .nav-link {
69 | border-top-right-radius: var(--bs-border-radius);
70 | border-bottom-right-radius: var(--bs-border-radius);
71 | border-bottom-left-radius: 0;
72 | border-top-left-radius: 0;
73 | margin-left: -var(--bs-border-width);
74 | text-align: right;
75 | }
76 |
77 | .right-tabs .nav-link:hover {
78 | border-left: var(--bs-border-width) solid transparent;
79 | }
80 |
81 | .right-tabs .nav-link.active {
82 | border-top: var(--bs-border-width) solid var(--bs-border-color);
83 | border-right: var(--bs-border-width) solid var(--bs-border-color);
84 | border-bottom: var(--bs-border-width) solid var(--bs-border-color);
85 | border-left: var(--bs-border-width) solid transparent;
86 | }
87 |
88 | .left-tabs.sideways-tabs {
89 | border-right: none;
90 | left: -3.2rem;
91 | }
92 |
93 | .sideways-tabs.left-tabs .nav-item {
94 | transform: rotate(-90deg);
95 | height: 1rem;
96 | margin-bottom: calc(92px - 1rem);
97 | }
98 |
99 | .sideways-tabs.left-tabs .nav-link {
100 | width: 92px;
101 | text-align: center;
102 | white-space: nowrap;
103 | overflow: hidden;
104 | text-overflow: ellipsis;
105 | border-top-right-radius: var(--bs-border-radius);
106 | border-bottom-right-radius: 0;
107 | border-bottom-left-radius: 0;
108 | border-top-left-radius: var(--bs-border-radius);
109 | border-bottom: var(--bs-border-width) solid var(--bs-border-color);
110 | }
111 |
112 | .sideways-tabs.left-tabs .nav-link:hover {
113 | border-right: var(--bs-border-width) solid #e9ecef;
114 | }
115 |
116 | .sideways-tabs.left-tabs .nav-link.active {
117 | border-top: var(--bs-border-width) solid var(--bs-border-color);
118 | border-right: var(--bs-border-width) solid var(--bs-border-color);
119 | border-bottom: var(--bs-border-width) solid transparent;
120 | border-left: var(--bs-border-width) solid var(--bs-border-color);
121 | }
122 |
123 | .right-tabs.sideways-tabs {
124 | border-left: none;
125 | right: 3.2rem;
126 | }
127 |
128 | .sideways-tabs.right-tabs .nav-item {
129 | transform: rotate(90deg);
130 | height: 1rem;
131 | margin-bottom: calc(92px - 1rem);
132 | }
133 |
134 | .sideways-tabs.right-tabs .nav-link {
135 | width: 92px;
136 | text-align: center;
137 | white-space: nowrap;
138 | overflow: hidden;
139 | text-overflow: ellipsis;
140 | border-top-right-radius: var(--bs-border-radius);
141 | border-bottom-right-radius: 0;
142 | border-bottom-left-radius: 0;
143 | border-top-left-radius: var(--bs-border-radius);
144 | border-bottom: var(--bs-border-width) solid var(--bs-border-color);
145 | }
146 |
147 | .sideways-tabs.right-tabs .nav-link:hover {
148 | border-left: var(--bs-border-width) solid #e9ecef;
149 | }
150 |
151 | .sideways-tabs.right-tabs .nav-link.active {
152 | border-top: var(--bs-border-width) solid var(--bs-border-color);
153 | border-right: var(--bs-border-width) solid var(--bs-border-color);
154 | border-bottom: var(--bs-border-width) solid transparent;
155 | border-left: var(--bs-border-width) solid var(--bs-border-color);
156 | }
157 |
158 | .vtabs .accordion-header {
159 | display: none;
160 | }
161 |
162 | @media (max-width: 400px) {
163 | .left-tabs.nav-tabs {
164 | flex-direction: row;
165 | border-right: none;
166 | border-left: none;
167 | min-width: 100%;
168 | border-bottom: var(--bs-border-width) solid var(--bs-border-color);
169 | left: auto;
170 | margin-top: auto;
171 | }
172 | .left-tabs .nav-link {
173 | width: 92px;
174 | text-align: center;
175 | white-space: nowrap;
176 | overflow: hidden;
177 | text-overflow: ellipsis;
178 | border-top-right-radius: var(--bs-border-radius);
179 | border-bottom-right-radius: 0;
180 | border-bottom-left-radius: 0;
181 | border-top-left-radius: var(--bs-border-radius);
182 | margin-right: 0;
183 | margin-bottom: -var(--bs-border-width);
184 | }
185 | .left-tabs .nav-link.nav-link:hover {
186 | border-right-color: var(--bs-border-color);
187 | border-bottom-color: transparent;
188 | }
189 | .left-tabs .nav-link.active {
190 | border-top-color: var(--bs-border-color);
191 | border-right-color: var(--bs-border-color);
192 | border-bottom-color: transparent;
193 | border-left-color: var(--bs-border-color);
194 | }
195 | .sideways-tabs.left-tabs .nav-item,
196 | .sideways-tabs.right-tabs .nav-item {
197 | transform: none;
198 | height: auto;
199 | width: auto;
200 | margin-bottom: 0;
201 | }
202 | .right-tabs.nav-tabs {
203 | flex-direction: row;
204 | border-right: none;
205 | border-left: none;
206 | min-width: 100%;
207 | border-top: var(--bs-border-width) solid var(--bs-border-color);
208 | right: auto;
209 | margin-top: auto;
210 | }
211 | .sideways-tabs.right-tabs .nav-link,
212 | .right-tabs .nav-link {
213 | width: 92px;
214 | text-align: center;
215 | white-space: nowrap;
216 | overflow: hidden;
217 | text-overflow: ellipsis;
218 | border-top-right-radius: 0;
219 | border-bottom-right-radius: var(--bs-border-radius);
220 | border-bottom-left-radius: var(--bs-border-radius);
221 | border-top-left-radius: 0;
222 | margin-left: 0;
223 | margin-top: -var(--bs-border-width);
224 | border-bottom-color: transparent;
225 | }
226 | .right-tabs .nav-link:hover {
227 | border-top-color: transparent;
228 | border-left-color: var(--bs-border-color);
229 | border-bottom-color: #e9ecef;
230 | }
231 | .sideways-tabs.right-tabs .nav-link.active,
232 | .right-tabs .nav-link.active {
233 | border-top-color: transparent;
234 | border-right-color: var(--bs-border-color);
235 | border-bottom-color: var(--bs-border-color);
236 | border-left-color: var(--bs-border-color);
237 | }
238 | }
239 | @media (max-width: 350px) {
240 | .left-tabs.nav-tabs {
241 | display: none;
242 | }
243 | .right-tabs.nav-tabs {
244 | display: none;
245 | }
246 | .vtabs .tab-content > .tab-pane {
247 | display: block !important;
248 | opacity: 1;
249 | }
250 | .vtabs .accordion-header {
251 | display: block;
252 | }
253 | .vtabs button.accordion-button:focus {
254 | border: none;
255 | outline: none;
256 | box-shadow: none;
257 | }
258 | }
259 | @media (min-width: 351px) {
260 | .vtabs .accordion-item {
261 | border: none;
262 | }
263 | .vtabs .accordion-body.collapse {
264 | display: block;
265 | }
266 | }
267 |
268 | /*# sourceMappingURL=verticalTabs.css.map */
269 |
--------------------------------------------------------------------------------
/scheduleBar.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * scheduleBar.js
3 | * openacousticdevices.info
4 | * November 2019
5 | *****************************************************************************/
6 |
7 | 'use strict';
8 |
9 | /* global window, document */
10 |
11 | const ui = require('./ui.js');
12 | const timeHandler = require('./timeHandler.js');
13 | const schedule = require('./schedule/schedule.js');
14 | const constants = require('./constants.js');
15 | const uiSun = require('./schedule/uiSun.js');
16 |
17 | const timeCanvas = document.getElementById('time-canvas');
18 | const timeContext = timeCanvas.getContext('2d');
19 | const labelCanvas = document.getElementById('label-canvas');
20 | const labelContext = labelCanvas.getContext('2d');
21 |
22 | let scaleFactor;
23 |
24 | const sunCanvas = document.getElementById('sun-canvas');
25 |
26 | let clickCallback;
27 |
28 | let selectedPeriod = null;
29 |
30 | /* Function to rescale */
31 |
32 | function rescale (canvas) {
33 |
34 | scaleFactor = 1;
35 |
36 | if (Object.prototype.hasOwnProperty.call(window, 'devicePixelRatio')) {
37 |
38 | if (window.devicePixelRatio > 1) {
39 |
40 | scaleFactor = window.devicePixelRatio;
41 |
42 | }
43 |
44 | }
45 |
46 | if (scaleFactor > 1) {
47 |
48 | canvas.width = canvas.width * scaleFactor;
49 | canvas.height = canvas.height * scaleFactor;
50 |
51 | canvas.style.width = canvas.width / scaleFactor + 'px';
52 | canvas.style.height = canvas.height / scaleFactor + 'px';
53 |
54 | }
55 |
56 | }
57 |
58 | function drawPeriod (startMins, endMins, timeCanvas) {
59 |
60 | /* width / 1440 minutes */
61 | const recX = startMins * timeCanvas.width / constants.MINUTES_IN_DAY;
62 | const recLen = (endMins - startMins) * timeCanvas.width / constants.MINUTES_IN_DAY;
63 |
64 | timeContext.fillRect(Math.round(recX), 0, Math.round(recLen), timeCanvas.height);
65 |
66 | }
67 |
68 | function updateCanvas () {
69 |
70 | let timePeriods = schedule.getTimePeriods();
71 |
72 | const timeZoneMode = ui.getTimeZoneMode();
73 |
74 | if (timeZoneMode !== constants.TIME_ZONE_MODE_UTC) {
75 |
76 | timePeriods = timeHandler.shiftTimePeriods(timePeriods, false);
77 |
78 | }
79 |
80 | const currentTimeDate = new Date();
81 |
82 | timeContext.clearRect(0, 0, timeCanvas.width, timeCanvas.height);
83 |
84 | if (uiSun.usingSunSchedule()) {
85 |
86 | uiSun.updateSunUI();
87 |
88 | } else {
89 |
90 | for (let i = 0; i < timePeriods.length; i++) {
91 |
92 | const startMins = timePeriods[i].startMins;
93 | let endMins = timePeriods[i].endMins;
94 | endMins = endMins === 0 ? constants.MINUTES_IN_DAY : endMins;
95 |
96 | if (selectedPeriod !== null && (selectedPeriod.startMins === startMins && selectedPeriod.endMins === endMins)) {
97 |
98 | timeContext.fillStyle = '#007BFF';
99 |
100 | } else {
101 |
102 | timeContext.fillStyle = '#FF0000';
103 |
104 | }
105 |
106 | if (startMins > endMins) {
107 |
108 | drawPeriod(startMins, constants.MINUTES_IN_DAY, timeCanvas);
109 | drawPeriod(0, endMins, timeCanvas);
110 |
111 | } else if (startMins === endMins) {
112 |
113 | drawPeriod(0, constants.MINUTES_IN_DAY, timeCanvas);
114 |
115 | } else {
116 |
117 | drawPeriod(startMins, endMins, timeCanvas);
118 |
119 | }
120 |
121 | }
122 |
123 | }
124 |
125 | /* 6am, midday and 6pm markers */
126 |
127 | if (ui.isNightMode()) {
128 |
129 | timeContext.fillStyle = '#FFFFFF';
130 |
131 | } else {
132 |
133 | timeContext.fillStyle = '#000000';
134 |
135 | }
136 |
137 | timeContext.fillRect(Math.round(0.25 * timeCanvas.width), 0, 1 * scaleFactor, timeCanvas.height);
138 | timeContext.fillRect(Math.round(0.5 * timeCanvas.width), 0, 1 * scaleFactor, timeCanvas.height);
139 | timeContext.fillRect(Math.round(0.75 * timeCanvas.width), 0, 1 * scaleFactor, timeCanvas.height);
140 |
141 | let currentMins = (currentTimeDate.getUTCHours() * constants.MINUTES_IN_HOUR) + currentTimeDate.getUTCMinutes();
142 |
143 | if (timeZoneMode !== constants.TIME_ZONE_MODE_UTC) {
144 |
145 | currentMins += timeHandler.getTimeZoneOffset();
146 |
147 | }
148 |
149 | let currentX = currentMins * timeCanvas.width / constants.MINUTES_IN_DAY;
150 |
151 | /* Wrap time around midnight at both ends */
152 |
153 | currentX = currentX % timeCanvas.width;
154 | currentX = currentX < 0 ? currentX + timeCanvas.width : currentX;
155 |
156 | timeContext.fillStyle = '#00AF00';
157 | timeContext.fillRect(Math.floor(currentX), 0, 2 * scaleFactor, timeCanvas.height);
158 |
159 | }
160 |
161 | exports.updateCanvas = updateCanvas;
162 |
163 | /* Regularly update time period canvas so green line reflects current time */
164 |
165 | function updateCanvasTimer () {
166 |
167 | updateCanvas();
168 | setTimeout(updateCanvasTimer, 60000);
169 |
170 | }
171 |
172 | /* Update which period is selected when schedule bar is clicked */
173 |
174 | function updateSelectedPeriod (event) {
175 |
176 | let timePeriods = schedule.getTimePeriods();
177 |
178 | /* If there's only one possible time period and it covers the entire length of the schedule, don't bother with the full check */
179 |
180 | if (timePeriods.length === 1 && timePeriods[0].startMins === timePeriods[0].endMins) {
181 |
182 | const selectedIndex = 0;
183 |
184 | if (clickCallback) {
185 |
186 | clickCallback(selectedIndex);
187 |
188 | }
189 |
190 | return;
191 |
192 | }
193 |
194 | const rect = timeCanvas.getBoundingClientRect();
195 | const clickMins = (event.clientX - rect.left) / timeCanvas.width * constants.MINUTES_IN_DAY;
196 |
197 | if (ui.getTimeZoneMode() !== constants.TIME_ZONE_MODE_UTC) {
198 |
199 | timePeriods = timeHandler.shiftTimePeriods(timePeriods, false);
200 |
201 | }
202 |
203 | let selectedIndex = -1;
204 |
205 | for (let i = 0; i < timePeriods.length; i++) {
206 |
207 | const startMins = timePeriods[i].startMins;
208 | const endMins = timePeriods[i].endMins;
209 |
210 | if (startMins > endMins) {
211 |
212 | if ((clickMins >= startMins && clickMins < constants.MINUTES_IN_DAY) || (clickMins >= 0 && clickMins < endMins)) {
213 |
214 | selectedIndex = i;
215 |
216 | }
217 |
218 | } else {
219 |
220 | if (clickMins >= startMins && clickMins < endMins) {
221 |
222 | selectedIndex = i;
223 |
224 | }
225 |
226 | }
227 |
228 | }
229 |
230 | if (clickCallback) {
231 |
232 | clickCallback(selectedIndex);
233 |
234 | }
235 |
236 | }
237 |
238 | /* Set clicked period to specific index */
239 |
240 | exports.setSelectedPeriod = (period) => {
241 |
242 | selectedPeriod = period;
243 | updateCanvas();
244 |
245 | };
246 |
247 | function clearSelectedPeriod () {
248 |
249 | selectedPeriod = null;
250 | updateCanvas();
251 |
252 | };
253 |
254 | exports.clearSelectedPeriod = clearSelectedPeriod;
255 |
256 | /* Draw labels below time period canvas */
257 |
258 | function drawTimeLabels () {
259 |
260 | const fontSize = 0.32 * timeCanvas.height;
261 |
262 | labelContext.clearRect(0, 0, labelCanvas.width, labelCanvas.height);
263 |
264 | labelContext.font = fontSize + 'pt Helvetica';
265 |
266 | if (ui.isNightMode()) {
267 |
268 | labelContext.fillStyle = '#FFFFFF';
269 |
270 | } else {
271 |
272 | labelContext.fillStyle = '#000000';
273 |
274 | }
275 |
276 | labelContext.fillText('00:00', 0, fontSize);
277 | labelContext.fillText('06:00', 0.25 * timeCanvas.width, fontSize);
278 | labelContext.fillText('12:00', 0.5 * timeCanvas.width, fontSize);
279 | labelContext.fillText('18:00', 0.751 * timeCanvas.width, fontSize);
280 | labelContext.fillText('24:00', timeCanvas.width, fontSize);
281 |
282 | }
283 |
284 | exports.drawTimeLabels = drawTimeLabels;
285 |
286 | exports.prepareScheduleCanvas = (callback) => {
287 |
288 | clickCallback = callback;
289 | timeCanvas.addEventListener('click', (event) => {
290 |
291 | if (!uiSun.usingSunSchedule()) {
292 |
293 | updateSelectedPeriod(event);
294 |
295 | }
296 |
297 | });
298 |
299 | /* Rescale for resolution of screen */
300 | rescale(timeCanvas);
301 | rescale(labelCanvas);
302 | rescale(sunCanvas);
303 |
304 | /* Draw labels below timeline */
305 | drawTimeLabels();
306 |
307 | /* Start recursive loop which keep canvas up to date */
308 | updateCanvasTimer();
309 |
310 | };
311 |
312 | exports.setSchedule = (timePeriods) => {
313 |
314 | let tps = timePeriods;
315 |
316 | tps = timeHandler.sortPeriods(tps);
317 |
318 | schedule.setTimePeriods(tps);
319 | updateCanvas();
320 |
321 | };
322 |
--------------------------------------------------------------------------------
/settings/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
83 |
84 |
85 |
86 | Enable sleep/record cyclic recording:
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | Sleep duration (s):
96 |
97 |
102 |
103 |
104 |
105 |
106 | Recording duration (s):
107 |
108 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | Enable LED:
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | Enable low-voltage cut-off:
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | Enable battery level indication:
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/constants.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * recordingCOnfigurations.js
3 | * openacousticdevices.info
4 | * December 2019
5 | *****************************************************************************/
6 |
7 | /* Setting parameters */
8 |
9 | exports.CONFIGURATIONS = [{
10 | trueSampleRate: 8,
11 | clockDivider: 4,
12 | acquisitionCycles: 16,
13 | oversampleRate: 1,
14 | sampleRate: 384000,
15 | sampleRateDivider: 48,
16 | recordCurrent: 9.22,
17 | energySaverRecordCurrent: 5.92,
18 | listenCurrent: 8.59,
19 | energySaverListenCurrent: 5.41
20 | }, {
21 | trueSampleRate: 16,
22 | clockDivider: 4,
23 | acquisitionCycles: 16,
24 | oversampleRate: 1,
25 | sampleRate: 384000,
26 | sampleRateDivider: 24,
27 | recordCurrent: 9.83,
28 | energySaverRecordCurrent: 6.63,
29 | listenCurrent: 8.72,
30 | energySaverListenCurrent: 5.54
31 | }, {
32 | trueSampleRate: 32,
33 | clockDivider: 4,
34 | acquisitionCycles: 16,
35 | oversampleRate: 1,
36 | sampleRate: 384000,
37 | sampleRateDivider: 12,
38 | recordCurrent: 11.3,
39 | energySaverRecordCurrent: 8.04,
40 | listenCurrent: 8.95,
41 | energySaverListenCurrent: 5.78
42 | }, {
43 | trueSampleRate: 48,
44 | clockDivider: 4,
45 | acquisitionCycles: 16,
46 | oversampleRate: 1,
47 | sampleRate: 384000,
48 | sampleRateDivider: 8,
49 | recordCurrent: 12.3,
50 | energySaverRecordCurrent: 8.93,
51 | listenCurrent: 9.14,
52 | energySaverListenCurrent: 5.98
53 | }, {
54 | trueSampleRate: 96,
55 | clockDivider: 4,
56 | acquisitionCycles: 16,
57 | oversampleRate: 1,
58 | sampleRate: 384000,
59 | sampleRateDivider: 4,
60 | recordCurrent: 15.8,
61 | energySaverRecordCurrent: 15.8,
62 | listenCurrent: 10.0,
63 | energySaverListenCurrent: 10.0
64 | }, {
65 | trueSampleRate: 192,
66 | clockDivider: 4,
67 | acquisitionCycles: 16,
68 | oversampleRate: 1,
69 | sampleRate: 384000,
70 | sampleRateDivider: 2,
71 | recordCurrent: 24.1,
72 | energySaverRecordCurrent: 24.1,
73 | listenCurrent: 11.5,
74 | energySaverListenCurrent: 11.5
75 | }, {
76 | trueSampleRate: 250,
77 | clockDivider: 4,
78 | acquisitionCycles: 16,
79 | oversampleRate: 1,
80 | sampleRate: 250000,
81 | sampleRateDivider: 1,
82 | recordCurrent: 26.4,
83 | energySaverRecordCurrent: 26.4,
84 | listenCurrent: 10.6,
85 | energySaverListenCurrent: 10.6
86 | }, {
87 | trueSampleRate: 384,
88 | clockDivider: 4,
89 | acquisitionCycles: 16,
90 | oversampleRate: 1,
91 | sampleRate: 384000,
92 | sampleRateDivider: 1,
93 | recordCurrent: 38.5,
94 | energySaverRecordCurrent: 38.5,
95 | listenCurrent: 12.7,
96 | energySaverListenCurrent: 12.7
97 | }];
98 |
99 | /* Configuration settings to be used when a device is on firmware < 1.4.4 */
100 | /* Only sent to devices. Not used in energy calculations */
101 |
102 | exports.OLD_CONFIGURATIONS = [{
103 | trueSampleRate: 8,
104 | clockDivider: 4,
105 | acquisitionCycles: 16,
106 | oversampleRate: 1,
107 | sampleRate: 128000,
108 | sampleRateDivider: 16
109 | }, {
110 | trueSampleRate: 16,
111 | clockDivider: 4,
112 | acquisitionCycles: 16,
113 | oversampleRate: 1,
114 | sampleRate: 128000,
115 | sampleRateDivider: 8
116 | }, {
117 | trueSampleRate: 32,
118 | clockDivider: 4,
119 | acquisitionCycles: 16,
120 | oversampleRate: 1,
121 | sampleRate: 128000,
122 | sampleRateDivider: 4
123 | }];
124 |
125 | /* GPS energy consumption */
126 |
127 | exports.GPS_FIX_TIME = 0.5 / 60.0;
128 | exports.GPS_FIX_CONSUMPTION = 30.0;
129 |
130 | exports.MINIMUM_GPS_FIX_TIME = 30;
131 |
132 | /* Packet lengths for each version */
133 |
134 | exports.PACKET_LENGTH_VERSIONS = [{
135 | firmwareVersion: [0, 0, 0],
136 | packetLength: 39
137 | }, {
138 | firmwareVersion: [1, 2, 0],
139 | packetLength: 40
140 | }, {
141 | firmwareVersion: [1, 2, 1],
142 | packetLength: 42
143 | }, {
144 | firmwareVersion: [1, 2, 2],
145 | packetLength: 43
146 | }, {
147 | firmwareVersion: [1, 4, 0],
148 | packetLength: 58
149 | }, {
150 | firmwareVersion: [1, 5, 0],
151 | packetLength: 59
152 | }, {
153 | firmwareVersion: [1, 6, 0],
154 | packetLength: 62
155 | }];
156 |
157 | const FIRMWARE_OFFICIAL_RELEASE = 0;
158 | const FIRMWARE_OFFICIAL_RELEASE_CANDIDATE = 1;
159 | const FIRMWARE_CUSTOM_EQUIVALENT = 2;
160 | const FIRMWARE_UNSUPPORTED = 3;
161 | exports.FIRMWARE_OFFICIAL_RELEASE = FIRMWARE_OFFICIAL_RELEASE;
162 | exports.FIRMWARE_OFFICIAL_RELEASE_CANDIDATE = FIRMWARE_OFFICIAL_RELEASE_CANDIDATE;
163 | exports.FIRMWARE_CUSTOM_EQUIVALENT = FIRMWARE_CUSTOM_EQUIVALENT;
164 | exports.FIRMWARE_UNSUPPORTED = FIRMWARE_UNSUPPORTED;
165 |
166 | const EQUIVALENCE_REGEX = /E[0-9]+\.[0-9]+\.[0-9]+/g;
167 | exports.EQUIVALENCE_REGEX = EQUIVALENCE_REGEX;
168 |
169 | /* Remove trailing digit and check if description is in list of supported firmware descriptions */
170 |
171 | exports.getFirmwareClassification = (desc) => {
172 |
173 | /* If official firmware or a release candidate of the official firmware */
174 |
175 | if (desc === 'AudioMoth-Firmware-Basic') {
176 |
177 | return FIRMWARE_OFFICIAL_RELEASE;
178 |
179 | }
180 |
181 | if (desc.replace(/-RC\d+$/, '-RC') === 'AudioMoth-Firmware-Basic-RC') {
182 |
183 | return FIRMWARE_OFFICIAL_RELEASE_CANDIDATE;
184 |
185 | }
186 |
187 | const foundEquivalence = desc.match(EQUIVALENCE_REGEX);
188 |
189 | if (foundEquivalence) {
190 |
191 | return FIRMWARE_CUSTOM_EQUIVALENT;
192 |
193 | }
194 |
195 | return FIRMWARE_UNSUPPORTED;
196 |
197 | };
198 |
199 | /**
200 | * @returns -1: A < B, 0: A === B, 1: A > B
201 | */
202 | function compareSemanticVersion (version, major, minor, patch) {
203 |
204 | for (let i = 0; i < 3; i++) {
205 |
206 | const versionNumber = parseInt(version[i]);
207 |
208 | const comparator = i === 0 ? major : i === 1 ? minor : patch;
209 |
210 | if (versionNumber > comparator) {
211 |
212 | return 1;
213 |
214 | } else if (versionNumber < comparator) {
215 |
216 | return -1;
217 |
218 | }
219 |
220 | }
221 |
222 | return 0;
223 |
224 | }
225 |
226 | exports.compareSemanticVersion = compareSemanticVersion;
227 |
228 | exports.isOlderSemanticVersion = (version, major, minor, patch) => {
229 |
230 | return compareSemanticVersion(version, major, minor, patch) === -1;
231 |
232 | };
233 |
234 | exports.isNewerSemanticVersion = (version, major, minor, patch) => {
235 |
236 | return compareSemanticVersion(version, major, minor, patch) === 1;
237 |
238 | };
239 |
240 | exports.isSameSemanticVersion = (version, major, minor, patch) => {
241 |
242 | return compareSemanticVersion(version, major, minor, patch) === 0;
243 |
244 | };
245 |
246 | exports.isOlderOrEqualSemanticVersion = (version, major, minor, patch) => {
247 |
248 | const comparisonResult = compareSemanticVersion(version, major, minor, patch);
249 |
250 | return comparisonResult === -1 || comparisonResult === 0;
251 |
252 | };
253 |
254 | exports.isNewerOrEqualSemanticVersion = (version, major, minor, patch) => {
255 |
256 | const comparisonResult = compareSemanticVersion(version, major, minor, patch);
257 |
258 | return comparisonResult === 0 || comparisonResult === 1;
259 |
260 | };
261 |
262 | /* Version number for the latest firmware */
263 |
264 | const LATEST_FIRMWARE_VERSION_MAJOR = 1;
265 | const LATEST_FIRMWARE_VERSION_MINOR = 11;
266 | const LATEST_FIRMWARE_VERSION_PATCH = 0;
267 |
268 | exports.LATEST_FIRMWARE_VERSION_MAJOR = LATEST_FIRMWARE_VERSION_MAJOR;
269 | exports.LATEST_FIRMWARE_VERSION_MINOR = LATEST_FIRMWARE_VERSION_MINOR;
270 | exports.LATEST_FIRMWARE_VERSION_PATCH = LATEST_FIRMWARE_VERSION_PATCH;
271 | exports.LATEST_FIRMWARE_VERSION_ARRAY = [LATEST_FIRMWARE_VERSION_MAJOR, LATEST_FIRMWARE_VERSION_MINOR, LATEST_FIRMWARE_VERSION_PATCH];
272 | exports.LATEST_FIRMWARE_VERSION_STRING = LATEST_FIRMWARE_VERSION_MAJOR.toString() + '.' + LATEST_FIRMWARE_VERSION_MINOR.toString() + '.' + LATEST_FIRMWARE_VERSION_PATCH.toString();
273 |
274 | /* Time zone modes */
275 |
276 | exports.TIME_ZONE_MODE_UTC = 0;
277 | exports.TIME_ZONE_MODE_LOCAL = 1;
278 | exports.TIME_ZONE_MODE_CUSTOM = 2;
279 | exports.TIME_ZONE_MODE_STRINGS = ['UTC', 'LOCAL', 'CUSTOM'];
280 |
281 | /* Calculation values */
282 |
283 | exports.UINT32_MAX = 0xFFFFFFFF;
284 | exports.UINT16_MAX = 0xFFFF;
285 |
286 | exports.MILLISECONDS_IN_SECOND = 1000;
287 |
288 | exports.SECONDS_IN_MINUTE = 60;
289 | exports.SECONDS_IN_DAY = 86400;
290 |
291 | exports.MINUTES_IN_HOUR = 60;
292 | exports.MINUTES_IN_DAY = 1440;
293 |
294 | /* Schedule limitations */
295 |
296 | exports.MAX_PERIODS = 4;
297 |
298 | /* Sunrise and sunset results */
299 |
300 | exports.SUN_ABOVE_HORIZON = 0;
301 | exports.NORMAL_SOLUTION = 1;
302 | exports.SUN_BELOW_HORIZON = 2;
303 |
304 | exports.DAY_LONGER_THAN_NIGHT = 0;
305 | exports.DAY_EQUAL_TO_NIGHT = 1;
306 | exports.DAY_SHORTER_THAN_NIGHT = 2;
307 |
308 | exports.MINIMUM_SUN_RECORDING_GAP = 60;
309 | exports.SUN_RECORDING_GAP_MULTIPLIER = 4;
310 |
311 | /* Sun schedule modes */
312 |
313 | exports.MODE_BEFORE_SUNRISE_AFTER_SUNRISE = 0;
314 | exports.MODE_BEFORE_SUNSET_AFTER_SUNSET = 1;
315 | exports.MODE_BEFORE_BOTH_AFTER_BOTH = 2;
316 | exports.MODE_BEFORE_SUNSET_AFTER_SUNRISE = 3;
317 | exports.MODE_BEFORE_SUNRISE_AFTER_SUNSET = 4;
318 |
319 | /* Sun events modes */
320 |
321 | exports.SUNRISE_AND_SUNSET = 0;
322 | exports.CIVIL_DAWN_AND_DUSK = 1;
323 | exports.NAUTICAL_DAWN_AND_DUSK = 2;
324 | exports.ASTRONOMICAL_DAWN_AND_DUSK = 3;
325 |
326 | /* First/last recording date range */
327 |
328 | exports.MIN_FIRST_LAST_DATE = '2020-01-01';
329 | exports.MAX_FIRST_LAST_DATE = '2029-12-31';
330 |
--------------------------------------------------------------------------------
/schedule/roundingInput.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * roundingInput.js
3 | * openacousticdevices.info
4 | * January 2024
5 | *****************************************************************************/
6 |
7 | 'use strict';
8 |
9 | const DEFAULT_WIDTH = '60px';
10 | const DEFAULT_HEIGHT = '25px';
11 |
12 | const inputData = {};
13 |
14 | exports.setEnabled = (div, setting) => {
15 |
16 | inputData[div.id].enabled = setting;
17 |
18 | const uiElements = getAllSpans(div);
19 | const textInput = getTextInput(div);
20 |
21 | if (setting) {
22 |
23 | textInput.style.backgroundColor = '';
24 | textInput.style.border = '';
25 | textInput.tabIndex = 0;
26 |
27 | for (let i = 0; i < uiElements.length; i++) {
28 |
29 | uiElements[i].classList.remove('grey');
30 | uiElements[i].style.backgroundColor = '';
31 |
32 | }
33 |
34 | updateGrey(div);
35 |
36 | } else {
37 |
38 | textInput.style.backgroundColor = 'EBEBE4';
39 | textInput.style.border = '1px solid #cccccc';
40 |
41 | for (let i = 0; i < uiElements.length; i++) {
42 |
43 | uiElements[i].classList.add('grey');
44 | uiElements[i].style.backgroundColor = 'EBEBE4';
45 |
46 | }
47 |
48 | textInput.tabIndex = -1;
49 |
50 | }
51 |
52 | };
53 |
54 | /**
55 | * @param {Element} div Parent div element
56 | * @returns Time string
57 | */
58 | function getValue (div) {
59 |
60 | return inputData[div.id].value;
61 |
62 | };
63 |
64 | exports.getValue = getValue;
65 |
66 | // Data structure setters
67 |
68 | function setValue (div, value) {
69 |
70 | const maxValue = getMaxValue();
71 |
72 | inputData[div.id].value = Math.min(value, maxValue);
73 |
74 | updateFieldSpan(div);
75 |
76 | updateGrey(div);
77 |
78 | }
79 |
80 | exports.setValue = setValue;
81 |
82 | function setSelectedIndex (div, index) {
83 |
84 | inputData[div.id].selectedIndex = index;
85 |
86 | }
87 |
88 | function setEntry (div, entry) {
89 |
90 | inputData[div.id].entry = entry;
91 |
92 | }
93 |
94 | function resetEntry (div) {
95 |
96 | inputData[div.id].entry = 0;
97 |
98 | }
99 |
100 | // Data structure getters
101 |
102 | function isEnabled (div) {
103 |
104 | return inputData[div.id].enabled;
105 |
106 | }
107 |
108 | function getSelectedIndex (div) {
109 |
110 | return inputData[div.id].selectedIndex;
111 |
112 | }
113 |
114 | function getEntry (div) {
115 |
116 | return inputData[div.id].entry;
117 |
118 | }
119 |
120 | // Get UI elements
121 |
122 | function getDurationSpan (div) {
123 |
124 | return div.getElementsByClassName('duration-span')[0];
125 |
126 | }
127 |
128 | function getTextInput (div) {
129 |
130 | return div.getElementsByClassName('text-input')[0];
131 |
132 | }
133 |
134 | function getAllSpans (div) {
135 |
136 | const holder = div.getElementsByClassName('holder')[0];
137 | return holder.children;
138 |
139 | }
140 |
141 | // Update values
142 |
143 | function getMaxValue () {
144 |
145 | return 60;
146 |
147 | }
148 |
149 | function updateFieldSpan (div) {
150 |
151 | const value = getValue(div);
152 |
153 | const field = getDurationSpan(div);
154 |
155 | const stringValue = value.toString().padStart(2, '0');
156 |
157 | field.innerText = stringValue;
158 |
159 | }
160 |
161 | // Keypress event handler
162 |
163 | function handleKeyDown (e) {
164 |
165 | const div = e.target.parentNode;
166 |
167 | if (e.key === 'Tab') {
168 |
169 | return;
170 |
171 | }
172 |
173 | e.preventDefault();
174 |
175 | if (e.key === 'ArrowUp') {
176 |
177 | resetEntry(div);
178 |
179 | const currentValue = getValue(div);
180 | const maxValue = getMaxValue();
181 |
182 | const newValue = currentValue + 1 > maxValue ? 0 : currentValue + 1;
183 |
184 | setValue(div, newValue);
185 |
186 | return;
187 |
188 | }
189 |
190 | if (e.key === 'ArrowDown') {
191 |
192 | resetEntry(div);
193 |
194 | const currentValue = getValue(div);
195 | const maxValue = getMaxValue();
196 |
197 | const newValue = currentValue - 1 < 0 ? maxValue : currentValue - 1;
198 |
199 | setValue(div, newValue);
200 |
201 | updateGrey(div);
202 |
203 | return;
204 |
205 | }
206 |
207 | if (e.key === 'Backspace' || e.key === 'Delete') {
208 |
209 | resetEntry(div);
210 | setValue(div, 0);
211 |
212 | updateGrey(div);
213 |
214 | return;
215 |
216 | }
217 |
218 | const patt = /^[0-9]$/;
219 |
220 | if (patt.test(e.key)) {
221 |
222 | const digit = parseInt(e.key);
223 |
224 | let entry = getEntry(div);
225 |
226 | setEntry(div, 10 * entry + digit);
227 | entry = getEntry(div);
228 |
229 | const maxValue = getMaxValue(div);
230 | const value = Math.min(maxValue, entry);
231 |
232 | setValue(div, value);
233 |
234 | if (10 * value > maxValue || (maxValue === 0 && digit > 0)) {
235 |
236 | resetEntry(div);
237 |
238 | }
239 |
240 | }
241 |
242 | }
243 |
244 | /**
245 | * Highlight the span at index selectedIndex
246 | * @param {Element} div Time input div element
247 | */
248 | function highlightInput (div) {
249 |
250 | const inputNode = getTextInput(div);
251 |
252 | const index = getSelectedIndex(div);
253 |
254 | const durationSpan = getDurationSpan(div);
255 |
256 | const deselectedColor = inputNode.style.background;
257 |
258 | if (index === 0) {
259 |
260 | durationSpan.style.color = '';
261 | durationSpan.style.backgroundColor = 'Highlight';
262 | durationSpan.style.color = 'HighlightText';
263 |
264 | } else {
265 |
266 | durationSpan.style.color = '';
267 | durationSpan.style.backgroundColor = deselectedColor;
268 | durationSpan.classList.remove('grey');
269 |
270 | }
271 |
272 | updateGrey(div);
273 |
274 | }
275 |
276 | function updateGrey (div) {
277 |
278 | if (!isEnabled(div)) {
279 |
280 | return;
281 |
282 | }
283 |
284 | // Update grey
285 |
286 | const durationSpanNode = getDurationSpan(div);
287 |
288 | const inputValue = getValue(div);
289 |
290 | if (inputValue < 10) {
291 |
292 | durationSpanNode.classList.add('greyStart');
293 |
294 | } else {
295 |
296 | durationSpanNode.classList.remove('greyStart');
297 |
298 | }
299 |
300 | const selectedIndex = getSelectedIndex(div);
301 |
302 | if (selectedIndex === 0) {
303 |
304 | durationSpanNode.classList.remove('greyStart');
305 |
306 | }
307 |
308 | }
309 |
310 | /**
311 | * Create all the UI elements needed and add a new data structure to the lookup table
312 | * @param {string} divName Name assigned to custom div
313 | * @param {function} focusOutFunction Function run when focus leaves input
314 | * @returns Parent div element
315 | */
316 | exports.create = (divName, focusOutFunction) => {
317 |
318 | const customDiv = document.getElementById(divName);
319 |
320 | let width, height;
321 |
322 | const attributes = customDiv.attributes;
323 |
324 | width = customDiv.style.width;
325 | width = (width === '') ? DEFAULT_WIDTH : width;
326 | height = customDiv.style.height;
327 | height = (height === '') ? DEFAULT_HEIGHT : height;
328 |
329 | const parent = customDiv.parentNode;
330 |
331 | const id = customDiv.id;
332 |
333 | parent.removeChild(customDiv);
334 |
335 | const div = document.createElement('div');
336 | div.style = 'position: relative;';
337 | div.id = id;
338 |
339 | for (let i = 0; i < attributes.length; i++) {
340 |
341 | if (attributes[i].name !== 'style') {
342 |
343 | div.setAttribute(attributes[i].name, attributes[i].value);
344 |
345 | }
346 |
347 | }
348 |
349 | const numSpanStyle1 = 'display: inline-block; text-align: center; width: 15px;';
350 |
351 | const inputNode = document.createElement('input');
352 | inputNode.className = 'text-input';
353 | inputNode.type = 'text';
354 | inputNode.style = 'width: ' + width + '; height: ' + height + '; color: white; caret-color: transparent;';
355 |
356 | div.appendChild(inputNode);
357 |
358 | const blockerNode = document.createElement('div');
359 | blockerNode.style = 'position: absolute; top: 0px; margin-left: 0px; margin-top: 0px; width: 100%; height: 100%;';
360 |
361 | const holderNode = document.createElement('div');
362 | holderNode.className = 'holder';
363 | holderNode.style = 'position: absolute; top: 0px; margin-left: 39px; margin-top: 3px;';
364 |
365 | const durationSpanNode = document.createElement('span');
366 | durationSpanNode.className = 'duration-span';
367 | durationSpanNode.innerText = '00';
368 | durationSpanNode.style = numSpanStyle1;
369 |
370 | holderNode.appendChild(durationSpanNode);
371 |
372 | blockerNode.appendChild(holderNode);
373 | div.appendChild(blockerNode);
374 |
375 | parent.appendChild(div);
376 |
377 | const data = {};
378 |
379 | data.value = 0;
380 | data.width = 2;
381 | data.selectedIndex = -1;
382 | data.enabled = true;
383 |
384 | inputData[div.id] = data;
385 |
386 | updateGrey(div);
387 |
388 | inputNode.addEventListener('keydown', handleKeyDown);
389 |
390 | function handleSpanClick () {
391 |
392 | if (!isEnabled(div)) {
393 |
394 | return;
395 |
396 | }
397 |
398 | setSelectedIndex(div, 0);
399 | highlightInput(div);
400 | inputNode.focus();
401 |
402 | updateGrey(div);
403 |
404 | }
405 |
406 | durationSpanNode.addEventListener('click', handleSpanClick);
407 |
408 | inputNode.addEventListener('focusin', () => {
409 |
410 | if (!isEnabled(div)) {
411 |
412 | inputNode.blur();
413 | return;
414 |
415 | }
416 |
417 | resetEntry(div);
418 |
419 | if (getSelectedIndex(div) === -1) {
420 |
421 | setSelectedIndex(div, 0);
422 | highlightInput(div);
423 |
424 | }
425 |
426 | updateGrey(div);
427 |
428 | });
429 |
430 | inputNode.addEventListener('focusout', () => {
431 |
432 | if (!isEnabled(div)) {
433 |
434 | return;
435 |
436 | }
437 |
438 | setSelectedIndex(div, -1);
439 | highlightInput(div);
440 |
441 | updateGrey(div);
442 |
443 | if (focusOutFunction) {
444 |
445 | focusOutFunction();
446 |
447 | }
448 |
449 | });
450 |
451 | inputNode.addEventListener('click', () => {
452 |
453 | if (!isEnabled(div)) {
454 |
455 | return;
456 |
457 | }
458 |
459 | setSelectedIndex(div, 0);
460 | highlightInput(div);
461 | inputNode.focus();
462 |
463 | });
464 |
465 | return div;
466 |
467 | };
468 |
--------------------------------------------------------------------------------
/processing/aligning.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
Synchronise AudioMoth Files
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | No AudioMoth WAV files selected.
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | Writing WAV files to source folder.
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/map/leaflet.css:
--------------------------------------------------------------------------------
1 | /* required styles */
2 |
3 | .leaflet-map-pane,
4 | .leaflet-tile,
5 | .leaflet-marker-icon,
6 | .leaflet-marker-shadow,
7 | .leaflet-tile-pane,
8 | .leaflet-tile-container,
9 | .leaflet-overlay-pane,
10 | .leaflet-shadow-pane,
11 | .leaflet-marker-pane,
12 | .leaflet-popup-pane,
13 | .leaflet-overlay-pane svg,
14 | .leaflet-zoom-box,
15 | .leaflet-image-layer,
16 | .leaflet-layer {
17 | position: absolute;
18 | left: 0;
19 | top: 0;
20 | }
21 | .leaflet-container {
22 | overflow: hidden;
23 | -ms-touch-action: none;
24 | }
25 | .leaflet-tile,
26 | .leaflet-marker-icon,
27 | .leaflet-marker-shadow {
28 | -webkit-user-select: none;
29 | -moz-user-select: none;
30 | user-select: none;
31 | -webkit-user-drag: none;
32 | }
33 | .leaflet-marker-icon,
34 | .leaflet-marker-shadow {
35 | display: block;
36 | }
37 | /* map is broken in FF if you have max-width: 100% on tiles */
38 | .leaflet-container img {
39 | max-width: none !important;
40 | }
41 | /* stupid Android 2 doesn't understand "max-width: none" properly */
42 | .leaflet-container img.leaflet-image-layer {
43 | max-width: 15000px !important;
44 | }
45 | .leaflet-tile {
46 | filter: inherit;
47 | visibility: hidden;
48 | }
49 | .leaflet-tile-loaded {
50 | visibility: inherit;
51 | }
52 | .leaflet-zoom-box {
53 | width: 0;
54 | height: 0;
55 | }
56 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
57 | .leaflet-overlay-pane svg {
58 | -moz-user-select: none;
59 | }
60 |
61 | .leaflet-tile-pane { z-index: 2; }
62 | .leaflet-objects-pane { z-index: 3; }
63 | .leaflet-overlay-pane { z-index: 4; }
64 | .leaflet-shadow-pane { z-index: 5; }
65 | .leaflet-marker-pane { z-index: 6; }
66 | .leaflet-popup-pane { z-index: 7; }
67 |
68 | .leaflet-vml-shape {
69 | width: 1px;
70 | height: 1px;
71 | }
72 | .lvml {
73 | behavior: url(#default#VML);
74 | display: inline-block;
75 | position: absolute;
76 | }
77 |
78 |
79 | /* control positioning */
80 |
81 | .leaflet-control {
82 | position: relative;
83 | z-index: 7;
84 | pointer-events: auto;
85 | }
86 | .leaflet-top,
87 | .leaflet-bottom {
88 | position: absolute;
89 | z-index: 1000;
90 | pointer-events: none;
91 | }
92 | .leaflet-top {
93 | top: 0;
94 | }
95 | .leaflet-right {
96 | right: 0;
97 | }
98 | .leaflet-bottom {
99 | bottom: 0;
100 | }
101 | .leaflet-left {
102 | left: 0;
103 | }
104 | .leaflet-control {
105 | float: left;
106 | clear: both;
107 | }
108 | .leaflet-right .leaflet-control {
109 | float: right;
110 | }
111 | .leaflet-top .leaflet-control {
112 | margin-top: 10px;
113 | }
114 | .leaflet-bottom .leaflet-control {
115 | margin-bottom: 10px;
116 | }
117 | .leaflet-left .leaflet-control {
118 | margin-left: 10px;
119 | }
120 | .leaflet-right .leaflet-control {
121 | margin-right: 10px;
122 | }
123 |
124 |
125 | /* zoom and fade animations */
126 |
127 | .leaflet-fade-anim .leaflet-tile,
128 | .leaflet-fade-anim .leaflet-popup {
129 | opacity: 0;
130 | -webkit-transition: opacity 0.2s linear;
131 | -moz-transition: opacity 0.2s linear;
132 | -o-transition: opacity 0.2s linear;
133 | transition: opacity 0.2s linear;
134 | }
135 | .leaflet-fade-anim .leaflet-tile-loaded,
136 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
137 | opacity: 1;
138 | }
139 |
140 | .leaflet-zoom-anim .leaflet-zoom-animated {
141 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
142 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
143 | -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
144 | transition: transform 0.25s cubic-bezier(0,0,0.25,1);
145 | }
146 | .leaflet-zoom-anim .leaflet-tile,
147 | .leaflet-pan-anim .leaflet-tile,
148 | .leaflet-touching .leaflet-zoom-animated {
149 | -webkit-transition: none;
150 | -moz-transition: none;
151 | -o-transition: none;
152 | transition: none;
153 | }
154 |
155 | .leaflet-zoom-anim .leaflet-zoom-hide {
156 | visibility: hidden;
157 | }
158 |
159 |
160 | /* cursors */
161 |
162 | .leaflet-clickable {
163 | cursor: pointer;
164 | }
165 | .leaflet-container {
166 | cursor: -webkit-grab;
167 | cursor: -moz-grab;
168 | }
169 | .leaflet-popup-pane,
170 | .leaflet-control {
171 | cursor: auto;
172 | }
173 | .leaflet-dragging .leaflet-container,
174 | .leaflet-dragging .leaflet-clickable {
175 | cursor: move;
176 | cursor: -webkit-grabbing;
177 | cursor: -moz-grabbing;
178 | }
179 |
180 |
181 | /* visual tweaks */
182 |
183 | .leaflet-container {
184 | background: #ddd;
185 | outline: 0;
186 | }
187 | .leaflet-container a {
188 | color: #0078A8;
189 | }
190 | .leaflet-container a.leaflet-active {
191 | outline: 2px solid orange;
192 | }
193 | .leaflet-zoom-box {
194 | border: 2px dotted #38f;
195 | background: rgba(255,255,255,0.5);
196 | }
197 |
198 |
199 | /* general typography */
200 | .leaflet-container {
201 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
202 | }
203 |
204 |
205 | /* general toolbar styles */
206 |
207 | .leaflet-bar {
208 | box-shadow: 0 1px 5px rgba(0,0,0,0.65);
209 | border-radius: 4px;
210 | }
211 | .leaflet-bar a,
212 | .leaflet-bar a:hover {
213 | background-color: #fff;
214 | border-bottom: 1px solid #ccc;
215 | width: 26px;
216 | height: 26px;
217 | line-height: 26px;
218 | display: block;
219 | text-align: center;
220 | text-decoration: none;
221 | color: black;
222 | }
223 | .leaflet-bar a,
224 | .leaflet-control-layers-toggle {
225 | background-position: 50% 50%;
226 | background-repeat: no-repeat;
227 | display: block;
228 | }
229 | .leaflet-bar a:hover {
230 | background-color: #f4f4f4;
231 | }
232 | .leaflet-bar a:first-child {
233 | border-top-left-radius: 4px;
234 | border-top-right-radius: 4px;
235 | }
236 | .leaflet-bar a:last-child {
237 | border-bottom-left-radius: 4px;
238 | border-bottom-right-radius: 4px;
239 | border-bottom: none;
240 | }
241 | .leaflet-bar a.leaflet-disabled {
242 | cursor: default;
243 | background-color: #f4f4f4;
244 | color: #bbb;
245 | }
246 |
247 | .leaflet-touch .leaflet-bar a {
248 | width: 30px;
249 | height: 30px;
250 | line-height: 30px;
251 | }
252 |
253 |
254 | /* zoom control */
255 |
256 | .leaflet-control-zoom-in,
257 | .leaflet-control-zoom-out {
258 | font: bold 18px 'Lucida Console', Monaco, monospace;
259 | text-indent: 1px;
260 | }
261 | .leaflet-control-zoom-out {
262 | font-size: 20px;
263 | }
264 |
265 | .leaflet-touch .leaflet-control-zoom-in {
266 | font-size: 22px;
267 | }
268 | .leaflet-touch .leaflet-control-zoom-out {
269 | font-size: 24px;
270 | }
271 |
272 |
273 | /* layers control */
274 |
275 | .leaflet-control-layers {
276 | box-shadow: 0 1px 5px rgba(0,0,0,0.4);
277 | background: #fff;
278 | border-radius: 5px;
279 | }
280 | .leaflet-control-layers-toggle {
281 | background-image: url(images/layers.png);
282 | width: 36px;
283 | height: 36px;
284 | }
285 | .leaflet-retina .leaflet-control-layers-toggle {
286 | background-image: url(images/layers-2x.png);
287 | background-size: 26px 26px;
288 | }
289 | .leaflet-touch .leaflet-control-layers-toggle {
290 | width: 44px;
291 | height: 44px;
292 | }
293 | .leaflet-control-layers .leaflet-control-layers-list,
294 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle {
295 | display: none;
296 | }
297 | .leaflet-control-layers-expanded .leaflet-control-layers-list {
298 | display: block;
299 | position: relative;
300 | }
301 | .leaflet-control-layers-expanded {
302 | padding: 6px 10px 6px 6px;
303 | color: #333;
304 | background: #fff;
305 | }
306 | .leaflet-control-layers-selector {
307 | margin-top: 2px;
308 | position: relative;
309 | top: 1px;
310 | }
311 | .leaflet-control-layers label {
312 | display: block;
313 | }
314 | .leaflet-control-layers-separator {
315 | height: 0;
316 | border-top: 1px solid #ddd;
317 | margin: 5px -10px 5px -6px;
318 | }
319 |
320 |
321 | /* attribution and scale controls */
322 |
323 | .leaflet-container .leaflet-control-attribution {
324 | background: #fff;
325 | background: rgba(255, 255, 255, 0.7);
326 | margin: 0;
327 | }
328 | .leaflet-control-attribution,
329 | .leaflet-control-scale-line {
330 | padding: 0 5px;
331 | color: #333;
332 | }
333 | .leaflet-control-attribution a {
334 | text-decoration: none;
335 | }
336 | .leaflet-control-attribution a:hover {
337 | text-decoration: underline;
338 | }
339 | .leaflet-container .leaflet-control-attribution,
340 | .leaflet-container .leaflet-control-scale {
341 | font-size: 11px;
342 | }
343 | .leaflet-left .leaflet-control-scale {
344 | margin-left: 5px;
345 | }
346 | .leaflet-bottom .leaflet-control-scale {
347 | margin-bottom: 5px;
348 | }
349 | .leaflet-control-scale-line {
350 | border: 2px solid #777;
351 | border-top: none;
352 | line-height: 1.1;
353 | padding: 2px 5px 1px;
354 | font-size: 11px;
355 | white-space: nowrap;
356 | overflow: hidden;
357 | -moz-box-sizing: content-box;
358 | box-sizing: content-box;
359 |
360 | background: #fff;
361 | background: rgba(255, 255, 255, 0.5);
362 | }
363 | .leaflet-control-scale-line:not(:first-child) {
364 | border-top: 2px solid #777;
365 | border-bottom: none;
366 | margin-top: -2px;
367 | }
368 | .leaflet-control-scale-line:not(:first-child):not(:last-child) {
369 | border-bottom: 2px solid #777;
370 | }
371 |
372 | .leaflet-touch .leaflet-control-attribution,
373 | .leaflet-touch .leaflet-control-layers,
374 | .leaflet-touch .leaflet-bar {
375 | box-shadow: none;
376 | }
377 | .leaflet-touch .leaflet-control-layers,
378 | .leaflet-touch .leaflet-bar {
379 | border: 2px solid rgba(0,0,0,0.2);
380 | background-clip: padding-box;
381 | }
382 |
383 |
384 | /* popup */
385 |
386 | .leaflet-popup {
387 | position: absolute;
388 | text-align: center;
389 | }
390 | .leaflet-popup-content-wrapper {
391 | padding: 1px;
392 | text-align: left;
393 | border-radius: 12px;
394 | }
395 | .leaflet-popup-content {
396 | margin: 13px 19px;
397 | line-height: 1.4;
398 | }
399 | .leaflet-popup-content p {
400 | margin: 18px 0;
401 | }
402 | .leaflet-popup-tip-container {
403 | margin: 0 auto;
404 | width: 40px;
405 | height: 20px;
406 | position: relative;
407 | overflow: hidden;
408 | }
409 | .leaflet-popup-tip {
410 | width: 17px;
411 | height: 17px;
412 | padding: 1px;
413 |
414 | margin: -10px auto 0;
415 |
416 | -webkit-transform: rotate(45deg);
417 | -moz-transform: rotate(45deg);
418 | -ms-transform: rotate(45deg);
419 | -o-transform: rotate(45deg);
420 | transform: rotate(45deg);
421 | }
422 | .leaflet-popup-content-wrapper,
423 | .leaflet-popup-tip {
424 | background: white;
425 |
426 | box-shadow: 0 3px 14px rgba(0,0,0,0.4);
427 | }
428 | .leaflet-container a.leaflet-popup-close-button {
429 | position: absolute;
430 | top: 0;
431 | right: 0;
432 | padding: 4px 4px 0 0;
433 | text-align: center;
434 | width: 18px;
435 | height: 14px;
436 | font: 16px/14px Tahoma, Verdana, sans-serif;
437 | color: #c3c3c3;
438 | text-decoration: none;
439 | font-weight: bold;
440 | background: transparent;
441 | }
442 | .leaflet-container a.leaflet-popup-close-button:hover {
443 | color: #999;
444 | }
445 | .leaflet-popup-scrolled {
446 | overflow: auto;
447 | border-bottom: 1px solid #ddd;
448 | border-top: 1px solid #ddd;
449 | }
450 |
451 | .leaflet-oldie .leaflet-popup-content-wrapper {
452 | zoom: 1;
453 | }
454 | .leaflet-oldie .leaflet-popup-tip {
455 | width: 24px;
456 | margin: 0 auto;
457 |
458 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
459 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
460 | }
461 | .leaflet-oldie .leaflet-popup-tip-container {
462 | margin-top: -1px;
463 | }
464 |
465 | .leaflet-oldie .leaflet-control-zoom,
466 | .leaflet-oldie .leaflet-control-layers,
467 | .leaflet-oldie .leaflet-popup-content-wrapper,
468 | .leaflet-oldie .leaflet-popup-tip {
469 | border: 1px solid #999;
470 | }
471 |
472 |
473 | /* div icon */
474 |
475 | .leaflet-div-icon {
476 | background: #fff;
477 | border: 1px solid #666;
478 | }
--------------------------------------------------------------------------------
/processing/uiSplit.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * uiSplit.js
3 | * openacousticdevices.info
4 | * February 2021
5 | *****************************************************************************/
6 |
7 | 'use strict';
8 |
9 | /* global document */
10 |
11 | const electron = require('electron');
12 | const {dialog, getCurrentWindow} = require('@electron/remote');
13 |
14 | /* Get functions which control elements common to the expansion, split, and downsample windows */
15 | const ui = require('./uiCommon.js');
16 | const uiOutput = require('./uiOutput.js');
17 | const uiInput = require('./uiInput.js');
18 |
19 | const path = require('path');
20 | const fs = require('fs');
21 |
22 | const audiomothUtils = require('audiomoth-utils');
23 |
24 | const currentWindow = getCurrentWindow();
25 |
26 | const MAX_LENGTHS = [1, 5, 10, 15, 30, 60, 300, 600, 3600];
27 |
28 | const maxLengthRadios = document.getElementsByName('max-length-radio');
29 |
30 | const selectionRadios = document.getElementsByName('selection-radio');
31 |
32 | const prefixCheckbox = document.getElementById('prefix-checkbox');
33 | const prefixInput = document.getElementById('prefix-input');
34 |
35 | const fileLabel = document.getElementById('file-label');
36 | const fileButton = document.getElementById('file-button');
37 | const splitButton = document.getElementById('split-button');
38 |
39 | let files = [];
40 | let splitting = false;
41 |
42 | const DEFAULT_SLEEP_AMOUNT = 2000;
43 |
44 | /* Disable UI elements in main window while progress bar is open and split is in progress */
45 |
46 | function disableUI () {
47 |
48 | fileButton.disabled = true;
49 | splitButton.disabled = true;
50 | selectionRadios[0].disabled = true;
51 | selectionRadios[1].disabled = true;
52 |
53 | for (let i = 0; i < maxLengthRadios.length; i++) {
54 |
55 | maxLengthRadios[i].disabled = true;
56 |
57 | }
58 |
59 | uiOutput.disableOutputCheckbox();
60 | uiOutput.disableOutputButton();
61 |
62 | prefixCheckbox.disabled = true;
63 | prefixInput.disabled = true;
64 |
65 | }
66 |
67 | function enableUI () {
68 |
69 | fileButton.disabled = false;
70 | splitButton.disabled = false;
71 | selectionRadios[0].disabled = false;
72 | selectionRadios[1].disabled = false;
73 |
74 | for (let i = 0; i < maxLengthRadios.length; i++) {
75 |
76 | maxLengthRadios[i].disabled = false;
77 |
78 | }
79 |
80 | uiOutput.enableOutputCheckbox();
81 | uiOutput.enableOutputButton();
82 |
83 | prefixCheckbox.disabled = false;
84 |
85 | if (prefixCheckbox.checked) {
86 |
87 | prefixInput.disabled = false;
88 |
89 | }
90 |
91 | splitting = false;
92 |
93 | }
94 |
95 | /* Split selected files */
96 |
97 | function splitFiles () {
98 |
99 | if (!files) {
100 |
101 | return;
102 |
103 | }
104 |
105 | let successCount = 0;
106 | let errorCount = 0;
107 | const errors = [];
108 | const errorFiles = [];
109 |
110 | let errorFilePath;
111 |
112 | let sleepAmount = DEFAULT_SLEEP_AMOUNT;
113 | let successesWithoutError = 0;
114 |
115 | const unwrittenErrors = [];
116 | let lastErrorWrite = -1;
117 |
118 | let errorFileStream;
119 |
120 | for (let i = 0; i < files.length; i++) {
121 |
122 | /* If progress bar is closed, the split task is considered cancelled. This will contact the main thread and ask if that has happened */
123 |
124 | const cancelled = electron.ipcRenderer.sendSync('poll-split-cancelled');
125 |
126 | if (cancelled) {
127 |
128 | console.log('Split cancelled.');
129 | enableUI();
130 | return;
131 |
132 | }
133 |
134 | /* Let the main thread know what value to set the progress bar to */
135 |
136 | electron.ipcRenderer.send('set-split-bar-progress', i, 0);
137 |
138 | const maxLength = MAX_LENGTHS[ui.getSelectedRadioValue('max-length-radio')];
139 |
140 | console.log('Splitting:', files[i]);
141 | console.log('Maximum file length:', maxLength);
142 |
143 | console.log('-');
144 |
145 | /* Check if the optional prefix/output directory setttings are being used. If left as null, splitter will put file(s) in the same directory as the input with no prefix */
146 |
147 | let outputPath = null;
148 |
149 | if (uiOutput.isCustomDestinationEnabled()) {
150 |
151 | outputPath = uiOutput.getOutputDir();
152 |
153 | if (uiOutput.isCreateSubdirectoriesEnabled() && selectionRadios[1].checked) {
154 |
155 | const dirnames = path.dirname(files[i]).replace(/\\/g, '/').split('/');
156 |
157 | const folderName = dirnames[dirnames.length - 1];
158 |
159 | outputPath = path.join(outputPath, folderName);
160 |
161 | if (!fs.existsSync(outputPath)) {
162 |
163 | fs.mkdirSync(outputPath);
164 |
165 | }
166 |
167 | }
168 |
169 | }
170 |
171 | const prefix = (prefixCheckbox.checked && prefixInput.value !== '') ? prefixInput.value : null;
172 |
173 | const response = audiomothUtils.split(files[i], outputPath, prefix, maxLength, (progress) => {
174 |
175 | electron.ipcRenderer.send('set-split-bar-progress', i, progress);
176 | electron.ipcRenderer.send('set-split-bar-file', i, path.basename(files[i]));
177 |
178 | });
179 |
180 | if (response.success) {
181 |
182 | successCount++;
183 | successesWithoutError++;
184 |
185 | if (successesWithoutError >= 10) {
186 |
187 | sleepAmount = DEFAULT_SLEEP_AMOUNT;
188 |
189 | }
190 |
191 | } else {
192 |
193 | /* Add error to log file */
194 |
195 | unwrittenErrors.push(errorCount);
196 | successesWithoutError = 0;
197 | errorCount++;
198 | errors.push(response.error);
199 | errorFiles.push(files[i]);
200 |
201 | electron.ipcRenderer.send('set-split-bar-error', path.basename(files[i]));
202 |
203 | if (errorCount === 1) {
204 |
205 | const errorFileLocation = uiOutput.isCustomDestinationEnabled() ? uiOutput.getOutputDir() : path.dirname(errorFiles[0]);
206 | errorFilePath = path.join(errorFileLocation, 'ERRORS.TXT');
207 | errorFileStream = fs.createWriteStream(errorFilePath, {flags: 'a'});
208 |
209 | errorFileStream.write('-- Split --\r\n');
210 |
211 | }
212 |
213 | const currentTime = new Date();
214 | const timeSinceLastErrorWrite = currentTime - lastErrorWrite;
215 |
216 | if (timeSinceLastErrorWrite > 1000 || lastErrorWrite === -1) {
217 |
218 | lastErrorWrite = new Date();
219 |
220 | const unwrittenErrorCount = unwrittenErrors.length;
221 |
222 | console.log('Writing', unwrittenErrorCount, 'errors');
223 |
224 | let fileContent = '';
225 |
226 | for (let e = 0; e < unwrittenErrorCount; e++) {
227 |
228 | const unwrittenErrorIndex = unwrittenErrors.pop();
229 | fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\r\n';
230 |
231 | }
232 |
233 | try {
234 |
235 | errorFileStream.write(fileContent);
236 |
237 | console.log('Error summary written to ' + errorFilePath);
238 |
239 | } catch (err) {
240 |
241 | console.error(err);
242 | electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, true);
243 | return;
244 |
245 | }
246 |
247 | }
248 |
249 | ui.sleep(sleepAmount);
250 | sleepAmount = sleepAmount / 2;
251 |
252 | }
253 |
254 | }
255 |
256 | /* If any errors occurred, do a final error write */
257 |
258 | const unwrittenErrorCount = unwrittenErrors.length;
259 |
260 | if (unwrittenErrorCount > 0) {
261 |
262 | console.log('Writing remaining', unwrittenErrorCount, 'errors');
263 |
264 | let fileContent = '';
265 |
266 | for (let e = 0; e < unwrittenErrorCount; e++) {
267 |
268 | const unwrittenErrorIndex = unwrittenErrors.pop();
269 | fileContent += path.basename(errorFiles[unwrittenErrorIndex]) + ' - ' + errors[unwrittenErrorIndex] + '\r\n';
270 |
271 | }
272 |
273 | try {
274 |
275 | errorFileStream.write(fileContent);
276 |
277 | console.log('Error summary written to ' + errorFilePath);
278 |
279 | errorFileStream.end();
280 |
281 | } catch (err) {
282 |
283 | console.error(err);
284 | electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, true);
285 | return;
286 |
287 | }
288 |
289 | }
290 |
291 | /* Notify main thread that split is complete so progress bar is closed */
292 |
293 | electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, false);
294 |
295 | }
296 |
297 | /* When the progress bar is complete and the summary window at the end has been displayed for a fixed amount of time, it will close and this re-enables the UI */
298 |
299 | electron.ipcRenderer.on('split-summary-closed', enableUI);
300 |
301 | /* Update label to reflect new file/folder selection */
302 |
303 | function updateInputDirectoryDisplay (directoryArray) {
304 |
305 | if (!directoryArray || directoryArray.length === 0) {
306 |
307 | fileLabel.innerHTML = 'No AudioMoth WAV files selected.';
308 | splitButton.disabled = true;
309 |
310 | } else {
311 |
312 | fileLabel.innerHTML = 'Found ';
313 | fileLabel.innerHTML += directoryArray.length + ' AudioMoth WAV file';
314 | fileLabel.innerHTML += (directoryArray.length === 1 ? '' : 's');
315 | fileLabel.innerHTML += '.';
316 | splitButton.disabled = false;
317 |
318 | }
319 |
320 | }
321 |
322 | /* Reset UI back to default state, clearing the selected files */
323 |
324 | function resetUI () {
325 |
326 | files = [];
327 |
328 | fileLabel.innerHTML = 'No AudioMoth WAV files selected.';
329 |
330 | splitButton.disabled = true;
331 |
332 | ui.updateButtonText();
333 |
334 | }
335 |
336 | /* Whenever the file/folder radio button changes, reset the UI */
337 |
338 | selectionRadios[0].addEventListener('change', resetUI);
339 | selectionRadios[1].addEventListener('change', resetUI);
340 |
341 | /* Select/process file(s) buttons */
342 |
343 | fileButton.addEventListener('click', () => {
344 |
345 | const fileRegex = audiomothUtils.getFilenameRegex(audiomothUtils.SPLIT);
346 |
347 | files = uiInput.selectRecordings(fileRegex);
348 |
349 | updateInputDirectoryDisplay(files);
350 |
351 | ui.updateButtonText();
352 |
353 | });
354 |
355 | splitButton.addEventListener('click', () => {
356 |
357 | if (splitting) {
358 |
359 | return;
360 |
361 | }
362 |
363 | if ((!prefixCheckbox.checked || prefixInput.value === '') && (!uiOutput.isCustomDestinationEnabled() || uiOutput.getOutputDir() === '')) {
364 |
365 | dialog.showMessageBox(currentWindow, {
366 | type: 'error',
367 | title: 'Cannot split with current settings',
368 | message: 'Without a prefix or custom destination, splitting will overwrite the original file. Set one of these values to continue.'
369 | });
370 |
371 | return;
372 |
373 | }
374 |
375 | /* Check if output location is the same as input */
376 |
377 | for (let i = 0; i < files.length; i++) {
378 |
379 | if (!prefixCheckbox.checked || prefixInput.value === '') {
380 |
381 | /* If folder mode is enabled, the input folder is the same as the output and subdirectories are enabled, files will be overwritten as the paths will match */
382 |
383 | if (selectionRadios[1].checked && uiOutput.isCustomDestinationEnabled() && uiOutput.isCreateSubdirectoriesEnabled()) {
384 |
385 | /* Get the parent folder of the selected file and compare that to the output directory */
386 |
387 | const fileFolderPath = path.dirname(path.dirname(files[i]));
388 |
389 | if (uiOutput.getOutputDir() === fileFolderPath) {
390 |
391 | dialog.showMessageBox(currentWindow, {
392 | type: 'error',
393 | title: 'Cannot split with current settings',
394 | message: 'Output destination is the same as input destination and no prefix is selected.'
395 | });
396 |
397 | return;
398 |
399 | }
400 |
401 | }
402 |
403 | if (uiOutput.getOutputDir() === path.dirname(files[i])) {
404 |
405 | dialog.showMessageBox(currentWindow, {
406 | type: 'error',
407 | title: 'Cannot split with current settings',
408 | message: 'Output destination is the same as input destination and no prefix is selected.'
409 | });
410 |
411 | return;
412 |
413 | }
414 |
415 | }
416 |
417 | }
418 |
419 | splitting = true;
420 | disableUI();
421 |
422 | electron.ipcRenderer.send('start-split-bar', files.length);
423 | setTimeout(splitFiles, 2000);
424 |
425 | });
426 |
--------------------------------------------------------------------------------
/schedule/sunIntervalInput.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * sunIntervalInput.js
3 | * openacousticdevices.info
4 | * November 2023
5 | *****************************************************************************/
6 |
7 | 'use strict';
8 |
9 | const DEFAULT_WIDTH = '60px';
10 | const DEFAULT_HEIGHT = '25px';
11 |
12 | const inputData = {};
13 |
14 | function isAlternateImplementation (div) {
15 |
16 | return inputData[div.id].alternateImplementation;
17 |
18 | }
19 |
20 | exports.setEnabled = (div, setting) => {
21 |
22 | inputData[div.id].enabled = setting;
23 |
24 | const uiElements = getAllSpans(div);
25 | const textInput = getTextInput(div);
26 |
27 | if (setting) {
28 |
29 | textInput.style.backgroundColor = '';
30 | textInput.style.border = '';
31 | textInput.tabIndex = 0;
32 |
33 | for (let i = 0; i < uiElements.length; i++) {
34 |
35 | uiElements[i].classList.remove('grey');
36 | uiElements[i].style.backgroundColor = '';
37 |
38 | }
39 |
40 | updateGrey(div);
41 |
42 | } else {
43 |
44 | textInput.style.backgroundColor = 'EBEBE4';
45 | textInput.style.border = '1px solid #cccccc';
46 |
47 | for (let i = 0; i < uiElements.length; i++) {
48 |
49 | uiElements[i].classList.add('grey');
50 | uiElements[i].style.backgroundColor = 'EBEBE4';
51 |
52 | }
53 |
54 | textInput.tabIndex = -1;
55 |
56 | }
57 |
58 | };
59 |
60 | /**
61 | * @param {Element} div Parent div element
62 | * @returns Time string
63 | */
64 | function getValue (div) {
65 |
66 | return inputData[div.id].value;
67 |
68 | };
69 |
70 | exports.getValue = getValue;
71 |
72 | // Data structure setters
73 |
74 | function setValue (div, value) {
75 |
76 | const maxValue = getMaxValue();
77 |
78 | inputData[div.id].value = Math.min(value, maxValue);
79 |
80 | updateFieldSpan(div);
81 |
82 | updateGrey(div);
83 |
84 | }
85 |
86 | exports.setValue = setValue;
87 |
88 | function setSelectedIndex (div, index) {
89 |
90 | inputData[div.id].selectedIndex = index;
91 |
92 | }
93 |
94 | function setEntry (div, entry) {
95 |
96 | inputData[div.id].entry = entry;
97 |
98 | }
99 |
100 | function resetEntry (div) {
101 |
102 | inputData[div.id].entry = isAlternateImplementation(div) ? '' : 0;
103 |
104 | }
105 |
106 | // Data structure getters
107 |
108 | function isEnabled (div) {
109 |
110 | return inputData[div.id].enabled;
111 |
112 | }
113 |
114 | function getSelectedIndex (div) {
115 |
116 | return inputData[div.id].selectedIndex;
117 |
118 | }
119 |
120 | function getEntry (div) {
121 |
122 | return inputData[div.id].entry;
123 |
124 | }
125 |
126 | function getWidth (div) {
127 |
128 | return inputData[div.id].width;
129 |
130 | }
131 |
132 | // Get UI elements
133 |
134 | function getDurationSpan0 (div) {
135 |
136 | return div.getElementsByClassName('duration-span0')[0];
137 |
138 | }
139 |
140 | function getDurationSpan1 (div) {
141 |
142 | return div.getElementsByClassName('duration-span1')[0];
143 |
144 | }
145 |
146 | function getTextInput (div) {
147 |
148 | return div.getElementsByClassName('text-input')[0];
149 |
150 | }
151 |
152 | function getAllSpans (div) {
153 |
154 | const holder = div.getElementsByClassName('holder')[0];
155 | return holder.children;
156 |
157 | }
158 |
159 | // Update values
160 |
161 | function getMaxValue () {
162 |
163 | return 360;
164 |
165 | }
166 |
167 | function updateFieldSpan (div) {
168 |
169 | const value = getValue(div);
170 |
171 | const field0 = getDurationSpan0(div);
172 | const field1 = getDurationSpan1(div);
173 |
174 | const stringValue = value.toString().padStart(3, '0');
175 |
176 | field0.innerText = stringValue.slice(0, 1);
177 | field1.innerText = stringValue.slice(1);
178 |
179 | }
180 |
181 | // Keypress event handler
182 |
183 | function handleKeyDown (e) {
184 |
185 | const div = e.target.parentNode;
186 |
187 | if (e.key === 'Tab') {
188 |
189 | return;
190 |
191 | }
192 |
193 | e.preventDefault();
194 |
195 | if (e.key === 'ArrowUp') {
196 |
197 | resetEntry(div);
198 |
199 | const currentValue = getValue(div);
200 | const maxValue = getMaxValue();
201 |
202 | const newValue = currentValue + 1 > maxValue ? 0 : currentValue + 1;
203 |
204 | setValue(div, newValue);
205 |
206 | return;
207 |
208 | }
209 |
210 | if (e.key === 'ArrowDown') {
211 |
212 | resetEntry(div);
213 |
214 | const currentValue = getValue(div);
215 | const maxValue = getMaxValue();
216 |
217 | const newValue = currentValue - 1 < 0 ? maxValue : currentValue - 1;
218 |
219 | setValue(div, newValue);
220 |
221 | updateGrey(div);
222 |
223 | return;
224 |
225 | }
226 |
227 | if (e.key === 'Backspace' || e.key === 'Delete') {
228 |
229 | resetEntry(div);
230 | setValue(div, 0);
231 |
232 | updateGrey(div);
233 |
234 | return;
235 |
236 | }
237 |
238 | const patt = /^[0-9]$/;
239 |
240 | if (patt.test(e.key)) {
241 |
242 | const digit = parseInt(e.key);
243 |
244 | let entry = getEntry(div);
245 |
246 | if (isAlternateImplementation(div)) {
247 |
248 | setEntry(div, entry + digit);
249 | entry = getEntry(div);
250 |
251 | const maxValue = getMaxValue(div);
252 | const value = Math.min(maxValue, parseInt(entry));
253 |
254 | setValue(div, value);
255 |
256 | const width = getWidth(div);
257 |
258 | if (10 * value > maxValue || (maxValue === 0 && digit > 0) || entry.length === width) {
259 |
260 | resetEntry(div);
261 |
262 | }
263 |
264 | } else {
265 |
266 | setEntry(div, 10 * entry + digit);
267 | entry = getEntry(div);
268 |
269 | const maxValue = getMaxValue(div);
270 | const value = Math.min(maxValue, entry);
271 |
272 | setValue(div, value);
273 |
274 | if (10 * value > maxValue || (maxValue === 0 && digit > 0)) {
275 |
276 | resetEntry(div);
277 |
278 | }
279 |
280 | }
281 |
282 | }
283 |
284 | }
285 |
286 | /**
287 | * Highlight the span at index selectedIndex
288 | * @param {Element} div Time input div element
289 | */
290 | function highlightInput (div) {
291 |
292 | const inputNode = getTextInput(div);
293 |
294 | const index = getSelectedIndex(div);
295 |
296 | const durationSpan0 = getDurationSpan0(div);
297 | const durationSpan1 = getDurationSpan1(div);
298 |
299 | const deselectedColor = inputNode.style.background;
300 |
301 | if (index === 0) {
302 |
303 | durationSpan0.style.color = '';
304 | durationSpan0.style.backgroundColor = 'Highlight';
305 | durationSpan0.style.color = 'HighlightText';
306 | durationSpan1.style.color = '';
307 | durationSpan1.style.backgroundColor = 'Highlight';
308 | durationSpan1.style.color = 'HighlightText';
309 |
310 | } else {
311 |
312 | durationSpan0.style.color = '';
313 | durationSpan0.style.backgroundColor = deselectedColor;
314 | durationSpan0.classList.remove('grey');
315 | durationSpan1.style.color = '';
316 | durationSpan1.style.backgroundColor = deselectedColor;
317 | durationSpan1.classList.remove('grey');
318 |
319 | }
320 |
321 | updateGrey(div);
322 |
323 | }
324 |
325 | function updateGrey (div) {
326 |
327 | if (!isEnabled(div)) {
328 |
329 | return;
330 |
331 | }
332 |
333 | // Update grey
334 |
335 | const durationSpanNode0 = getDurationSpan0(div);
336 | const durationSpanNode1 = getDurationSpan1(div);
337 |
338 | const inputValue = getValue(div);
339 |
340 | if (inputValue < 10) {
341 |
342 | durationSpanNode0.classList.add('allGrey');
343 | durationSpanNode1.classList.add('greyStart');
344 |
345 | } else if (inputValue < 100) {
346 |
347 | durationSpanNode0.classList.add('allGrey');
348 | durationSpanNode1.classList.remove('greyStart');
349 |
350 | } else {
351 |
352 | durationSpanNode0.classList.remove('allGrey');
353 | durationSpanNode1.classList.remove('greyStart');
354 |
355 | }
356 |
357 | const selectedIndex = getSelectedIndex(div);
358 |
359 | if (selectedIndex === 0) {
360 |
361 | durationSpanNode0.classList.remove('allGrey');
362 | durationSpanNode1.classList.remove('greyStart');
363 |
364 | }
365 |
366 | }
367 |
368 | /**
369 | * Create all the UI elements needed and add a new data structure to the lookup table
370 | * @param {string} divName Name assigned to custom div
371 | * @param {boolean} alternateImplementation Whether or not to use alternate implementation which allows leading zeroes
372 | * @param {function} focusOutFunction Function run when focus leaves input
373 | * @returns Parent div element
374 | */
375 | exports.create = (divName, alternateImplementation, focusOutFunction) => {
376 |
377 | const customDiv = document.getElementById(divName);
378 |
379 | let width, height;
380 |
381 | const attributes = customDiv.attributes;
382 |
383 | width = customDiv.style.width;
384 | width = (width === '') ? DEFAULT_WIDTH : width;
385 | height = customDiv.style.height;
386 | height = (height === '') ? DEFAULT_HEIGHT : height;
387 |
388 | const parent = customDiv.parentNode;
389 |
390 | const id = customDiv.id;
391 |
392 | parent.removeChild(customDiv);
393 |
394 | const div = document.createElement('div');
395 | div.style = 'position: relative;';
396 | div.id = id;
397 |
398 | for (let i = 0; i < attributes.length; i++) {
399 |
400 | if (attributes[i].name !== 'style') {
401 |
402 | div.setAttribute(attributes[i].name, attributes[i].value);
403 |
404 | }
405 |
406 | }
407 |
408 | const numSpanStyle1 = 'display: inline-block; text-align: center; width: 7px;';
409 | const numSpanStyle2 = 'display: inline-block; text-align: center; width: 15px;';
410 |
411 | const inputNode = document.createElement('input');
412 | inputNode.className = 'text-input';
413 | inputNode.type = 'text';
414 | inputNode.style = 'width: ' + width + '; height: ' + height + '; color: white; caret-color: transparent;';
415 |
416 | div.appendChild(inputNode);
417 |
418 | const blockerNode = document.createElement('div');
419 | blockerNode.style = 'position: absolute; top: 0px; margin-left: 0px; margin-top: 0px; width: 100%; height: 100%;';
420 |
421 | const holderNode = document.createElement('div');
422 | holderNode.className = 'holder';
423 | holderNode.style = 'position: absolute; top: 0px; margin-left: 32px; margin-top: 3px;';
424 |
425 | const durationSpanNode0 = document.createElement('span');
426 | durationSpanNode0.className = 'duration-span0';
427 | durationSpanNode0.innerText = '0';
428 | durationSpanNode0.style = numSpanStyle1;
429 |
430 | const durationSpanNode1 = document.createElement('span');
431 | durationSpanNode1.className = 'duration-span1';
432 | durationSpanNode1.innerText = '00';
433 | durationSpanNode1.style = numSpanStyle2;
434 |
435 | holderNode.appendChild(durationSpanNode0);
436 | holderNode.appendChild(durationSpanNode1);
437 |
438 | blockerNode.appendChild(holderNode);
439 | div.appendChild(blockerNode);
440 |
441 | parent.appendChild(div);
442 |
443 | const data = {};
444 |
445 | alternateImplementation = alternateImplementation === undefined ? false : alternateImplementation;
446 |
447 | data.alternateImplementation = alternateImplementation;
448 | data.value = 0;
449 | data.width = 3;
450 | data.selectedIndex = -1;
451 | data.entry = alternateImplementation ? '0' : 0;
452 | data.enabled = true;
453 |
454 | inputData[div.id] = data;
455 |
456 | updateGrey(div);
457 |
458 | inputNode.addEventListener('keydown', handleKeyDown);
459 |
460 | function handleSpanClick () {
461 |
462 | if (!isEnabled(div)) {
463 |
464 | return;
465 |
466 | }
467 |
468 | setSelectedIndex(div, 0);
469 | highlightInput(div);
470 | inputNode.focus();
471 |
472 | updateGrey(div);
473 |
474 | }
475 |
476 | durationSpanNode0.addEventListener('click', handleSpanClick);
477 | durationSpanNode1.addEventListener('click', handleSpanClick);
478 |
479 | inputNode.addEventListener('focusin', () => {
480 |
481 | if (!isEnabled(div)) {
482 |
483 | inputNode.blur();
484 | return;
485 |
486 | }
487 |
488 | resetEntry(div);
489 |
490 | if (getSelectedIndex(div) === -1) {
491 |
492 | setSelectedIndex(div, 0);
493 | highlightInput(div);
494 |
495 | }
496 |
497 | updateGrey(div);
498 |
499 | });
500 |
501 | inputNode.addEventListener('focusout', () => {
502 |
503 | if (!isEnabled(div)) {
504 |
505 | return;
506 |
507 | }
508 |
509 | setSelectedIndex(div, -1);
510 | highlightInput(div);
511 |
512 | updateGrey(div);
513 |
514 | if (focusOutFunction) {
515 |
516 | focusOutFunction();
517 |
518 | }
519 |
520 | });
521 |
522 | inputNode.addEventListener('click', () => {
523 |
524 | if (!isEnabled(div)) {
525 |
526 | return;
527 |
528 | }
529 |
530 | setSelectedIndex(div, 0);
531 | highlightInput(div);
532 | inputNode.focus();
533 |
534 | });
535 |
536 | return div;
537 |
538 | };
539 |
--------------------------------------------------------------------------------
/settings/durationInput.js:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * durationInput.js
3 | * openacousticdevices.info
4 | * November 2019
5 | *****************************************************************************/
6 |
7 | 'use strict';
8 |
9 | /* global document, HTMLElement, customElements, Event */
10 |
11 | const ariaSpeak = require('../ariaSpeak.js');
12 |
13 | const DEFAULT_MAX_CHAR_LENGTH = 5;
14 | /* Currently, only single digit minimums are supported. Minimums of 10 or greater will require reimplementation */
15 | const DEFAULT_MIN_VALUE = 0;
16 | const DEFAULT_MAX_VALUE = 43200;
17 |
18 | const DEFAULT_WIDTH = '100px';
19 | const DEFAULT_HEIGHT = '25px';
20 |
21 | function getSpan (node) {
22 |
23 | return node.getElementsByClassName('duration-span')[0];
24 |
25 | }
26 |
27 | function getSpanFromChild (node) {
28 |
29 | return getSpan(node.parentNode);
30 |
31 | }
32 |
33 | function getTextInput (node) {
34 |
35 | return node.getElementsByClassName('duration-text-input')[0];
36 |
37 | }
38 |
39 | function getAttributeValue (node, attributeName) {
40 |
41 | return node.parentNode.getAttribute(attributeName);
42 |
43 | }
44 |
45 | function setAttributeValue (node, attributeName, value) {
46 |
47 | node.parentNode.setAttribute(attributeName, value);
48 |
49 | }
50 |
51 | exports.getValue = (div) => {
52 |
53 | return parseInt(div.getAttribute('inputValue'));
54 |
55 | };
56 |
57 | exports.setValue = (div, value) => {
58 |
59 | div.setAttribute('inputValue', value.toString());
60 | div.setAttribute('numbersEntered', value.toString().length - 1);
61 | getSpan(div).innerText = value.toString();
62 |
63 | };
64 |
65 | exports.setEnabled = (div, setting) => {
66 |
67 | div.setAttribute('enabled', setting ? 'true' : 'false');
68 |
69 | const span = getSpan(div);
70 | const textInput = getTextInput(div);
71 |
72 | if (setting) {
73 |
74 | textInput.style.backgroundColor = '';
75 | textInput.style.border = '';
76 | span.classList.remove('grey');
77 | span.style.backgroundColor = '';
78 |
79 | textInput.tabIndex = 0;
80 |
81 | } else {
82 |
83 | switch (process.platform) {
84 |
85 | case 'linux':
86 | case 'win32':
87 | textInput.style.backgroundColor = '#EBEBE4';
88 | textInput.style.border = '1px solid #cccccc';
89 | span.classList.add('grey');
90 | span.style.backgroundColor = '#EBEBE4';
91 | break;
92 |
93 | case 'darwin':
94 | span.classList.add('grey');
95 | span.style.backgroundColor = 'white';
96 | break;
97 |
98 | }
99 |
100 | textInput.tabIndex = -1;
101 |
102 | }
103 |
104 | };
105 |
106 | function highlightInput (node) {
107 |
108 | const span = getSpanFromChild(node);
109 |
110 | if (getAttributeValue(node, 'selected') === 'true') {
111 |
112 | span.style.backgroundColor = 'Highlight';
113 | span.style.color = 'HighlightText';
114 |
115 | } else {
116 |
117 | span.style.color = '';
118 | span.style.backgroundColor = '';
119 | span.classList.remove('grey');
120 |
121 | }
122 |
123 | }
124 |
125 | exports.updateHighlights = (div) => {
126 |
127 | highlightInput(div.getElementsByClassName('duration-holder')[0]);
128 |
129 | };
130 |
131 | function updateValue (inputKey, node) {
132 |
133 | let newValue, maxValue;
134 |
135 | const currentNumbersEntered = parseInt(getAttributeValue(node, 'numbersEntered'));
136 | const currentInputValue = getAttributeValue(node, 'inputValue');
137 |
138 | maxValue = getAttributeValue(node, 'maxValue');
139 | maxValue = (maxValue === null) ? DEFAULT_MAX_VALUE : maxValue;
140 |
141 | if (currentNumbersEntered === 1) {
142 |
143 | newValue = inputKey;
144 |
145 | setAttributeValue(node, 'inputValue', parseInt(newValue));
146 | getSpanFromChild(node).innerText = newValue;
147 |
148 | } else {
149 |
150 | if (currentInputValue === '0') {
151 |
152 | newValue = inputKey;
153 |
154 | setAttributeValue(node, 'inputValue', parseInt(newValue));
155 | getSpanFromChild(node).innerText = newValue;
156 |
157 | setAttributeValue(node, 'numbersEntered', 1);
158 |
159 | } else {
160 |
161 | newValue = currentInputValue + inputKey;
162 |
163 | if (parseInt(newValue) > maxValue) {
164 |
165 | setAttributeValue(node, 'inputValue', maxValue.toString());
166 | getSpanFromChild(node).innerText = maxValue.toString();
167 |
168 | } else {
169 |
170 | setAttributeValue(node, 'inputValue', parseInt(newValue));
171 | getSpanFromChild(node).innerText = newValue;
172 |
173 | }
174 |
175 | }
176 |
177 | }
178 |
179 | }
180 |
181 | function incrementValue (node) {
182 |
183 | let newValue, maxValue, minValue;
184 |
185 | maxValue = getAttributeValue(node, 'maxValue');
186 | maxValue = (maxValue === null) ? DEFAULT_MAX_VALUE : parseInt(maxValue);
187 |
188 | newValue = parseInt(getAttributeValue(node, 'inputValue')) + 1;
189 | newValue = (newValue > maxValue) ? maxValue : newValue;
190 |
191 | setAttributeValue(node, 'inputValue', newValue);
192 | getSpanFromChild(node).innerText = newValue;
193 |
194 | minValue = getAttributeValue(node, 'minValue');
195 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue);
196 |
197 | setAttributeValue(node, 'numbersEntered', minValue.toString().length - 1);
198 |
199 | }
200 |
201 | function decrementValue (node) {
202 |
203 | let newValue, minValue;
204 |
205 | minValue = getAttributeValue(node, 'minValue');
206 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue);
207 |
208 | newValue = parseInt(getAttributeValue(node, 'inputValue')) - 1;
209 | newValue = (parseInt(newValue) < minValue) ? minValue : newValue;
210 |
211 | setAttributeValue(node, 'inputValue', newValue);
212 | getSpanFromChild(node).innerText = newValue;
213 |
214 | setAttributeValue(node, 'numbersEntered', minValue.toString().length - 1);
215 |
216 | }
217 |
218 | function handleKeyDown (e) {
219 |
220 | if (e.key === 'Tab') {
221 |
222 | return;
223 |
224 | }
225 |
226 | e.preventDefault();
227 |
228 | if (e.key === 'ArrowUp') {
229 |
230 | incrementValue(e.target);
231 | e.target.parentNode.dispatchEvent(new Event('change'));
232 | return;
233 |
234 | }
235 |
236 | if (e.key === 'ArrowDown') {
237 |
238 | decrementValue(e.target);
239 | e.target.parentNode.dispatchEvent(new Event('change'));
240 | return;
241 |
242 | }
243 |
244 | if (e.key === 'Backspace' || e.key === 'Delete') {
245 |
246 | let minValue = getAttributeValue(e.target, 'minValue');
247 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue);
248 |
249 | setAttributeValue(e.target, 'inputValue', minValue.toString());
250 | getSpanFromChild(e.target).innerText = minValue.toString();
251 | setAttributeValue(e.target, 'numbersEntered', '0');
252 |
253 | e.target.parentNode.dispatchEvent(new Event('change'));
254 |
255 | return;
256 |
257 | }
258 |
259 | const patt = /^[0-9]$/;
260 |
261 | if (patt.test(e.key)) {
262 |
263 | const currentNumbersEntered = parseInt(getAttributeValue(e.target, 'numbersEntered'));
264 |
265 | let maxCharLength = getAttributeValue(e.target, 'maxcharlength');
266 | maxCharLength = (maxCharLength === null) ? DEFAULT_MAX_CHAR_LENGTH : maxCharLength;
267 |
268 | if (currentNumbersEntered <= maxCharLength) {
269 |
270 | setAttributeValue(e.target, 'numbersEntered', (currentNumbersEntered + 1).toString());
271 | updateValue(e.key, e.target);
272 |
273 | }
274 |
275 | e.target.parentNode.dispatchEvent(new Event('change'));
276 |
277 | }
278 |
279 | }
280 |
281 | function isEnabled (node) {
282 |
283 | return node.getAttribute('enabled') === 'true';
284 |
285 | }
286 |
287 | class DurationInput extends HTMLElement {
288 |
289 | connectedCallback () {
290 |
291 | let width, height, minValue;
292 |
293 | const parent = this.parentNode;
294 |
295 | const attributes = this.attributes;
296 |
297 | width = this.style.width;
298 | width = (width === '') ? DEFAULT_WIDTH : width;
299 | height = this.style.height;
300 | height = (height === '') ? DEFAULT_HEIGHT : height;
301 |
302 | parent.removeChild(this);
303 |
304 | const divNode = document.createElement('div');
305 | divNode.style = 'position: relative;';
306 |
307 | for (let i = 0; i < attributes.length; i++) {
308 |
309 | if (attributes[i].name !== 'style') {
310 |
311 | divNode.setAttribute(attributes[i].name, attributes[i].value);
312 |
313 | }
314 |
315 | }
316 |
317 | minValue = divNode.getAttribute('minValue');
318 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue);
319 |
320 | const inputNode = document.createElement('input');
321 | inputNode.className = 'duration-text-input';
322 | inputNode.type = 'text';
323 | inputNode.style = 'width: ' + width + '; height: ' + height + '; color: white; caret-color: transparent;';
324 |
325 | divNode.appendChild(inputNode);
326 |
327 | const blockerNode = document.createElement('div');
328 | blockerNode.style = 'position: absolute; top: 0px; margin-left: 0px; margin-top: 0px; width: 100%; height: 100%;';
329 |
330 | const holderNode = document.createElement('div');
331 | holderNode.className = 'duration-holder';
332 | holderNode.style = 'position: absolute; top: 0px; margin-left: 5%; margin-top: 3px; width: 90%;';
333 |
334 | const spanNode = document.createElement('span');
335 | spanNode.className = 'duration-span';
336 | spanNode.innerText = minValue.toString();
337 | spanNode.style = 'float: right;';
338 |
339 | holderNode.appendChild(spanNode);
340 |
341 | blockerNode.appendChild(holderNode);
342 |
343 | divNode.appendChild(blockerNode);
344 |
345 | parent.appendChild(divNode);
346 |
347 | divNode.setAttribute('inputValue', minValue.toString());
348 | divNode.setAttribute('numbersEntered', minValue.toString().length - 1);
349 | divNode.setAttribute('selected', 'false');
350 | divNode.setAttribute('enabled', 'true');
351 |
352 | inputNode.addEventListener('keydown', handleKeyDown);
353 |
354 | holderNode.addEventListener('click', () => {
355 |
356 | if (!isEnabled(divNode)) {
357 |
358 | return;
359 |
360 | }
361 |
362 | setAttributeValue(inputNode, 'selected', 'true');
363 | highlightInput(inputNode);
364 |
365 | inputNode.focus();
366 |
367 | const ariaLabel = divNode.getAttribute('aria-label');
368 | let ariaDescription = ariaLabel || '';
369 | ariaDescription += ' edit ' + spanNode.innerText;
370 |
371 | ariaSpeak.speak(ariaDescription);
372 |
373 | });
374 |
375 | inputNode.addEventListener('focusin', () => {
376 |
377 | minValue = divNode.getAttribute('minValue');
378 | minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue);
379 |
380 | if (!isEnabled(divNode)) {
381 |
382 | inputNode.blur();
383 | return;
384 |
385 | }
386 |
387 | divNode.setAttribute('numbersEntered', minValue.toString().length - 1);
388 | setAttributeValue(inputNode, 'selected', 'true');
389 | highlightInput(inputNode);
390 |
391 | const ariaLabel = divNode.getAttribute('aria-label');
392 | let ariaDescription = ariaLabel || '';
393 | ariaDescription += ' edit ' + spanNode.innerText;
394 |
395 | ariaSpeak.speak(ariaDescription);
396 |
397 | });
398 |
399 | inputNode.addEventListener('focusout', () => {
400 |
401 | let maxValue;
402 |
403 | maxValue = divNode.getAttribute('maxValue');
404 | maxValue = (maxValue === null) ? DEFAULT_MAX_VALUE : parseInt(maxValue);
405 |
406 | if (!isEnabled(divNode)) {
407 |
408 | return;
409 |
410 | }
411 |
412 | const currentInputValue = parseInt(getAttributeValue(inputNode, 'inputValue'));
413 |
414 | if (currentInputValue < minValue || currentInputValue > maxValue) {
415 |
416 | setAttributeValue(inputNode, 'inputValue', minValue.toString());
417 | setAttributeValue(inputNode, 'numbersEntered', minValue.toString().length - 1);
418 | getSpanFromChild(inputNode).innerText = minValue.toString();
419 |
420 | }
421 |
422 | setAttributeValue(inputNode, 'selected', 'false');
423 | highlightInput(inputNode);
424 |
425 | divNode.dispatchEvent(new Event('change'));
426 |
427 | });
428 |
429 | inputNode.addEventListener('click', () => {
430 |
431 | if (!isEnabled(divNode)) {
432 |
433 | return;
434 |
435 | }
436 |
437 | highlightInput(inputNode);
438 | inputNode.focus();
439 |
440 | const ariaLabel = divNode.getAttribute('aria-label');
441 | let ariaDescription = ariaLabel || '';
442 | ariaDescription += ' edit ' + spanNode.innerText;
443 |
444 | ariaSpeak.speak(ariaDescription);
445 |
446 | });
447 |
448 | }
449 |
450 | }
451 |
452 | customElements.define('duration-input', DurationInput);
453 |
--------------------------------------------------------------------------------