+-> fix remote picture previewer
+-> add a download option in media list
+-> add a previewer for movies
-> make camera frames positions configurable
-> add a view log functionality
--> add a previewer for snapshots
--> add a previewer for movies
-> style scroll bars
-> hint text next to section titles
import base64
import json
import logging
+import os
from tornado.web import RequestHandler, HTTPError, asynchronous
elif op == 'download':
self.download(camera_id, filename)
+ elif op == 'preview':
+ self.preview(camera_id, filename)
+
else:
raise HTTPError(400, 'unknown operation')
camera_config.get('@host'),
camera_config.get('@port'),
camera_config.get('@remote_camera_id'))
-
+
camera_full_url = camera_config['@proto'] + '://' + camera_url
-
+
if remote_list is None:
return self.finish_json({'error': 'Failed to get picture list for %(url)s.' % {
'url': camera_full_url}})
else:
pictures = mediafiles.list_pictures(camera_config)
- self.finish_json({'pictures': pictures})
+ self.finish_json({
+ 'mediaList': pictures,
+ 'cameraName': camera_config['@name']
+ })
@BaseHandler.auth()
def download(self, camera_id, filename):
logging.debug('downloading picture %(filename)s of camera %(id)s' % {
'filename': filename, 'id': camera_id})
- # TODO implement me
+ if camera_id not in config.get_camera_ids():
+ raise HTTPError(404, 'no such camera')
- self.finish_json()
+ camera_config = config.get_camera(camera_id)
+ if camera_config['@proto'] != 'v4l2':
+ def on_response(remote_list):
+ camera_url = remote.make_remote_camera_url(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@remote_camera_id'))
+
+ camera_full_url = camera_config['@proto'] + '://' + camera_url
+
+ if remote_list is None:
+ return self.finish_json({'error': 'Failed to get picture list for %(url)s.' % {
+ 'url': camera_full_url}})
+
+ self.finish_json(remote_list)
+
+ remote.download_picture(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@username'),
+ camera_config.get('@password'),
+ camera_config.get('@remote_camera_id'), on_response)
+
+ else:
+ content = mediafiles.get_media_content(camera_config, filename)
+
+ pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename)
+ self.set_header('Content-Type', 'image/jpeg')
+ self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
+
+ self.finish(content)
+
+
+ @BaseHandler.auth()
+ def preview(self, camera_id, filename):
+ logging.debug('previewing picture %(filename)s of camera %(id)s' % {
+ 'filename': filename, 'id': camera_id})
+
+ if camera_id not in config.get_camera_ids():
+ raise HTTPError(404, 'no such camera')
+
+ camera_config = config.get_camera(camera_id)
+ if camera_config['@proto'] != 'v4l2':
+ def on_response(response):
+ camera_url = remote.make_remote_camera_url(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@remote_camera_id'))
+
+ camera_full_url = camera_config['@proto'] + '://' + camera_url
+
+ if response is None:
+ return self.finish_json({'error': 'Failed to get picture list for %(url)s.' % {
+ 'url': camera_full_url}})
+
+ self.set_header('Content-Type', 'image/jpeg')
+ self.finish(response)
+
+ remote.preview_picture(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@username'),
+ camera_config.get('@password'),
+ camera_config.get('@remote_camera_id'),
+ filename,
+ width=self.get_argument('width', None),
+ height=self.get_argument('height', None),
+ callback=on_response)
+
+ else:
+ content = mediafiles.get_media_content(camera_config, filename)
+
+ # TODO add support for ?width=, ?height=
+
+ self.set_header('Content-Type', 'image/jpeg')
+ self.finish(content)
class MovieHandler(BaseHandler):
import os.path
import config
+import utils
_PICTURE_EXTS = ['.jpg']
# return []
full_paths = _list_media_files(target_dir, exts=_PICTURE_EXTS)
- picture_files = [p[len(target_dir):] for p in full_paths]
+ picture_files = []
+
+ for p in full_paths:
+ path = p[len(target_dir):]
+ if not path.startswith('/'):
+ path = '/' + path
+
+ picture_files.append({
+ 'path': path,
+ 'momentStr': utils.pretty_date_time(datetime.datetime.fromtimestamp(os.path.getmtime(p))),
+ 'timestamp': os.path.getmtime(p)
+ })
# TODO files listed here may not belong to the given camera
target_dir = camera_config.get('target_dir')
full_paths = _list_media_files(target_dir, exts=_MOVIE_EXTS)
- movie_files = [p[len(target_dir):] for p in full_paths]
+ movie_files = [{
+ 'path': p[len(target_dir):],
+ 'momentStr': utils.pretty_date_time(datetime.datetime.fromtimestamp(os.path.getmtime(p))),
+ 'timestamp': os.path.getmtime(p)
+ } for p in full_paths]
# TODO files listed here may not belong to the given camera
return movie_files
+
+
+def get_media_content(camera_config, path):
+ target_dir = camera_config.get('target_dir')
+
+ full_path = os.path.join(target_dir, path)
+
+ try:
+ with open(full_path) as f:
+ return f.read()
+
+ except Exception as e:
+ logging.error('failed to read file %(path)s: %(msg)s' % {
+ 'path': full_path, 'msg': unicode(e)})
+
+ return None
return callback(None)
- return callback(response['pictures'])
+ return callback(response)
http_client = AsyncHTTPClient()
http_client.fetch(request, on_response)
+
+
+def preview_picture(host, port, username, password, camera_id, filename, width, height, callback):
+ logging.debug('getting preview for file %(filename)s of remote camera %(id)s on %(host)s:%(port)s' % {
+ 'filename': filename,
+ 'id': camera_id,
+ 'host': host,
+ 'port': port})
+
+ uri = '/picture/%(id)s/preview/%(filename)s/?' % {
+ 'id': camera_id,
+ 'filename': filename}
+
+ if width:
+ uri += 'width=' + str(width)
+ if height:
+ uri += 'height=' + str(height)
+
+ request = _make_request(host, port, username, password, uri)
+
+ def on_response(response):
+ if response.error:
+ logging.error('failed to get preview for file %(filename)s of remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
+ 'filename': filename,
+ 'id': camera_id,
+ 'host': host,
+ 'port': port,
+ 'msg': unicode(response.error)})
+
+ return callback(None)
+
+ return callback(response.body)
+
+ http_client = AsyncHTTPClient()
+ http_client.fetch(request, on_response)
(r'^/config/(?P<camera_id>\d+)/(?P<op>get|set|rem|set_preview)/?$', handlers.ConfigHandler),
(r'^/config/(?P<op>add|list|list_devices)/?$', handlers.ConfigHandler),
(r'^/picture/(?P<camera_id>\d+)/(?P<op>current|list)/?$', handlers.PictureHandler),
- (r'^/picture/(?P<camera_id>\d+)/(?P<op>download)/(?P<filename>.+)/?$', handlers.PictureHandler),
+ (r'^/picture/(?P<camera_id>\d+)/(?P<op>download|preview)/(?P<filename>.+)/?$', handlers.PictureHandler),
(r'^/movie/(?P<camera_id>\d+)/(?P<op>list)/?$', handlers.MovieHandler),
(r'^/movie/(?P<camera_id>\d+)/(?P<op>download)/(?P<filename>.+)/?$', handlers.MovieHandler),
(r'^/update/?$', handlers.UpdateHandler),
text = u'{day} {month} {year}, {hm}'.format(
day=date_time.day,
- month=_(date_time.strftime('%B')),
+ month=date_time.strftime('%B'),
year=date_time.year,
hm=date_time.strftime('%H:%M')
)
}
+div.media-dialog {
+ overflow: auto;
+}
+
+div.media-list-group-title {
+ background-color: #313131;
+ font-size: 1.3em;
+ font-weight: bold;
+ text-align: center;
+ padding: 1em 0px 0.2em 0px;
+}
+
+div.media-list-entry {
+ height: 4em;
+ background-color: #414141;
+ border-bottom: 1px solid #313131;
+ cursor: pointer;
+ transition: background-color 0.1s linear;
+}
+
+div.media-list-entry:HOVER {
+ background-color: #494949;
+}
+
+div.media-list-entry:ACTIVE {
+ background-color: #3b3b3b;
+}
+
+img.media-list-preview {
+ float: left;
+ height: 3em;
+ margin: 0.45em;
+ border: 1px solid #212121;
+ box-shadow: 1px 1px 6px rgba(0,0,0,0.3);
+}
+
+div.media-list-entry-name {
+ font-size: 1.3em;
+ padding: 0.4em 0em;
+ text-align: center;
+ white-space: nowrap;
+}
+
+div.media-list-entry-moment {
+ font-size: 1em;
+ text-align: center;
+ white-space: nowrap;
+}
+
+div.picture-dialog-content {
+ position: relative;
+}
+
+div.picture-dialog-prev-arrow,
+div.picture-dialog-next-arrow {
+ position: absolute;
+ top: 45%;
+ background-color: rgba(0, 0, 0, 0.3);
+ background-image: url(../img/arrows.svg);
+ background-size: cover;
+ width: 3em;
+ height: 3em;
+ border-radius: 0.3em;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ cursor: pointer;
+}
+
+div.picture-dialog-prev-arrow {
+ left: 1em;
+}
+
+div.picture-dialog-next-arrow {
+ right: 1em;
+ background-position: -100% 0%;
+}
+
+img.picture-dialog-content {
+ border: 1px solid #292929;
+}
+
+
/* camera frames */
div.camera-list {
div.camera-top-bar {
padding: 3px 0px;
- font-size: 17px;
- height: 17px;
+ font-size: 20px;
+ height: 25px;
}
div.modal-container div.camera-top-bar {
span.camera-name {
float: left;
+ line-height: 25px;
}
div.camera-buttons {
div.camera-button {
display: inline-block;
- width: 16px;
- height: 16px;
+ width: 24px;
+ height: 24px;
background-image: url(../img/top-bar-buttons.svg);
+ background-size: cover;
margin-left: 3px;
cursor: pointer;
- opacity: 1;
transition: all 0.1s linear;
}
background-position: -100% 0px;
}
+div.camera-button.media-pictures {
+ background-position: -300% 0px;
+}
+
+div.camera-button.media-movies {
+ background-position: -400% 0px;
+}
+
div.camera-container {
position: relative;
padding: 0px;
width: 24px;
height: 24px;
}
-
- div.camera-top-bar {
- font-size: 20px;
- height: 25px;
- }
}
@media all and (max-width: 1900px) {
}
div.button.mouse-effect {
- opacity: 0.9;
+ opacity: 0.7;
transition: opacity 0.1s linear;
}
}
div.button.mouse-effect:ACTIVE {
- opacity: 0.6;
+ opacity: 0.7;
}
span.modal-title {
color: white;
+ font-size: 1.2em;
+ line-height: 1.2em;
}
div.modal-close-button {
position: absolute;
- top: 0.3em;
+ top: 0.2em;
right: 0.3em;
- width: 16px;
- height: 16px;
+ width: 1.1em;
+ height: 1.1em;
background-image: url(../img/top-bar-buttons.svg);
- opacity: 1;
+ background-size: cover;
cursor: pointer;
}
width: 100%;
margin: auto;
text-align: center;
+ table-layout: fixed;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="96"
+ height="48"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="arrows.svg"
+ inkscape:export-filename="/media/data/projects/motioneye/static/img/settings.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="5.0625"
+ inkscape:cx="43.687152"
+ inkscape:cy="14.670355"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1920"
+ inkscape:window-height="1030"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:snap-bbox="true"
+ inkscape:bbox-nodes="true"
+ inkscape:bbox-paths="true"
+ inkscape:snap-bbox-edge-midpoints="true"
+ inkscape:snap-bbox-midpoints="true"
+ inkscape:object-paths="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-smooth-nodes="true"
+ inkscape:snap-midpoints="true"
+ inkscape:snap-object-midpoints="true"
+ inkscape:snap-center="true"
+ inkscape:snap-page="true"
+ inkscape:snap-intersection-paths="true"
+ showguides="true"
+ inkscape:guide-bbox="true" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1004.3622)">
+ <path
+ style="fill:none;stroke:#3498db;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 30,1012.3622 -17,17 17,15"
+ id="path3755"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc" />
+ <path
+ sodipodi:nodetypes="ccc"
+ inkscape:connector-curvature="0"
+ id="path3757"
+ d="m 66,1012.3622 17,17 -17,15"
+ style="fill:none;stroke:#3498db;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ </g>
+</svg>
sodipodi:docname="motioneye.svg"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs6" /><sodipodi:namedview
pagecolor="#969696"
bordercolor="#666666"
inkscape:pageopacity="0.70196078"
inkscape:pageshadow="2"
inkscape:window-width="1920"
- inkscape:window-height="1027"
+ inkscape:window-height="1030"
id="namedview4"
showgrid="false"
- inkscape:zoom="5.0625"
+ inkscape:zoom="16.354248"
inkscape:cx="47.677361"
inkscape:cy="27.810209"
inkscape:window-x="0"
sodipodi:cy="96.59259"
sodipodi:rx="63.407406"
sodipodi:ry="60.444443"
- d="m 228.14814,96.59259 a 63.407406,60.444443 0 1 1 -126.81481,0 63.407406,60.444443 0 1 1 126.81481,0 z"
+ d="m 228.14814,96.59259 c 0,33.38254 -28.38846,60.44444 -63.4074,60.44444 -35.01895,0 -63.40741,-27.0619 -63.40741,-60.44444 0,-33.382544 28.38846,-60.444442 63.40741,-60.444442 35.01894,0 63.4074,27.061898 63.4074,60.444442 z"
transform="matrix(0.25864487,0,0,-0.27132354,-23.409347,-2.592158)" /><path
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8.06000042;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
d="m 19.344,27.142 c -2.295613,0 -4.348358,-0.983485 -5.661,-2.502 l -6.363,0 C 5.92392,24.64 4.8,23.51608 4.8,22.12 l 0,-10.8 C 4.8,9.92392 5.92392,8.8 7.32,8.8 l 23.76,0 c 1.39608,0 2.52,1.12392 2.52,2.52 l 0,10.8 c 0,1.39608 -1.12392,2.52 -2.52,2.52 l -0.936,0 0,0.324 a 1.440144,1.440144 0 1 1 -2.88,0 l 0,-0.324 -2.259,0 c -1.308955,1.518515 -3.365386,2.502 -5.661,2.502 z m -1.152,-1.062 2.304,0 c 0.39888,0 0.72,-0.32112 0.72,-0.72 l 0,-0.288 c 0,-0.39888 -0.32112,-0.72 -0.72,-0.72 l -2.304,0 c -0.39888,0 -0.72,0.32112 -0.72,0.72 l 0,0.288 c 0,0.39888 0.32112,0.72 0.72,0.72 z m 1.152,-4.32 c 2.942573,0 5.328,-2.385427 5.328,-5.328 0,-2.942573 -2.385427,-5.328 -5.328,-5.328 -2.942573,0 -5.328,2.385427 -5.328,5.328 0,2.942573 2.385427,5.328 5.328,5.328 z"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="48"
+ width="80"
height="16"
id="svg2"
version="1.1"
id="defs4" />
<sodipodi:namedview
id="base"
- pagecolor="#ffffff"
+ pagecolor="#b39a9a"
bordercolor="#666666"
borderopacity="1.0"
- inkscape:pageopacity="0.0"
+ inkscape:pageopacity="0.42745098"
inkscape:pageshadow="2"
- inkscape:zoom="1"
- inkscape:cx="35.119687"
- inkscape:cy="7.4678179"
+ inkscape:zoom="2.25"
+ inkscape:cx="111.39282"
+ inkscape:cy="5.668431"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
- inkscape:window-height="1027"
+ inkscape:window-height="1030"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
id="path3761"
d="m 36.5,1041.8622 7,0 0,5 -7,0 z"
style="fill:none;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
+ <path
+ style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#3498db;fill-opacity:1;stroke:none;stroke-width:8.06000042;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+ d="m 56.025,1041.1703 c -0.717378,0 -1.358863,0.3074 -1.769063,0.7819 l -1.988437,0 c -0.436275,0 -0.7875,0.3512 -0.7875,0.7875 l 0,3.375 c 0,0.4363 0.351225,0.7875 0.7875,0.7875 l 7.425,0 c 0.436275,0 0.7875,-0.3512 0.7875,-0.7875 l 0,-3.375 c 0,-0.4363 -0.351225,-0.7875 -0.7875,-0.7875 l -0.2925,0 0,-0.1012 a 0.450045,0.450045 0 1 0 -0.9,0 l 0,0.1012 -0.705938,0 c -0.409047,-0.4745 -1.051682,-0.7819 -1.769062,-0.7819 z m -0.36,0.3319 0.72,0 c 0.12465,0 0.225,0.1004 0.225,0.225 l 0,0.09 c 0,0.1247 -0.10035,0.225 -0.225,0.225 l -0.72,0 c -0.12465,0 -0.225,-0.1003 -0.225,-0.225 l 0,-0.09 c 0,-0.1246 0.10035,-0.225 0.225,-0.225 z m 0.36,1.35 c 0.919555,0 1.665,0.7455 1.665,1.665 0,0.9196 -0.745445,1.665 -1.665,1.665 -0.919553,0 -1.665,-0.7454 -1.665,-1.665 0,-0.9195 0.745447,-1.665 1.665,-1.665 z"
+ id="path3803"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:type="arc"
+ style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ id="path3815"
+ sodipodi:cx="7.8036885"
+ sodipodi:cy="8.2353296"
+ sodipodi:rx="5.6186557"
+ sodipodi:ry="5.3845448"
+ d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
+ transform="matrix(-1.1568604,0,0,1.2071587,65.027778,1034.4209)" />
+ <path
+ transform="matrix(-1.1568604,0,0,1.2071587,81.027778,1034.4209)"
+ d="m 13.422344,8.2353296 a 5.6186557,5.3845448 0 1 1 -11.2373112,0 5.6186557,5.3845448 0 1 1 11.2373112,0 z"
+ sodipodi:ry="5.3845448"
+ sodipodi:rx="5.6186557"
+ sodipodi:cy="8.2353296"
+ sodipodi:cx="7.8036885"
+ id="path3817"
+ style="fill:none;stroke:#3498db;stroke-width:0.84620845;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ sodipodi:type="arc" />
+ <path
+ style="fill:#3498db;fill-opacity:1;stroke:#3498db;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+ d="m 69.5,1040.8622 6,3.5 -6,3.5 z"
+ id="path3819"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
</g>
</svg>
return mapped;
};
+Array.prototype.sortKey = function (keyFunc) {
+ this.sort(function (e1, e2) {
+ var k1 = keyFunc(e1);
+ var k2 = keyFunc(e2);
+
+ if (k1 < k2) {
+ return -1;
+ }
+ else if (k1 > k2) {
+ return 1;
+ }
+ else {
+ return 0;
+ }
+ });
+};
+
/* UI initialization */
runModalDialog({title: message, buttons: 'yesno', onYes: onYes});
}
+function runPictureDialog(entries, pos) {
+ var content = $('<div class="picture-dialog-content"></div>');
+
+ var img = $('<img class="picture-dialog-content">');
+ content.append(img);
+
+ var prevArrow = $('<div class="picture-dialog-prev-arrow button mouse-effect" title="previous picture"></div>');
+ content.append(prevArrow);
+
+ var nextArrow = $('<div class="picture-dialog-next-arrow button mouse-effect" title="next picture"></div>');
+ content.append(nextArrow);
+
+ var windowWidth = $(window).width();
+
+ function updatePicture() {
+ var entry = entries[pos];
+ var width;
+ if (windowWidth <= 1000) {
+ width = parseInt(windowWidth * 0.9);
+ }
+ else {
+ width = parseInt(windowWidth * 0.5);
+ }
+
+ img.width(width);
+ img.attr('src', '/picture/' + entry.cameraId + '/preview' + entry.path + '?width=' + width);
+
+ prevArrow.css('display', pos > 0 ? '' : 'none');
+ nextArrow.css('display', pos < entries.length - 1 ? '' : 'none');
+
+ $('div.modal-container').find('span.modal-title:last').html(entry.name);
+ }
+
+ prevArrow.click(function () {
+ if (pos > 0) {
+ pos--;
+ }
+
+ updatePicture();
+ });
+
+ nextArrow.click(function () {
+ if (pos < entries.length - 1) {
+ pos++;
+ }
+
+ updatePicture();
+ });
+
+ img.load(updateModalDialogPosition);
+
+ runModalDialog({
+ title: ' ',
+ closeButton: true,
+ buttons: [
+ {caption: 'Close'},
+ {caption: 'Download', isDefault: true, click: function () {
+ window.location.href = img.attr('src').replace('preview', 'download');
+
+ return false;
+ }}
+ ],
+ content: content,
+ stack: true,
+ onShow: updatePicture
+ });
+}
+
function runAddCameraDialog() {
if (!$('#motionEyeSwitch')[0].checked) {
return runAlertDialog('Please enable motionEye first!');
}
+function runMediaDialog(cameraId, mediaType) {
+ var dialogDiv = $(
+ '<div class="media-dialog">' +
+ '</div>');
+
+ var windowWidth = $(window).width();
+
+ if (windowWidth <= 1000) {
+ dialogDiv.width(parseInt(windowWidth * 0.9));
+ dialogDiv.height(parseInt(windowWidth * 0.9 * 480 / 640));
+ }
+ else {
+ dialogDiv.width(parseInt(windowWidth * 0.5));
+ dialogDiv.height(parseInt(windowWidth * 0.5 * 480 / 640));
+ }
+
+ function fetchMedia() {
+ var progress = $('<div style="text-align: center; margin: 2px;"><img src="' + staticUrl + 'img/small-progress.gif"></div>');
+
+ cameraSelect.hide();
+ cameraSelect.before(progress);
+ cameraSelect.parent().find('div').remove(); /* remove any previous progress div */
+
+ var data = {
+ host: hostEntry.val(),
+ port: portEntry.val(),
+ username: usernameEntry.val(),
+ password: passwordEntry.val()
+ };
+
+ ajax('GET', '/config/list/', data, function (data) {
+ if (data == null || data.error) {
+ progress.remove();
+ if (passwordEntry.val()) { /* only show an error message when a password is supplied */
+ showErrorMessage(data && data.error);
+ }
+
+ return;
+ }
+
+ cameraSelect.html('');
+ progress.remove();
+
+ if (data.error || !data.cameras) {
+ return;
+ }
+
+ data.cameras.forEach(function (info) {
+ cameraSelect.append('<option value="' + info.id + '">' + info.name + '</option>');
+ });
+
+ cameraSelect.show();
+ });
+ }
+
+ showModalDialog('<div class="modal-progress"></div>');
+
+ /* fetch the media list */
+ ajax('GET', '/' + mediaType + '/' + cameraId + '/list/', null, function (data) {
+ if (data == null || data.error) {
+ hideModalDialog();
+ showErrorMessage(data && data.error);
+ return;
+ }
+
+ /* sort and group the media */
+ var groups = {};
+ data.mediaList.forEach(function (media) {
+ var path = media.path;
+ var parts = path.split('/');
+ var keyParts = parts.splice(0, parts.length - 1);
+ var key = keyParts.join('/');
+ var list = (groups[key] = groups[key] || []);
+
+ list.push({
+ 'path': path,
+ 'group': key,
+ 'name': parts[parts.length - 1],
+ 'cameraId': cameraId,
+ 'momentStr': media.momentStr,
+ 'timestamp': media.timestamp
+ });
+ });
+
+ var keys = Object.keys(groups);
+ keys.sort();
+
+ keys.forEach(function (key) {
+ if (key) {
+ var groupDiv = $('<div class="media-list-group-title">' + key + '</div>');
+ dialogDiv.append(groupDiv);
+ }
+
+ var entries = groups[key];
+ entries.sortKey(function (e) {return e.timestamp;});
+
+ entries.forEach(function (entry, pos) {
+ var entryDiv = $('<div class="media-list-entry"></div>');
+
+ var previewImg = $('<img class="media-list-preview" src="/' + mediaType + '/' + cameraId + '/preview' + entry.path + '"/>');
+ entryDiv.append(previewImg);
+
+ var nameDiv = $('<div class="media-list-entry-name">' + entry.name + '</div>');
+ entryDiv.append(nameDiv);
+
+ var momentDiv = $('<div class="media-list-entry-moment">' + entry.momentStr + '</div>');
+ entryDiv.append(momentDiv);
+ entryDiv.click(function () {
+ if (mediaType === 'picture') {
+ runPictureDialog(entries, pos);
+ }
+ });
+
+ dialogDiv.append(entryDiv);
+ });
+ });
+
+ /* scroll to bottom */
+ dialogDiv.find('img.media-list-preview:last').load(function () {
+ dialogDiv.scrollTop(dialogDiv.prop('scrollHeight'));
+ });
+
+ var title;
+ if (mediaType === 'picture') {
+ title = 'Pictures taken by ' + data.cameraName;
+ }
+ else {
+ title = 'Movies recored by ' + data.cameraName;
+ }
+
+ runModalDialog({
+ title: title,
+ closeButton: true,
+ buttons: '',
+ content: dialogDiv,
+ onShow: function () {
+ dialogDiv.scrollTop(dialogDiv.prop('scrollHeight'));
+ }
+ });
+ });
+}
+
+
/* camera frames */
function addCameraFrameUi(cameraId, cameraName, framerate) {
'<div class="camera-top-bar">' +
'<span class="camera-name"></span>' +
'<div class="camera-buttons">' +
+ '<div class="button camera-button mouse-effect media-pictures" title="pictures"></div>' +
+ '<div class="button camera-button mouse-effect media-movies" title="movies"></div>' +
'<div class="button camera-button mouse-effect configure" title="configure"></div>' +
- '<div class="button camera-button mouse-effect full-screen" title="full screen"></div>' +
+// '<div class="button camera-button mouse-effect full-screen" title="full screen"></div>' +
'</div>' +
'</div>' +
'<div class="camera-container">' +
var nameSpan = cameraFrameDiv.find('span.camera-name');
var configureButton = cameraFrameDiv.find('div.camera-button.configure');
var fullScreenButton = cameraFrameDiv.find('div.camera-button.full-screen');
+ var picturesButton = cameraFrameDiv.find('div.camera-button.media-pictures');
+ var moviesButton = cameraFrameDiv.find('div.camera-button.media-movies');
var cameraPlaceholder = cameraFrameDiv.find('div.camera-placeholder');
var cameraProgress = cameraFrameDiv.find('div.camera-progress');
var cameraImg = cameraFrameDiv.find('img.camera');
};
}(cameraId));
+ picturesButton.click(function (cameraId) {
+ return function () {
+ runMediaDialog(cameraId, 'picture');
+ };
+ }(cameraId));
+
+ moviesButton.click(function (cameraId) {
+ return function () {
+ runMediaDialog(cameraId, 'movie');
+ };
+ }(cameraId));
+
/* error and load handlers */
cameraImg.error(function () {
this.error = true;
var windowAspectRatio = windowWidth / windowHeight;
var frameIndex = cameraFrameDiv.index();
var pageContainer = $('div.page-container');
+
+ if (frameImg.hasClass('error')) {
+ return; /* no full screen for erroneous cameras */
+ }
fullScreenCameraId = cameraId;
+var _modalDialogContexts = [];
+
/* UI widgets */
/* modal dialog */
-function showModalDialog(content, onClose) {
+function showModalDialog(content, onClose, onShow, stack) {
var glass = $('div.modal-glass');
var container = $('div.modal-container');
if (container.is(':animated')) {
return setTimeout(function () {
- showModalDialog(content, onClose);
+ showModalDialog(content, onClose, onShow, stack);
}, 100);
}
- if (container.is(':visible')) {
+ if (container.is(':visible') && stack) {
/* the modal dialog is already visible,
* we just replace the content */
- if (container[0]._onClose) {
- container[0]._onClose();
- }
+ var children = container.children(':visible');
+ _modalDialogContexts.push({
+ children: children,
+ onClose: container[0]._onClose,
+ });
+
+ children.css('display', 'none');
- container[0]._onClose = onClose; /* remember the onClose handler */
- container.html(content);
+ container[0]._onClose = onClose; /* set the new onClose handler */
+ container.append(content);
updateModalDialogPosition();
+ if (onShow) {
+ onShow();
+ }
+
return;
}
container.css('display', 'block');
updateModalDialogPosition();
container.animate({'opacity': '1'}, 200);
+
+ if (onShow) {
+ onShow();
+ }
}
function hideModalDialog() {
}, 100);
}
+ if (_modalDialogContexts.length) {
+ if (container[0]._onClose) {
+ container[0]._onClose();
+ }
+
+ container.children(':visible').remove();
+
+ var context = _modalDialogContexts.pop();
+ context.children.css('display', '');
+ container[0]._onClose = context.onClose;
+ updateModalDialogPosition();
+
+ return;
+ }
+
glass.animate({'opacity': '0'}, 200, function () {
glass.css('display', 'none');
});
var windowWidth = $(window).width();
var windowHeight = $(window).height();
- var modalWidth = container.width();
- var modalHeight = container.height();
+ var modalWidth = container.width() + 10 /* the margins */;
+ var modalHeight = container.height() + 10 /* the margins */;
container.css('left', Math.floor((windowWidth - modalWidth) / 2));
container.css('top', Math.floor((windowHeight - modalHeight) / 2));
if (info.click) {
var oldClick = info.click;
info.click = function () {
- hideModalDialog();
-
if (oldClick() == false) {
return;
}
+
+ hideModalDialog();
};
}
else {
* * onOk: Function
* * onCancel: Function
* * onClose: Function
+ * * onShow: Function
+ * * stack: Boolean
*/
var content = $('<div></div>');
}
var handleKeyUp = function (e) {
+ if (!content.is(':visible')) {
+ return;
+ }
+
switch (e.which) {
case 13:
- if (defaultClick) {
- if (defaultClick() == false) {
- return;
- };
+ if (defaultClick && defaultClick() == false) {
+ return;
}
/* intentionally no break */
$('html').bind('keyup', handleKeyUp);
/* and finally, show the dialog */
- showModalDialog(content, onClose);
+
+ showModalDialog(content, onClose, options.onShow, options.stack);
/* focus the default button if nothing else is focused */
if (content.find('*:focus').length === 0) {