mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-10-29 15:55:01 -04:00
improve mobile-friendliness (#68)
nav div changes: * make it togglable (on all devices) by hamburger button * on narrow devices, make it closed by default and be at the top rather than on the left open zoomed by default trim some arguably less important columns on narrow displays, and reduce some horizontal padding always show videos full-screen on narrow displays
This commit is contained in:
parent
be479a1ffe
commit
e177cbd042
@ -143,9 +143,14 @@ function onSelectVideo(nvrSettingsView, camera, streamType, range, recording) {
|
|||||||
);
|
);
|
||||||
const videoTitle =
|
const videoTitle =
|
||||||
camera.shortName + ', ' + formattedStart + ' to ' + formattedEnd;
|
camera.shortName + ', ' + formattedStart + ' to ' + formattedEnd;
|
||||||
|
const maxWidth = window.innerWidth / 2;
|
||||||
|
let width = recording.videoSampleEntryWidth;
|
||||||
|
while (width > maxWidth) {
|
||||||
|
width /= 2;
|
||||||
|
}
|
||||||
new VideoDialogView()
|
new VideoDialogView()
|
||||||
.attach($('body'))
|
.attach($('body'))
|
||||||
.play(videoTitle, recording.videoSampleEntryWidth / 4, url);
|
.play(videoTitle, width, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -220,7 +225,7 @@ function updateSession(session) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sessionBar.append($('<span id="session-username" />').text(session.username));
|
sessionBar.append($('<span id="session-username" />').text(session.username));
|
||||||
const logout = $('<a>logout</a>');
|
const logout = $('<a id="logout">logout</a>');
|
||||||
logout.click(() => {
|
logout.click(() => {
|
||||||
api
|
api
|
||||||
.logout(session.csrf)
|
.logout(session.csrf)
|
||||||
@ -377,6 +382,11 @@ export default class NVRApplication {
|
|||||||
* Start the application.
|
* Start the application.
|
||||||
*/
|
*/
|
||||||
start() {
|
start() {
|
||||||
|
let nav = $('#nav');
|
||||||
|
|
||||||
|
$('#toggle-nav').click(() => {
|
||||||
|
nav.toggle('slide');
|
||||||
|
});
|
||||||
loginDialog = $('#login').dialog({
|
loginDialog = $('#login').dialog({
|
||||||
autoOpen: false,
|
autoOpen: false,
|
||||||
modal: true,
|
modal: true,
|
||||||
|
|||||||
@ -1,12 +1,24 @@
|
|||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
#top {
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 2ex;
|
||||||
|
}
|
||||||
|
#toggle-nav {
|
||||||
|
font-size: 1.25em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
#nav {
|
#nav {
|
||||||
float: left;
|
float: left;
|
||||||
|
margin: 0 0.5em 0.5ex 0;
|
||||||
}
|
}
|
||||||
#session {
|
#session {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
#logout {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
#datetime .ui-datepicker {
|
#datetime .ui-datepicker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -14,17 +26,15 @@ body {
|
|||||||
|
|
||||||
#videos {
|
#videos {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-top: 0.5em;
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
}
|
||||||
#videos tbody:after {
|
#videos tbody:after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
height: 3ex;
|
height: 3ex;
|
||||||
}
|
}
|
||||||
table.videos {
|
table#videos {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
/*border-spacing: 0.5em 0.5ex;*/
|
||||||
}
|
}
|
||||||
tbody tr.name {
|
tbody tr.name {
|
||||||
font-size: 110%;
|
font-size: 110%;
|
||||||
@ -47,8 +57,8 @@ tr.r td {
|
|||||||
tr.r th,
|
tr.r th,
|
||||||
tr.r td {
|
tr.r td {
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0.5ex 1.5em;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
padding: 0.5ex 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
@ -62,9 +72,6 @@ fieldset legend {
|
|||||||
#from, #to {
|
#from, #to {
|
||||||
padding-right: 0.75em;
|
padding-right: 0.75em;
|
||||||
}
|
}
|
||||||
#st {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#range {
|
#range {
|
||||||
padding: 0.5em 0;
|
padding: 0.5em 0;
|
||||||
@ -78,3 +85,17 @@ video {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
#nav {
|
||||||
|
float: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.resolution, .frameRate, .size {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
tr.r th,
|
||||||
|
tr.r td {
|
||||||
|
padding: 0.5ex 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -3,9 +3,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Moonfire NVR</title>
|
<title>Moonfire NVR</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="session">
|
<div id="top">
|
||||||
|
<a id="toggle-nav">☰</a>
|
||||||
|
<span id="session"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
<form action="#">
|
<form action="#">
|
||||||
@ -16,14 +19,15 @@
|
|||||||
<fieldset id="datetime">
|
<fieldset id="datetime">
|
||||||
<legend>Date & Time Range</legend>
|
<legend>Date & Time Range</legend>
|
||||||
<div id="from">
|
<div id="from">
|
||||||
<div id="start-date"></div>
|
<div id="start-date"></div>
|
||||||
<div id="st">
|
<label for="start-time">Time:</label>
|
||||||
<label for="start-time">Time:</label>
|
<input id="start-time" name="start-time" type="text"
|
||||||
<input id="start-time" name="start-time" type="text" title="Starting
|
max-length="20"
|
||||||
time within the day. Blank for the beginning of the day. Otherwise
|
title="Starting time within the day. Blank for the beginning of
|
||||||
HH:mm[:ss[:FFFFF]][+-HH:mm], where F is 90,000ths of a second.
|
the day. Otherwise HH:mm[:ss[:FFFFF]][+-HH:mm], where F is
|
||||||
Timezone is normally left out; it's useful once a year during the
|
90,000ths of a second. Timezone is normally left out; it's useful
|
||||||
ambiguous times of the "fall back" hour."></div>
|
once a year during the ambiguous times of the "fall
|
||||||
|
back" hour.">
|
||||||
</div>
|
</div>
|
||||||
<div id="range">Range:
|
<div id="range">Range:
|
||||||
<input type="radio" name="end-date-type" id="end-date-same" checked>
|
<input type="radio" name="end-date-type" id="end-date-same" checked>
|
||||||
@ -33,12 +37,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="to">
|
<div id="to">
|
||||||
<div id="end-date"></div>
|
<div id="end-date"></div>
|
||||||
<label for="start-time">Time:</label>
|
<label for="end-time">Time:</label>
|
||||||
<input id="end-time" name="end-time" type="text" title="Ending
|
<input id="end-time" name="end-time" type="text" max-length="20"
|
||||||
time within the day. Blank for the end of the day. Otherwise
|
title="Ending time within the day. Blank for the end of the day.
|
||||||
HH:mm[:ss[:FFFFF]][+-HH:mm], where F is 90,000ths of a second.
|
Otherwise HH:mm[:ss[:FFFFF]][+-HH:mm], where F is 90,000ths of a
|
||||||
Timezone is normally left out; it's useful once a year during the
|
second. Timezone is normally left out; it's useful once a year
|
||||||
ambiguous times of the "fall back" hour.">
|
during the ambiguous times of the "fall back" hour.">
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@ -112,7 +112,7 @@ export default class RecordingsView {
|
|||||||
$('<tr class="hdr">').append(
|
$('<tr class="hdr">').append(
|
||||||
$(
|
$(
|
||||||
_columnOrder
|
_columnOrder
|
||||||
.map((name) => '<th>' + _columnLabels[name] + '</th>')
|
.map((name) => `<th class="${name}">${_columnLabels[name]}</th>`)
|
||||||
.join('')
|
.join('')
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -271,7 +271,7 @@ export default class RecordingsView {
|
|||||||
$('tr.r', tbody).remove();
|
$('tr.r', tbody).remove();
|
||||||
this.recordings_.forEach((r) => {
|
this.recordings_.forEach((r) => {
|
||||||
const row = $('<tr class="r" />');
|
const row = $('<tr class="r" />');
|
||||||
row.append(_columnOrder.map(() => $('<td/>')));
|
row.append(_columnOrder.map((c) => $(`<td class="${c}"/>`)));
|
||||||
row.on('click', () => {
|
row.on('click', () => {
|
||||||
console.log('Video clicked');
|
console.log('Video clicked');
|
||||||
if (this.clickHandler_ !== null) {
|
if (this.clickHandler_ !== null) {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export default class VideoDialogView {
|
|||||||
* This does not attach the player to the DOM anywhere! In fact, construction
|
* This does not attach the player to the DOM anywhere! In fact, construction
|
||||||
* of the necessary video element is delayed until an attach is requested.
|
* of the necessary video element is delayed until an attach is requested.
|
||||||
* Since the close of the video removes all traces of it in the DOM, this
|
* Since the close of the video removes all traces of it in the DOM, this
|
||||||
* apprach allows repeated use by calling attach again!
|
* approach allows repeated use by calling attach again!
|
||||||
*/
|
*/
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
@ -80,13 +80,13 @@ export default class VideoDialogView {
|
|||||||
* @return {VideoDialogView} Returns "this" for chaining.
|
* @return {VideoDialogView} Returns "this" for chaining.
|
||||||
*/
|
*/
|
||||||
play(title, width, url) {
|
play(title, width, url) {
|
||||||
|
const videoDomElement = this.videoElement_[0];
|
||||||
this.dialogElement_.dialog({
|
this.dialogElement_.dialog({
|
||||||
title: title,
|
title: title,
|
||||||
width: width,
|
width: width,
|
||||||
close: () => {
|
close: () => {
|
||||||
const videoDOMElement = this.videoElement_[0];
|
videoDomElement.pause();
|
||||||
videoDOMElement.pause();
|
videoDomElement.src = ''; // Remove current source to stop loading
|
||||||
videoDOMElement.src = ''; // Remove current source to stop loading
|
|
||||||
this.videoElement_ = null;
|
this.videoElement_ = null;
|
||||||
this.dialogElement_.remove();
|
this.dialogElement_.remove();
|
||||||
this.dialogElement_ = null;
|
this.dialogElement_ = null;
|
||||||
@ -95,6 +95,21 @@ export default class VideoDialogView {
|
|||||||
// Now that dialog is up, set the src so video starts
|
// Now that dialog is up, set the src so video starts
|
||||||
console.log('Video url: ' + url);
|
console.log('Video url: ' + url);
|
||||||
this.videoElement_.attr('src', url);
|
this.videoElement_.attr('src', url);
|
||||||
|
|
||||||
|
// On narrow displays (as defined by index.css), play videos in
|
||||||
|
// full-screen mode. When the user exits full-screen mode, close the
|
||||||
|
// dialog.
|
||||||
|
const narrowWindow = $('#nav').css('float') == 'none';
|
||||||
|
if (narrowWindow) {
|
||||||
|
console.log('Narrow window; starting video in full-screen mode.');
|
||||||
|
videoDomElement.requestFullscreen();
|
||||||
|
videoDomElement.addEventListener('fullscreenchange', (event) => {
|
||||||
|
if (document.fullscreenElement !== videoDomElement) {
|
||||||
|
console.log('Closing video because user exited full-screen mode.');
|
||||||
|
this.dialogElement_.dialog("close");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user