浏览代码

Implements custom context menu to flip the local video

master
hristoterezov 9 年前
父节点
当前提交
c3338d3bf2

+ 1
- 1
Makefile 查看文件

@@ -3,7 +3,7 @@ BROWSERIFY = ./node_modules/.bin/browserify
3 3
 UGLIFYJS = ./node_modules/.bin/uglifyjs
4 4
 EXORCIST = ./node_modules/.bin/exorcist
5 5
 CLEANCSS = ./node_modules/.bin/cleancss
6
-CSS_FILES = font.css toastr.css main.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css
6
+CSS_FILES = font.css toastr.css main.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css jquery.contextMenu.css
7 7
 DEPLOY_DIR = libs
8 8
 BROWSERIFY_FLAGS = -d
9 9
 OUTPUT_DIR = .

+ 1
- 0
app.js 查看文件

@@ -3,6 +3,7 @@
3 3
 
4 4
 import "babel-polyfill";
5 5
 import "jquery";
6
+import "jquery-contextmenu";
6 7
 import "jquery-ui";
7 8
 import "strophe";
8 9
 import "strophe-disco";

+ 206
- 0
css/jquery.contextMenu.css 查看文件

@@ -0,0 +1,206 @@
1
+@charset "UTF-8";
2
+/*!
3
+ * jQuery contextMenu - Plugin for simple contextMenu handling
4
+ *
5
+ * Version: v2.1.1
6
+ *
7
+ * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF)
8
+ * Web: http://swisnl.github.io/jQuery-contextMenu/
9
+ *
10
+ * Copyright (c) 2011-2016 SWIS BV and contributors
11
+ *
12
+ * Licensed under
13
+ *   MIT License http://www.opensource.org/licenses/mit-license
14
+ *
15
+ * Date: 2016-02-28T09:53:18.890Z
16
+ */
17
+@font-face {
18
+  font-family: "context-menu-icons";
19
+  font-style: normal; 
20
+  font-weight: normal;
21
+
22
+  src: url("font/context-menu-icons.eot?2qmzf");
23
+  src: url("font/context-menu-icons.eot?2qmzf#iefix") format("embedded-opentype"), url("font/context-menu-icons.woff2?2qmzf") format("woff2"), url("font/context-menu-icons.woff?2qmzf") format("woff"), url("font/context-menu-icons.ttf?2qmzf") format("truetype");
24
+}
25
+
26
+.context-menu-icon:before {
27
+  position: absolute;
28
+  top: 50%;
29
+  left: 0;
30
+  width: 28px; 
31
+  font-family: "context-menu-icons";
32
+  font-size: 16px;
33
+  font-style: normal;
34
+  font-weight: normal;
35
+  line-height: 1;
36
+  color: #2980b9;
37
+  text-align: center;
38
+  -webkit-transform: translateY(-50%);
39
+      -ms-transform: translateY(-50%);
40
+       -o-transform: translateY(-50%);
41
+          transform: translateY(-50%);
42
+
43
+  -webkit-font-smoothing: antialiased;
44
+  -moz-osx-font-smoothing: grayscale;
45
+}
46
+
47
+.context-menu-icon-add:before {
48
+  content: "";
49
+}
50
+
51
+.context-menu-icon-copy:before {
52
+  content: "";
53
+}
54
+
55
+.context-menu-icon-cut:before {
56
+  content: "";
57
+}
58
+
59
+.context-menu-icon-delete:before {
60
+  content: "";
61
+}
62
+
63
+.context-menu-icon-edit:before {
64
+  content: "";
65
+}
66
+
67
+.context-menu-icon-paste:before {
68
+  content: "";
69
+}
70
+
71
+.context-menu-icon-quit:before {
72
+  content: "";
73
+}
74
+
75
+.context-menu-icon.context-menu-hover:before {
76
+  color: #fff;
77
+}
78
+
79
+.context-menu-list {
80
+  position: absolute; 
81
+  display: inline-block;
82
+  min-width: 180px;
83
+  max-width: 360px;
84
+  padding: 4px 0;
85
+  margin: 5px;
86
+  font-family: inherit;
87
+  font-size: inherit;
88
+  list-style-type: none;
89
+  background: #fff;
90
+  border: 1px solid #bebebe;
91
+  border-radius: 3px;
92
+  -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, .5);
93
+          box-shadow: 0 2px 5px rgba(0, 0, 0, .5);
94
+}
95
+
96
+.context-menu-item {
97
+  position: relative;
98
+  padding: 3px 28px;
99
+  color: #2f2f2f;
100
+  -webkit-user-select: none;
101
+     -moz-user-select: none;
102
+      -ms-user-select: none;
103
+          user-select: none; 
104
+  background-color: #fff;
105
+}
106
+
107
+.context-menu-separator {
108
+  padding: 0; 
109
+  margin: 5px 0;
110
+  border-bottom: 1px solid #e6e6e6;
111
+}
112
+
113
+.context-menu-item > label > input,
114
+.context-menu-item > label > textarea {
115
+  -webkit-user-select: text;
116
+     -moz-user-select: text;
117
+      -ms-user-select: text;
118
+          user-select: text;
119
+}
120
+
121
+.context-menu-item.context-menu-hover {
122
+  color: #fff;
123
+  cursor: pointer; 
124
+  background-color: #2980b9;
125
+}
126
+
127
+.context-menu-item.context-menu-disabled {
128
+  color: #626262; 
129
+  background-color: #fff;
130
+}
131
+
132
+.context-menu-item.context-menu-disabled {
133
+  color: #626262;
134
+}
135
+
136
+.context-menu-input.context-menu-hover,
137
+.context-menu-item.context-menu-disabled.context-menu-hover {
138
+  cursor: default; 
139
+  background-color: #eee;
140
+}
141
+
142
+.context-menu-submenu:after {
143
+  position: absolute;
144
+  top: 50%;
145
+  right: 8px;
146
+  z-index: 1; 
147
+  width: 0;
148
+  height: 0;
149
+  content: '';
150
+  border-color: transparent transparent transparent #2f2f2f;
151
+  border-style: solid;
152
+  border-width: 4px 0 4px 4px;
153
+  -webkit-transform: translateY(-50%);
154
+      -ms-transform: translateY(-50%);
155
+       -o-transform: translateY(-50%);
156
+          transform: translateY(-50%);
157
+}
158
+
159
+/**
160
+ * Inputs
161
+ */
162
+.context-menu-item.context-menu-input {
163
+  padding: 5px 10px;
164
+}
165
+
166
+/* vertically align inside labels */
167
+.context-menu-input > label > * {
168
+  vertical-align: top;
169
+}
170
+
171
+/* position checkboxes and radios as icons */
172
+.context-menu-input > label > input[type="checkbox"],
173
+.context-menu-input > label > input[type="radio"] {
174
+  position: relative;
175
+  top: 3px;
176
+}
177
+
178
+.context-menu-input > label,
179
+.context-menu-input > label > input[type="text"],
180
+.context-menu-input > label > textarea,
181
+.context-menu-input > label > select {
182
+  display: block;
183
+  width: 100%; 
184
+  -webkit-box-sizing: border-box;
185
+     -moz-box-sizing: border-box;
186
+          box-sizing: border-box;
187
+}
188
+
189
+.context-menu-input > label > textarea {
190
+  height: 100px;
191
+}
192
+
193
+.context-menu-item > .context-menu-list {
194
+  top: 5px; 
195
+  /* re-positioned by js */
196
+  right: -5px;
197
+  display: none;
198
+}
199
+
200
+.context-menu-item.context-menu-visible > .context-menu-list {
201
+  display: block;
202
+}
203
+
204
+.context-menu-accesskey {
205
+  text-decoration: underline;
206
+}

