diff --git a/www/assets/app.js b/www/assets/app.js index c3fd8fa..5d86007 100644 --- a/www/assets/app.js +++ b/www/assets/app.js @@ -15,6 +15,19 @@ import './bootstrap'; import feather from 'feather-icons'; feather.replace(); +/** Update css variables --{header, footer}-height + * by querying elements real height */ +(function() { + let css_root = document.querySelector(':root'); + let header = document.getElementsByTagName('header')[0]; + let header_height = header.clientHeight; + css_root.style.setProperty('--header-height', header_height + 'px'); + let footer = document.getElementsByTagName('footer')[0]; + let footer_height = footer.clientHeight; + css_root.style.setProperty('--footer-height', footer_height + 'px'); + +})(); + try { document.getElementsByClassName('prevent').map( (e) => e.addEventListener('click', (e) => e.preventDefault()) diff --git a/www/assets/controllers/delete-record_controller.js b/www/assets/controllers/delete-record_controller.js new file mode 100644 index 0000000..3b09f02 --- /dev/null +++ b/www/assets/controllers/delete-record_controller.js @@ -0,0 +1,25 @@ +import { Controller } from '@hotwired/stimulus'; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static targets = ['filename'] + + delete() { + let filename = this.filenameTarget.value; + let url = `/records/delete/${filename}`; + fetch(url, { + method: 'POST' + }) + .then(response => { + if (response.ok) { + window.location.reload(); + } else { + console.log(response); + } + }) + .catch(error => { + console.log(error); + } + ); + } +} \ No newline at end of file diff --git a/www/assets/controllers/record-play_controller.js b/www/assets/controllers/record-play_controller.js new file mode 100644 index 0000000..3b09f02 --- /dev/null +++ b/www/assets/controllers/record-play_controller.js @@ -0,0 +1,25 @@ +import { Controller } from '@hotwired/stimulus'; + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static targets = ['filename'] + + delete() { + let filename = this.filenameTarget.value; + let url = `/records/delete/${filename}`; + fetch(url, { + method: 'POST' + }) + .then(response => { + if (response.ok) { + window.location.reload(); + } else { + console.log(response); + } + }) + .catch(error => { + console.log(error); + } + ); + } +} \ No newline at end of file diff --git a/www/assets/controllers/record.js b/www/assets/controllers/record.js deleted file mode 100644 index 55782be..0000000 --- a/www/assets/controllers/record.js +++ /dev/null @@ -1,7 +0,0 @@ -(function() { - try { - let delete_buttons = document.getElementsByClassName("delete-button"); - } catch { - console.debug("no delete buttons found"); - } -})(); \ No newline at end of file diff --git a/www/assets/styles/app.css b/www/assets/styles/app.css index a9aa650..216b77b 100644 --- a/www/assets/styles/app.css +++ b/www/assets/styles/app.css @@ -1,9 +1,24 @@ :root { - --bg: lightgray; + --bg: white; + --font-family: 'Latin Modern Math'; + --font-size: 2em; } * { box-sizing: border-box; + font-family: var(--font-family); + font-size: large; +} + +html { + position: relative; + min-height: 100vh; +} + +a { + text-decoration: none; + color: inherit; + cursor: pointer; } .container { @@ -20,6 +35,18 @@ flex-direction: row; } +.grow { + flex-grow: 100; +} + +.end { + justify-self: flex-end; +} + +.item { + align-self: flex-end; +} + .grid { display: grid; } @@ -38,7 +65,7 @@ z-index: 1; } -.button, input { +.button, input[type=submit], input[type=button] { background-color: #f1f1f1; color: black; border-radius: 5px; @@ -129,23 +156,31 @@ body { header { padding: 1em; - display: flex; - flex-direction: row; /** Align text and center of image */ justify-content: center; align-items: baseline; } +header h1 { + font-size: 4rem; +} + +header:first-child { + justify-content: center; +} + header img.logo { - width: 100px; - height: 100px; + width: auto; + height: 10rem; + position: relative; + top: -2rem; + padding-top: 1em; } main { - min-height: 100vh; + min-height: calc(100vh - (var(--header-height, 4em) + var(--footer-height, 4em)) ); padding: 5em; z-index: 0; - position: relative; } footer { @@ -153,6 +188,7 @@ footer { background-color: black; padding: 2em; text-align: center; + overflow: hidden; } footer a { @@ -168,8 +204,6 @@ li, td { align-items: center; } -td - /* .dropdown-button:hover { background-color: #900; color: white diff --git a/www/assets/styles/menu.css b/www/assets/styles/menu.css index d934207..d433632 100644 --- a/www/assets/styles/menu.css +++ b/www/assets/styles/menu.css @@ -1,7 +1,8 @@ nav { --nav-width: 20em; - --nav-bg: white; - --burger-size: 2em; + --nav-bg: lightgrey; + --burger-size: 5em; + --burger-weight: 3px; position: fixed; top: 0; left: 0; @@ -40,7 +41,7 @@ nav { top: 0; left: 0; background: black; - height: 2px; + height: var(--burger-weight); width: 75%; transition: all 0.4s ease; color: #000; @@ -51,19 +52,19 @@ nav { .hamburger>div::after { content: ''; position: absolute; - top: -7px; + top: 10px; background: black; width: 100%; - height: 2px; + height: var(--burger-weight); transition: all 0.4s ease; } .hamburger>div::after { - top: 7px; + top: -10px; } .toggler:checked+.hamburger>div { - background: rgba(0, 0, 0, 0); + background-color: var(--nav-bg); } .toggler:checked+.hamburger>div::before { @@ -87,7 +88,7 @@ nav { .toggler:checked~.menu { width: fit-content; height: fit-content; - background-color: rgba(255, 255, 255, 1) !important; + background-color: var(--nav-bg) !important; } .menu>ul { @@ -95,12 +96,12 @@ nav { flex-direction: column; position: fixed; width: 0; - height: 100vmax; + height: calc(100vh - var(--burger-size)); padding-left: 1em; padding-right: 1em; margin-top: var(--burger-size); visibility: hidden; - background-color: white; + background-color: var(--nav-bg); z-index: 10; } @@ -112,7 +113,7 @@ nav { .menu>ul>li>a { color: black; text-decoration: none; - font-size: 2rem; + font-size: var(--font-size); } .toggler~.fill { @@ -120,11 +121,11 @@ nav { } .toggler:checked~.fill { - background: white; + background: var(--nav-bg); position: absolute; top: 0; left: 0; - height: 100vh; + height: var(--burger-size); width: var(--nav-width); display: block; z-index: 10; diff --git a/www/assets/utils/spectro.js b/www/assets/utils/spectro.js index 6785240..178a71a 100644 --- a/www/assets/utils/spectro.js +++ b/www/assets/utils/spectro.js @@ -3,8 +3,8 @@ * https://codepen.io/jakealbaugh/pen/jvQweW */ -// UPDATE: there is a problem in chrome with starting audio context -// before a user gesture. This fixes it. +const ICECAST_URL = '/stream'; + var started = false; try { var spectro_button = document.getElementById('spectro-button'); @@ -13,12 +13,13 @@ try { started = true; console.log("starting spectro"); initialize(); - }) + }) } catch { console.debug("spectro not found"); } function initialize() { + const AUDIO_ELEMENT = document.getElementById('player'); const CVS = document.getElementById('spectro-canvas'); const CTX = CVS.getContext('2d'); const W = CVS.width = window.innerWidth; @@ -27,14 +28,29 @@ function initialize() { const ACTX = new AudioContext(); const ANALYSER = ACTX.createAnalyser(); - ANALYSER.fftSize = 4096; - - navigator.mediaDevices - .getUserMedia({ audio: true }) - .then(process); + ANALYSER.fftSize = 4096; - function process(stream) { - const SOURCE = ACTX.createMediaStreamSource(stream); + // navigator.mediaDevices + // .getUserMedia({ audio: true }) + // .then(process); + + // Add icecast stream + // var audio = new Audio(ICECAST_URL); + let stream; + AUDIO_ELEMENT.src = ICECAST_URL; + AUDIO_ELEMENT.play(); + AUDIO_ELEMENT.onplay = function () { + if (navigator.userAgent.indexOf('Firefox') > -1) { + stream = AUDIO_ELEMENT.mozCaptureStream(); + } else { + console.debug('Not a firefox browser, defaults to `captureStream()`'); + stream = AUDIO_ELEMENT.captureStream(); + } + process(AUDIO_ELEMENT); + } + + function process(audio) { + const SOURCE = ACTX.createMediaElementSource(audio); SOURCE.connect(ANALYSER); const DATA = new Uint8Array(ANALYSER.frequencyBinCount); const LEN = DATA.length; diff --git a/www/src/Controller/HomeController.php b/www/src/Controller/HomeController.php index 150ad72..38f77f4 100644 --- a/www/src/Controller/HomeController.php +++ b/www/src/Controller/HomeController.php @@ -62,7 +62,7 @@ class HomeController extends AbstractController ORDER BY `date` DESC LIMIT 1"; $stmt = $this->connection->prepare($sql); $result = $stmt->executeQuery(); - return $result->fetchAllAssociative()[0]; + return $result->fetchAllAssociative(); } private function last_chart_generated() { diff --git a/www/src/Controller/RecordsController.php b/www/src/Controller/RecordsController.php index f7021a8..a827eaa 100644 --- a/www/src/Controller/RecordsController.php +++ b/www/src/Controller/RecordsController.php @@ -1,4 +1,5 @@ $date, ]); } - + /** - * @Route("/records/remove/{basename}", name="record_remove") + * @Route("/records/delete/{basename}", name="record_delete") */ - public function remove_record($basename) + public function delete_record(Connection $connection, $basename) { + $this->connection = $connection; $this->remove_record_by_basename($basename); return $this->redirectToRoute('records_index'); } - private function list_records() + private function list_records() { - $records_path = $this->getParameter('app.records_dir')."/out/*.wav"; + $records_path = $this->getParameter('app.records_dir') . "/out/*.wav"; $records = glob($records_path); - $records = array_map(function($record) { + $records = array_map(function ($record) { $record = basename($record); return $record; }, $records); return $records; } - private function get_record_date($record_path) + private function get_record_date($record_path) { $record_basename = basename($record_path); $record_date = explode("_", explode(".", $record_basename)[0])[1]; @@ -58,17 +60,37 @@ class RecordsController extends AbstractController return $date; } - private function only_on($date, $records) { - $filtered_records = array_filter($records, function($record) use ($date) { + private function only_on($date, $records) + { + $filtered_records = array_filter($records, function ($record) use ($date) { return $this->get_record_date($record) == $date; }); return $filtered_records; } - private function remove_record_by_basename($basename) { - $record_path = $this->getParameter('app.records_dir')."/out/$basename"; - unlink($record_path); - unlink($record_path.".d/model.out.csv"); - rmdir($this->getParameter('app.records_dir')."/out/$basename.d"); + private function remove_record_by_basename($basename) + { + if (strlen($basename) > 1) { + /** Remove files associated with this filename */ + $record_path = $this->getParameter('app.records_dir') . "/out/$basename"; + if (is_file($record_path)) + unlink($record_path); + $model_out_dir = $record_path.".d"; + $model_out_path = $model_out_dir."/model.out.csv"; + if (is_file($model_out_path)) + unlink($model_out_path); + if (is_dir($model_out_dir)) + rmdir($model_out_dir); + /** Remove database entry associated with this filename */ + $this->remove_observations_from_record($basename); + } } -} \ No newline at end of file + + private function remove_observations_from_record($basename) + { + $sql = "DELETE FROM observation WHERE audio_file = :filename"; + $stmt = $this->connection->prepare($sql); + $stmt->bindValue(':filename', $basename); + $stmt->executeStatement(); + } +} diff --git a/www/templates/base.html.twig b/www/templates/base.html.twig index c292e8b..ee10174 100644 --- a/www/templates/base.html.twig +++ b/www/templates/base.html.twig @@ -1,5 +1,5 @@ - + @@ -16,12 +16,16 @@ {{ encore_entry_script_tags('app') }} {% endblock %} </head> - <body> + <body class="container col"> {% block body %} {% include "menu.html.twig" %} <header> - <img class="logo" src="/media/logo.svg" alt="BirdNET logo"> - <h1>BirdNET-stream</h1> + <a href="/"> + <div class="container row"> + <img class="logo" src="/media/logo.svg" alt="BirdNET logo"> + <h1>BirdNET-stream</h1> + </div> + </a> </header> <main> {% block content %} diff --git a/www/templates/footer.html.twig b/www/templates/footer.html.twig index 44e988d..d49122c 100644 --- a/www/templates/footer.html.twig +++ b/www/templates/footer.html.twig @@ -1,4 +1,4 @@ -<footer> +<footer class="end"> <p> © <span class="creation-date">2022 <!-- check if the current year is the same as the creation year --> diff --git a/www/templates/records/delete_button.html.twig b/www/templates/records/delete_button.html.twig new file mode 100644 index 0000000..88cd3ab --- /dev/null +++ b/www/templates/records/delete_button.html.twig @@ -0,0 +1,5 @@ +<div {{ stimulus_controller('delete-record') }}> + <button class="delete button" value="{{ filename }}" {{ stimulus_target('delete-record', 'filename') }} {{ stimulus_action('delete-record', 'delete') }}> + <i data-feather="trash-2"></i> + </button> +</div> diff --git a/www/templates/records/index.html.twig b/www/templates/records/index.html.twig index 1c6aacb..a645d06 100644 --- a/www/templates/records/index.html.twig +++ b/www/templates/records/index.html.twig @@ -7,7 +7,10 @@ {% if records is defined and records | length > 0 %} <ul> {% for record in records %} - <li>{{ record }}</li> + <li>{{ record }} + {% include "records/delete_button.html.twig" with { filename: record } only %} + {% include "records/player.html.twig" with { filename: record } only %} + </li> {% endfor %} </ul> {% endif %} diff --git a/www/templates/records/player.html.twig b/www/templates/records/player.html.twig new file mode 100644 index 0000000..5468975 --- /dev/null +++ b/www/templates/records/player.html.twig @@ -0,0 +1,3 @@ +<audio preload="none" controls> + <source src="/media/records/{{ filename }}"> +</audio> \ No newline at end of file diff --git a/www/templates/spectro/index.html.twig b/www/templates/spectro/index.html.twig index 293885c..f7cd399 100644 --- a/www/templates/spectro/index.html.twig +++ b/www/templates/spectro/index.html.twig @@ -4,6 +4,12 @@ <h2>{{ "Spectrogram" | trans }}</h2> <div> <button id="spectro-button">{{ "Launch Live Spectrogram" | trans }}</button> + <audio id="player" controls> + <source media="/stream"> + {{ "No audio sources yet" | trans }} + </audio> <canvas id="spectro-canvas"></canvas> </div> + + {{ encore_entry_script_tags('spectro') }} {% endblock %} \ No newline at end of file diff --git a/www/templates/stats.html.twig b/www/templates/stats.html.twig index 534ba26..db9a265 100644 --- a/www/templates/stats.html.twig +++ b/www/templates/stats.html.twig @@ -3,6 +3,7 @@ <ul> <li class="most-recorded-species"> {{ "Most recorded species" | trans }}: + {% if stats["most-recorded-species"] is defined %} <span class="scientific-name"> {{ stats["most-recorded-species"]["scientific_name"] }} </span> @@ -12,9 +13,13 @@ {{ stats["most-recorded-species"]["contact_count"] }} </span> {{ "contacts" | trans }}. + {% else %} + {{ "No species in database." | trans }} + {% endif %} </li> <li class="last-recorded-species"> {{ "Last detected species" | trans }}: + {% if stats["last-recorded-species"] is defined %} <span class="scientific-name"> {{ stats["last-detected-species"]["scientific_name"] }} </span> @@ -37,6 +42,9 @@ {{ date | date("H:i") }} </span> </span>. + {% else %} + {{ "No species in database" | trans }} + {% endif %} </li> </ul> </div> \ No newline at end of file diff --git a/www/templates/today/manage.html.twig b/www/templates/today/manage.html.twig new file mode 100644 index 0000000..b1dd132 --- /dev/null +++ b/www/templates/today/manage.html.twig @@ -0,0 +1,8 @@ +<div class="manager"> + <button class="button" id="select-all" title="{{ "Select all" | trans }}"> + <i data-feather="check-square"></i> + </button> + <button class="delete button" title="{{ "Delete selected" | trans }}"> + <i data-feather="trash-2"></i> + </button> +</div> diff --git a/www/templates/today/species.html.twig b/www/templates/today/species.html.twig index 6a3f6a4..63998ad 100644 --- a/www/templates/today/species.html.twig +++ b/www/templates/today/species.html.twig @@ -35,9 +35,11 @@ <div class="records"> <h3>{{ "Contact records" | trans }}</h3> {% if records is defined and records | length > 0 %} + {% include "today/manage.html.twig" %} <table> <thead> <tr> + <th></th> <th>{{ "Filename" | trans }}</th> <th>{{ "Time" | trans }}</th> <th>{{ "Confidence" | trans }}</th> @@ -46,6 +48,9 @@ <tbody> {% for record in records %} <tr> + <td> + <input title="{{ "Select this record" | trans }}" type="checkbox" name="select-file" value="{{ record.audio_file }}"> + </td> <td> <a title="{{ "Download audio file" | trans }}" href="/media/records/{{ record.audio_file }}"> {{ record.audio_file }} @@ -54,9 +59,8 @@ <td>{{ record.date | date("H:m") }}</td> <td>{{ record.confidence }}</td> <td> - {% include "utils/player.html.twig" with { "file": record.audio_file } only %} - <button class="delete" value="{{ record.audio_file }}"><i data-feather="trash-2"></i></button> - </td> + {% include "records/player.html.twig" with { "filename": record.audio_file } only %} + {% include "records/delete_button.html.twig" with { "filename": record.audio_file } only %} </td> </tr> {% endfor %} </tbody> diff --git a/www/templates/utils/calendar.html.twig b/www/templates/utils/calendar.html.twig index c2ebded..6ba5b14 100644 --- a/www/templates/utils/calendar.html.twig +++ b/www/templates/utils/calendar.html.twig @@ -1,7 +1,7 @@ <div class="date-selector container row"> <label for="date">{{ "Date" | trans }}</label> <button class="previous-date-button button"><</button> - <input type="date" id="date" name="date" value="{{ date ? date : "now" | date("Y-m-d") }}" placeholder="YYYY-MM-DD" required pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}" title="{{ "Enter a date in this format YYYY-MM-DD" | trans }}"/> + <input type="date" id="date" name="date" value="{{ date ? date : "now" | date("Y-m-d") }}" required title="{{ "Enter a date in this format YYYY-MM-DD" | trans }}"/> <button class="next-date-button button prevent">></button> <a href="/{{ endpoint }}/{{ date ? date : " now" | date("Y-m-d") }}/" class="button main">{{ "Go" | trans }}</a> </div> diff --git a/www/templates/utils/player.html.twig b/www/templates/utils/player.html.twig deleted file mode 100644 index b0ff192..0000000 --- a/www/templates/utils/player.html.twig +++ /dev/null @@ -1,3 +0,0 @@ -<audio preload="none" controls> - <source src="/media/records/{{ file }}"> -</audio> \ No newline at end of file