]> www.vanbest.org Git - motioneye-debian/commitdiff
added a media browser, previewer and downloader
authorCalin Crisan <ccrisan@gmail.com>
Sun, 10 Nov 2013 17:21:58 +0000 (19:21 +0200)
committerCalin Crisan <ccrisan@gmail.com>
Sun, 10 Nov 2013 17:21:58 +0000 (19:21 +0200)
13 files changed:
doc/todo.txt
src/handlers.py
src/mediafiles.py
src/remote.py
src/server.py
src/utils.py
static/css/main.css
static/css/ui.css
static/img/arrows.svg [new file with mode: 0644]
static/img/motioneye.svg
static/img/top-bar-buttons.svg
static/js/main.js
static/js/ui.js

index e7968cd11f0689b426c32518d5ba8a6e800f9602..ddcf181a6575cb80f344731f91d331b03cfa92c4 100644 (file)
@@ -1,7 +1,8 @@
+-> 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
index cdef782eaf05bfb111d7a04c817daaee8fb3f691..eb62cb28adba017cdc306e200c7dcc76dc89163b 100644 (file)
@@ -18,6 +18,7 @@
 import base64
 import json
 import logging
+import os
 
 from tornado.web import RequestHandler, HTTPError, asynchronous
 
@@ -531,6 +532,9 @@ class PictureHandler(BaseHandler):
         elif op == 'download':
             self.download(camera_id, filename)
         
+        elif op == 'preview':
+            self.preview(camera_id, filename)
+        
         else:
             raise HTTPError(400, 'unknown operation')
     
@@ -575,9 +579,9 @@ class PictureHandler(BaseHandler):
                         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}})
@@ -594,16 +598,95 @@ class PictureHandler(BaseHandler):
         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):
index 57014732580378dbdf92424b4f282def637c2ca0..f5cd3a3e6fb838a5233aafc6aea679471eedb840 100644 (file)
@@ -20,6 +20,7 @@ import logging
 import os.path
 
 import config
+import utils
 
 
 _PICTURE_EXTS = ['.jpg']
@@ -109,7 +110,18 @@ def list_pictures(camera_config):
 #         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
     
@@ -120,8 +132,28 @@ def list_movies(camera_config):
     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
index d4afebfc0ab1f2754f5fdfd7f1e060acc3cc71d6..21299da9aedc467970c5d4d64af4f2458ac5b858 100644 (file)
@@ -237,7 +237,42 @@ def list_pictures(host, port, username, password, camera_id, callback):
             
             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)
index fcac5fa6e08f2c29ec75e1995644c834165577be..f7ead25a4ea9d4bc270410c3766041e5986cbf81 100644 (file)
@@ -45,7 +45,7 @@ application = Application(
         (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),
index 4fad4d8f3569c0f7c9bd5db214915c555830021b..25c9d65189597c66f300e49647d559369fda1be7 100644 (file)
@@ -29,7 +29,7 @@ def pretty_date_time(date_time, tzinfo=None):
 
     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')
     )
index a08d5f9b314b72e7816cb1ab4a492c3eb6f2e889..6c44ffb29c3b490cb914edaca32c002572a62ded 100644 (file)
@@ -355,6 +355,89 @@ table.add-camera-dialog input[type=password] {
 }
 
 