+ 2
- 1
lang/main.json 查看文件

@@ -99,7 +99,8 @@
99 99
         "mute": "Participant is muted",
100 100
         "kick": "Kick out",
101 101
         "muted": "Muted",
102
-        "domute": "Mute"
102
+        "domute": "Mute",
103
+        "flip": "Flip"
103 104
 
104 105
     },
105 106
     "connectionindicator":

+ 24
- 3
modules/UI/videolayout/LargeVideo.js 查看文件

@@ -168,6 +168,7 @@ class VideoContainer extends LargeContainer {
168 168
         super();
169 169
         this.stream = null;
170 170
         this.videoType = null;
171
+        this.localFlipX = true;
171 172
 
172 173
         this.isVisible = false;
173 174
 
@@ -284,13 +285,25 @@ class VideoContainer extends LargeContainer {
284 285
         }
285 286
 
286 287
         stream.attach(this.$video[0]);
287
-
288
-        let flipX = stream.isLocal() && !this.isScreenSharing();
288
+        let flipX = stream.isLocal() && this.localFlipX;
289 289
         this.$video.css({
290 290
             transform: flipX ? 'scaleX(-1)' : 'none'
291 291
         });
292 292
     }
293 293
 
294
+    /**
295
+     * Changes the flipX state of the local video.
296
+     * @param val {boolean} true if flipped.
297
+     */
298
+    setLocalFlipX(val) {
299
+        this.localFlipX = val;
300
+        if(!this.$video || !this.stream || !this.stream.isLocal())
301
+            return;
302
+        this.$video.css({
303
+            transform: this.localFlipX ? 'scaleX(-1)' : 'none'
304
+        });
305
+    }
306
+
294 307
     /**
295 308
      * Check if current video stream is screen sharing.
296 309
      * @returns {boolean}
@@ -453,7 +466,7 @@ export default class LargeVideoManager {
453 466
         } else {
454 467
             preUpdate = Promise.resolve();
455 468
         }
456
-        
469
+
457 470
         preUpdate.then(() => {
458 471
             let {id, stream, videoType, resolve} = this.newStreamData;
459 472
             this.newStreamData = null;
@@ -651,4 +664,12 @@ export default class LargeVideoManager {
651 664
             }
652 665
         });
653 666
     }
667
+
668
+    /**
669
+     * Changes the flipX state of the local video.
670
+     * @param val {boolean} true if flipped.
671
+     */
672
+    onLocalFlipXChange(val) {
673
+        this.videoContainer.setLocalFlipX(val);
674
+    }
654 675
 }

