style: Adapt responsive design for mobile devices

This commit is contained in:
Samuel Ortion 2022-08-18 05:45:38 +02:00
parent 32a2b92f14
commit e74139fe72
20 changed files with 237 additions and 70 deletions

View File

@ -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())

View 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);
}
);
}
}

View 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);
}
);
}
}

View File

@ -1,7 +0,0 @@
(function() {
try {
let delete_buttons = document.getElementsByClassName("delete-button");
} catch {
console.debug("no delete buttons found");
}
})();

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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() {

View File

@ -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");
@ -26,28 +27,29 @@ class RecordsController extends AbstractController
'date' => $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);
}
}
}
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();
}
}

View File

@ -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>
<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 %}

View File

@ -1,4 +1,4 @@
<footer>
<footer class="end">
<p>
&copy; <span class="creation-date">2022
<!-- check if the current year is the same as the creation year -->

View 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>

View File

@ -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 %}

View File

@ -0,0 +1,3 @@
<audio preload="none" controls>
<source src="/media/records/{{ filename }}">
</audio>

View File

@ -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 %}

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -1,7 +1,7 @@
<div class="date-selector container row">
<label for="date">{{ "Date" | trans }}</label>
<button class="previous-date-button button">&lt;</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">&gt;</button>
<a href="/{{ endpoint }}/{{ date ? date : " now" | date("Y-m-d") }}/" class="button main">{{ "Go" | trans }}</a>
</div>

View File

@ -1,3 +0,0 @@
<audio preload="none" controls>
<source src="/media/records/{{ file }}">
</audio>