style: Adapt responsive design for mobile devices
This commit is contained in:
parent
32a2b92f14
commit
e74139fe72
@ -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())
|
||||
|
25
www/assets/controllers/delete-record_controller.js
Normal file
25
www/assets/controllers/delete-record_controller.js
Normal file
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
25
www/assets/controllers/record-play_controller.js
Normal file
25
www/assets/controllers/record-play_controller.js
Normal file
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
(function() {
|
||||
try {
|
||||
let delete_buttons = document.getElementsByClassName("delete-button");
|
||||
} catch {
|
||||
console.debug("no delete buttons found");
|
||||
}
|
||||
})();
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
@ -19,6 +19,7 @@ try {
|
||||
}
|
||||
|
||||
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;
|
||||
@ -29,12 +30,27 @@ function initialize() {
|
||||
|
||||
ANALYSER.fftSize = 4096;
|
||||
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
.then(process);
|
||||
// navigator.mediaDevices
|
||||
// .getUserMedia({ audio: true })
|
||||
// .then(process);
|
||||
|
||||
function process(stream) {
|
||||
const SOURCE = ACTX.createMediaStreamSource(stream);
|
||||
// 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;
|
||||
|
@ -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() {
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@ -14,7 +15,7 @@ class RecordsController extends AbstractController
|
||||
/**
|
||||
* @Route("/records/{date}", name="records_index")
|
||||
*/
|
||||
public function records_index($date="now")
|
||||
public function records_index($date = "now")
|
||||
{
|
||||
if ($date == "now") {
|
||||
$date = date("Y-m-d");
|
||||
@ -28,19 +29,20 @@ class RecordsController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @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()
|
||||
{
|
||||
$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);
|
||||
@ -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";
|
||||
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);
|
||||
unlink($record_path.".d/model.out.csv");
|
||||
rmdir($this->getParameter('app.records_dir')."/out/$basename.d");
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>
|
||||
@ -16,12 +16,16 @@
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<body class="container col">
|
||||
{% block body %}
|
||||
{% include "menu.html.twig" %}
|
||||
<header>
|
||||
<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 %}
|
||||
|
@ -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 -->
|
||||
|
5
www/templates/records/delete_button.html.twig
Normal file
5
www/templates/records/delete_button.html.twig
Normal file
@ -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>
|
@ -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 %}
|
||||
|
3
www/templates/records/player.html.twig
Normal file
3
www/templates/records/player.html.twig
Normal file
@ -0,0 +1,3 @@
|
||||
<audio preload="none" controls>
|
||||
<source src="/media/records/{{ filename }}">
|
||||
</audio>
|
@ -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 %}
|
@ -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>
|
8
www/templates/today/manage.html.twig
Normal file
8
www/templates/today/manage.html.twig
Normal file
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -1,3 +0,0 @@
|
||||
<audio preload="none" controls>
|
||||
<source src="/media/records/{{ file }}">
|
||||
</audio>
|
Loading…
Reference in New Issue
Block a user