+ 56
- 8
modules/UI/videolayout/LocalVideo.js 查看文件

@@ -4,16 +4,15 @@ import UIUtil from "../util/UIUtil";
4 4
 import UIEvents from "../../../service/UI/UIEvents";
5 5
 import SmallVideo from "./SmallVideo";
6 6
 
7
-var LargeVideo = require("./LargeVideo");
8
-
9 7
 const RTCUIUtils = JitsiMeetJS.util.RTCUIHelper;
10 8
 const TrackEvents = JitsiMeetJS.events.track;
11 9
 
12 10
 function LocalVideo(VideoLayout, emitter) {
13 11
     this.videoSpanId = "localVideoContainer";
14 12
     this.container = $("#localVideoContainer").get(0);
13
+    this.localVideoId = null;
15 14
     this.bindHoverHandler();
16
-    this.flipX = true;
15
+    this._buildContextMenu();
17 16
     this.isLocal = true;
18 17
     this.emitter = emitter;
19 18
     Object.defineProperty(this, 'id', {
@@ -165,9 +164,8 @@ LocalVideo.prototype.changeVideo = function (stream) {
165 164
     localVideoContainerSelector.off('click');
166 165
     localVideoContainerSelector.on('click', localVideoClick);
167 166
 
168
-    this.flipX = stream.videoType != "desktop";
169 167
     let localVideo = document.createElement('video');
170
-    localVideo.id = 'localVideo_' + stream.getId();
168
+    localVideo.id = this.localVideoId = 'localVideo_' + stream.getId();
171 169
 
172 170
     RTCUIUtils.setAutoPlay(localVideo, true);
173 171
     RTCUIUtils.setVolume(localVideo, 0);
@@ -182,9 +180,9 @@ LocalVideo.prototype.changeVideo = function (stream) {
182 180
     // onclick has to be used with Temasys plugin
183 181
     localVideo.onclick = localVideoClick;
184 182
 
185
-    if (this.flipX) {
186
-        $(localVideo).addClass("flipVideoX");
187
-    }
183
+    let isVideo = stream.videoType != "desktop";
184
+    this._enableDisableContextMenu(isVideo);
185
+    this.setFlipX(isVideo? APP.settings.getLocalFlipX() : false);
188 186
 
189 187
     // Attach WebRTC stream
190 188
     localVideo = stream.attach(localVideo);
@@ -222,4 +220,54 @@ LocalVideo.prototype.setVisible = function(visible) {
222 220
     }
223 221
 };
224 222
 
223
+/**
224
+ * Sets the flipX state of the video.
225
+ * @param val {boolean} true for flipped otherwise false;
226
+ */
227
+LocalVideo.prototype.setFlipX = function (val) {
228
+    this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
229
+    if(!this.localVideoId)
230
+        return;
231
+    if(val) {
232
+        this.selectVideoElement().addClass("flipVideoX");
233
+    } else {
234
+        this.selectVideoElement().removeClass("flipVideoX");
235
+    }
236
+};
237
+
238
+/**
239
+ * Builds the context menu for the local video.
240
+ */
241
+LocalVideo.prototype._buildContextMenu = function () {
242
+    $.contextMenu({
243
+        selector: '#' + this.videoSpanId,
244
+        zIndex: 10000,
245
+        items: {
246
+            flip: {
247
+                name: "Flip",
248
+                callback: () => {
249
+                    let val = !APP.settings.getLocalFlipX();
250
+                    this.setFlipX(val);
251
+                    APP.settings.setLocalFlipX(val);
252
+                }
253
+            }
254
+        },
255
+        events: {
256
+            show : function(options){
257
+                options.items.flip.name =
258
+                    APP.translation.translateString("videothumbnail.flip");
259
+            }
260
+        }
261
+    });
262
+};
263
+
264
+/**
265
+ * Enables or disables the context menu for the local video.
266
+ * @param enable {boolean} true for enable, false for disable
267
+ */
268
+LocalVideo.prototype._enableDisableContextMenu = function (enable) {
269
+    if($('#' + this.videoSpanId).contextMenu)
270
+        $('#' + this.videoSpanId).contextMenu(enable);
271
+};
272
+
225 273
 export default LocalVideo;

+ 0
- 3
modules/UI/videolayout/SmallVideo.js 查看文件

@@ -165,9 +165,6 @@ SmallVideo.createStreamElement = function (stream) {
165 165
         console.log("(TIME) Render " + type + ":\t",
166 166
                     now);
167 167
     };
168
-
169
-    element.oncontextmenu = function () { return false; };
170
-
171 168
     return element;
172 169
 };
173 170
 

+ 22
- 0
modules/UI/videolayout/VideoLayout.js 查看文件

@@ -33,6 +33,11 @@ var eventEmitter = null;
33 33
  */
34 34
 var pinnedId = null;
35 35
 
36
+/**
37
+ * flipX state of the localVideo
38
+ */
39
+let localFlipX = null;
40
+
36 41
 /**
37 42
  * On contact list item clicked.
38 43
  */
@@ -92,6 +97,11 @@ let largeVideo;
92 97
 var VideoLayout = {
93 98
     init (emitter) {
94 99
         eventEmitter = emitter;
100
+        eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED, function (val) {
101
+                localFlipX = val;
102
+                if(largeVideo)
103
+                    largeVideo.onLocalFlipXChange(val);
104
+            });
95 105
         localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
96 106
         // sets default video type of local video
97 107
         localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
@@ -105,6 +115,9 @@ var VideoLayout = {
105 115
 
106 116
     initLargeVideo (isSideBarVisible) {
107 117
         largeVideo = new LargeVideoManager();
118
+        if(localFlipX) {
119
+            largeVideo.onLocalFlipXChange(localFlipX);
120
+        }
108 121
         largeVideo.updateContainerSize(isSideBarVisible);
109 122
         AudioLevels.init();
110 123
     },
@@ -1084,6 +1097,15 @@ var VideoLayout = {
1084 1097
             videoResolutionLabel.css({display: "block"});
1085 1098
         else if (!isResolutionHD && videoResolutionLabel.is(":visible"))
1086 1099
             videoResolutionLabel.css({display: "none"});
1100
+    },
1101
+
1102
+    /**
1103
+     * Sets the flipX state of the local video.
1104
+     * @param {boolean} true for flipped otherwise false;
1105
+     */
1106
+    setLocalFlipX: function (val) {
1107
+        this.localFlipX = val;
1108
+
1087 1109
     }
1088 1110
 };
1089 1111
 

+ 19
- 0
modules/settings/Settings.js 查看文件

@@ -6,6 +6,7 @@ let language = null;
6 6
 let cameraDeviceId = '';
7 7
 let micDeviceId = '';
8 8
 let welcomePageDisabled = false;
9
+let localFlipX = null;
9 10
 
10 11
 function supportsLocalStorage() {
11 12
     try {
@@ -31,6 +32,7 @@ if (supportsLocalStorage()) {
31 32
     }
32 33
 
33 34
     email = UIUtil.unescapeHtml(window.localStorage.email || '');
35
+    localFlipX = JSON.parse(window.localStorage.localFlipX || true);
34 36
     displayName = UIUtil.unescapeHtml(window.localStorage.displayname || '');
35 37
     language = window.localStorage.language;
36 38
     cameraDeviceId = window.localStorage.cameraDeviceId || '';
@@ -87,6 +89,23 @@ export default {
87 89
         window.localStorage.language = lang;
88 90
     },
89 91
 
92
+    /**
93
+     * Sets new flipX state of local video and saves it to the local storage.
94
+     * @param {string} val flipX state of local video
95
+     */
96
+    setLocalFlipX: function (val) {
97
+        localFlipX = val;
98
+        window.localStorage.localFlipX = val;
99
+    },
100
+
101
+    /**
102
+     * Returns flipX state of local video.
103
+     * @returns {string} flipX
104
+     */
105
+    getLocalFlipX: function () {
106
+        return localFlipX;
107
+    },
108
+
90 109
     /**
91 110
      * Get device id of the camera which is currently in use.
92 111
      * Empty string stands for default device.

+ 4
- 0
package.json 查看文件

@@ -24,6 +24,7 @@
24 24
     "jquery": "~2.1.1",
25 25
     "jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0",
26 26
     "lib-jitsi-meet": "jitsi/lib-jitsi-meet",
27
+    "jquery-contextmenu": "*",
27 28
     "jquery-ui": "^1.10.5",
28 29
     "jssha": "1.5.0",
29 30
     "retry": "0.6.1",
@@ -98,6 +99,9 @@
98 99
     "jQuery-Impromptu": {
99 100
       "depends": "jquery:jQuery"
100 101
     },
102
+    "jquery-contextmenu": {
103
+        "depends": "jquery:jQuery"
104
+    },
101 105
     "autosize": {
102 106
       "depends": "jquery:jQuery"
103 107
     }

+ 5
- 1
service/UI/UIEvents.js 查看文件

@@ -71,5 +71,9 @@ export default {
71 71
      * Notifies interested listeners that the follow-me feature is enabled or
72 72
      * disabled.
73 73
      */
74
-    FOLLOW_ME_ENABLED: "UI.follow_me_enabled"
74
+    FOLLOW_ME_ENABLED: "UI.follow_me_enabled",
75
+    /**
76
+     * Notifies that flipX property of the local video is changed.
77
+     */
78
+    LOCAL_FLIPX_CHANGED: "UI.local_flipx_changed"
75 79
 };

正在加载...
取消
保存