+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 {
@@ -393,8 +476,8 @@ div.camera-frame:HOVER {
 
 div.camera-top-bar {
     padding: 3px 0px;
-    font-size: 17px;
-    height: 17px;
+    font-size: 20px;
+    height: 25px;
 }
 
 div.modal-container div.camera-top-bar {
@@ -403,6 +486,7 @@ div.modal-container div.camera-top-bar {
 
 span.camera-name {
     float: left;
+    line-height: 25px;
 }
 
 div.camera-buttons {
@@ -411,12 +495,12 @@ 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;
 }
 
@@ -432,6 +516,14 @@ div.camera-button.configure {
     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;
@@ -521,11 +613,6 @@ img.camera-progress {
         width: 24px;
         height: 24px;
     }
-    
-    div.camera-top-bar {
-        font-size: 20px;
-        height: 25px;
-    }
 }
 
 @media all and (max-width: 1900px) {
index b9c6cc23bd55fc956578a945593a91c362e52165..6b55a69579b34b03c0abb95f17f0c867b2927987 100644 (file)
@@ -60,7 +60,7 @@ div.button.dialog.default {
 }
 
 div.button.mouse-effect {
-    opacity: 0.9;
+    opacity: 0.7;
     transition: opacity 0.1s linear;
 }
 
@@ -69,7 +69,7 @@ div.button.mouse-effect:HOVER {
 }
 
 div.button.mouse-effect:ACTIVE {
-    opacity: 0.6;
+    opacity: 0.7;
 }
 
 
@@ -310,16 +310,18 @@ div.modal-title-bar {
 
 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;
 }
 
@@ -327,6 +329,7 @@ table.modal-buttons-container {
     width: 100%;
     margin: auto;
     text-align: center;
+    table-layout: fixed;
     -webkit-user-select: none;
     -moz-user-select: none;
     user-select: none;
diff --git a/static/img/arrows.svg b/static/img/arrows.svg
new file mode 100644 (file)
index 0000000..f1239a7
--- /dev/null
@@ -0,0 +1,86 @@
+<?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>
index 6e8677c526cca81f54de9d03f6a446bc6d986a4a..acc09f47dc5c434fac505d1c10bb5d9aff4a19fe 100644 (file)
@@ -18,7 +18,7 @@
    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"
@@ -50,7 +50,7 @@
        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"
index 32203ba8dcd42aa30c75469004fadec49bcd6ca3..a10c0065d221f83a35a11353a6c497e8c1274cf3 100644 (file)
@@ -9,7 +9,7 @@
    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>
index 234fc96efcee7ca3cf39b707702d28038d12eae4..cb6d00da7101a3b217d1a15f168e4d6f34d66f8e 100644 (file)
@@ -122,6 +122,23 @@ Array.prototype.map = function (func, thisArg) {
     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 */
 
@@ -992,6 +1009,74 @@ function runConfirmDialog(message, onYes) {
     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!');
@@ -1206,6 +1291,149 @@ function runAddCameraDialog() {
 }
 
 
+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) {
@@ -1223,8 +1451,10 @@ 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">' +
@@ -1237,6 +1467,8 @@ function addCameraFrameUi(cameraId, cameraName, framerate) {
     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');
@@ -1290,6 +1522,18 @@ function addCameraFrameUi(cameraId, cameraName, framerate) {
         };
     }(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;
@@ -1420,6 +1664,10 @@ function doFullScreenCamera(cameraId) {
     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;
 
index 67ab5e1e9ba7cd55f517864d55f59b48ed0259b2..88198bbc990984a4d925e04170d8fd1cfcf71c94 100644 (file)
@@ -1,4 +1,6 @@
 
+var _modalDialogContexts = [];
+
 
     /* UI widgets */
 
@@ -525,28 +527,36 @@ function makeRegexValidator($input, regex, required) {
 
     /* 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;
     }
     
@@ -559,6 +569,10 @@ function showModalDialog(content, onClose) {
     container.css('display', 'block');
     updateModalDialogPosition();
     container.animate({'opacity': '1'}, 200);
+    
+    if (onShow) {
+        onShow();
+    }
 }
 
 function hideModalDialog() {
@@ -571,6 +585,21 @@ 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');
     });
@@ -594,8 +623,8 @@ function updateModalDialogPosition() {
     
     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));
@@ -624,11 +653,11 @@ function makeModalDialogButtons(buttonsInfo) {
         if (info.click) {
             var oldClick = info.click;
             info.click = function () {
-                hideModalDialog();
-                
                 if (oldClick() == false) {
                     return;
                 }
+                
+                hideModalDialog();
             };
         }
         else {
@@ -681,6 +710,8 @@ function runModalDialog(options) {
      * * onOk: Function
      * * onCancel: Function
      * * onClose: Function
+     * * onShow: Function
+     * * stack: Boolean
      */
     
     var content = $('<div></div>');
@@ -748,12 +779,14 @@ function runModalDialog(options) {
     }
     
     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 */
            
@@ -776,7 +809,8 @@ function runModalDialog(options) {
     $('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) {