on xenial help sign in
inline preview video
a9b5255Nick Dedekind
CMakeLists.txt+1
@@ -55,6 +55,7 @@ find_package(Qt5Gui 5.2 REQUIRED)
55find_package(Qt5DBus 5.2 REQUIRED)
55find_package(Qt5DBus 5.2 REQUIRED)
56find_package(Qt5Concurrent 5.2 REQUIRED)
56find_package(Qt5Concurrent 5.2 REQUIRED)
57find_package(Qt5Sql 5.2 REQUIRED)
57find_package(Qt5Sql 5.2 REQUIRED)
58+find_package(Qt5Multimedia 5.4 REQUIRED)
58
59
59# Standard install paths
60# Standard install paths
60include(GNUInstallDirs)
61include(GNUInstallDirs)
qml/Components/MediaServices/MediaServices.qmlnew file+332
@@ -0,0 +1,332 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtMultimedia 5.0
19+import Ubuntu.Components 1.3
20+
21+FocusScope {
22+ id: root
23+ property var sourceData
24+ property string context: ""
25+ property bool fullscreen: false
26+ property list<Action> actions
27+
28+ property alias header: headerContent.item
29+ property alias content: contentLoader.item
30+ property alias footer: footerContent.item
31+
32+ implicitHeight: {
33+ if (fullscreen && parent) {
34+ return parent.height;
35+ }
36+ console.log(contentLoader.height, fullscreen, parent)
37+ return contentLoader.height
38+ }
39+ height: implicitHeight
40+
41+ OrientationHelper {
42+ id: orientationHelper
43+ automaticOrientation: false
44+
45+ states: [
46+ State {
47+ name: "portrait"
48+ when: !rotationAction.checked
49+ },
50+ State {
51+ name: "landscape"
52+ when: rotationAction.checked
53+ PropertyChanges {
54+ target: orientationHelper
55+ orientationAngle: 90
56+ }
57+ }
58+ ]
59+
60+ Loader {
61+ id: contentLoader
62+
63+ sourceComponent: {
64+ switch (context) {
65+ case "video":
66+ return videoComponent;
67+ }
68+ return undefined;
69+ }
70+
71+ Component {
72+ id: videoComponent
73+ VideoPlayer {
74+ id: player
75+ width: orientationHelper.width
76+ height: fullscreen ? orientationHelper.height : implicitHeight
77+ fixedHeight: fullscreen
78+ orientation: orientationHelper.state == "portrait" ? Qt.PortraitOrientation : Qt.LandscapeOrientation
79+
80+ screenshot: {
81+ var screenshot = root.sourceData["screenshot"];
82+ if (screenshot) return screenshot;
83+
84+ var source = root.sourceData["source"];
85+ if (source) {
86+ if (source.toString().indexOf("file://") === 0) {
87+ return "image://thumbnailer/" + source.toString().substr(7);
88+ }
89+ }
90+ return "";
91+ }
92+
93+ mediaPlayer: footer ? footer.mediaPlayer : null
94+
95+ onClicked: {
96+ if (mediaPlayer.availability !== MediaPlayer.Available) return;
97+
98+ if (mediaPlayer.playbackState === MediaPlayer.StoppedState) {
99+ mediaPlayer.play();
100+ } else if (controlHideTimer.running) {
101+ if (mediaPlayer.playbackState === MediaPlayer.PlayingState) {
102+ mediaPlayer.pause();
103+ } else {
104+ mediaPlayer.play();
105+ }
106+ } else {
107+ controlHideTimer.restart();
108+ }
109+ }
110+ onPositionChanged: controlHideTimer.restart();
111+ }
112+ }
113+ }
114+
115+ Loader {
116+ id: headerContent
117+ anchors {
118+ top: parent.top
119+ left: parent.left
120+ right: parent.right
121+ topMargin: -units.gu(6)
122+ }
123+ height: units.gu(6)
124+ visible: false
125+
126+ // eater
127+ MouseArea {
128+ anchors.fill: parent
129+ }
130+
131+ sourceComponent: root.fullscreen ? headerComponent : undefined
132+
133+ Component {
134+ id: headerComponent
135+
136+ MediaServicesHeader {
137+ onGoPrevious: {
138+ rotationAction.checked = false;
139+ fullscreen = false;
140+ }
141+
142+ component: {
143+ switch (context) {
144+ case "video":
145+ break;
146+ }
147+ return undefined;
148+ }
149+ }
150+ }
151+
152+ // If we interact with the bar, reset the hide timer.
153+ MouseArea {
154+ z: 1
155+ anchors.fill: parent
156+ onPressed: {
157+ mouse.accepted = false
158+ if (controlHideTimer.running) controlHideTimer.restart()
159+ }
160+ }
161+ }
162+
163+ Loader {
164+ id: footerContent
165+ anchors {
166+ left: parent.left
167+ right: parent.right
168+ bottom: parent.bottom
169+ bottomMargin: -units.gu(7)
170+ }
171+ height: units.gu(7)
172+ visible: false
173+
174+ sourceComponent: {
175+ switch (context) {
176+ case "video":
177+ return videoControlsComponent;
178+ }
179+ return undefined;
180+ }
181+ // eater
182+ MouseArea {
183+ anchors.fill: parent
184+ }
185+
186+ Component {
187+ id: videoControlsComponent
188+ VideoPlayerControls {
189+ id: controls
190+
191+ viewAction: rotationAction.enabled ? rotationAction : fullscreenAction
192+ userActions: root.actions
193+
194+ mediaPlayer.source: {
195+ var x = root.sourceData["source"];
196+ if (x.toString().indexOf("video://") === 0) {
197+ return x.toString().substr(6);
198+ }
199+ return x;
200+ }
201+ mediaPlayer.onPlaybackStateChanged: {
202+ controlHideTimer.restart();
203+ }
204+
205+ Binding {
206+ target: controlStates
207+ property: "forceControlsShown"
208+
209+ value: (fullscreen && mediaPlayer.playbackState === MediaPlayer.StoppedState) ||
210+ mediaPlayer.playbackState === MediaPlayer.PausedState ||
211+ interacting
212+ }
213+
214+ onInteractingChanged: {
215+ controlHideTimer.restart();
216+ }
217+
218+ Binding {
219+ target: header
220+ property: "title"
221+ value: controls.mediaPlayer.metaData.title
222+ when: header != null
223+ }
224+ }
225+ }
226+
227+ // If we interact with the bar, reset the hide timer.
228+ MouseArea {
229+ z: 1
230+ anchors.fill: parent
231+ onPressed: {
232+ mouse.accepted = false
233+ if (controlHideTimer.running) controlHideTimer.restart()
234+ }
235+ }
236+ }
237+ }
238+
239+ StateGroup {
240+ id: controlStates
241+ property bool controlTimerActive: false
242+ property bool forceControlsShown: false
243+
244+ states: [
245+ State {
246+ name: "controlsShown"
247+ when: controlStates.forceControlsShown || controlStates.controlTimerActive
248+ PropertyChanges {
249+ target: footerContent
250+ anchors.bottomMargin: 0
251+ visible: true
252+ }
253+ PropertyChanges {
254+ target: headerContent
255+ anchors.topMargin: 0
256+ visible: true
257+ }
258+ }
259+ ]
260+
261+ transitions: [
262+ Transition {
263+ to: "controlsShown"
264+ SequentialAnimation {
265+ PropertyAction { target: root; property: "clip"; value: true }
266+ PropertyAction { property: "visible" }
267+ NumberAnimation {
268+ properties: "anchors.bottomMargin,anchors.topMargin"
269+ duration: UbuntuAnimation.FastDuration
270+ }
271+ PropertyAction { target: root; property: "clip"; value: false }
272+ }
273+ },
274+ Transition {
275+ from: "controlsShown"
276+ SequentialAnimation {
277+ PropertyAction { target: root; property: "clip"; value: true }
278+ NumberAnimation {
279+ properties: "anchors.bottomMargin,anchors.topMargin"
280+ duration: UbuntuAnimation.FastDuration
281+ }
282+ PropertyAction { property: "visible" }
283+ PropertyAction { target: root; property: "clip"; value: false }
284+ }
285+ }
286+ ]
287+ }
288+
289+ Timer {
290+ id: controlHideTimer
291+ interval: 4000
292+ running: false
293+
294+ onRunningChanged: {
295+ if (running) {
296+ controlStates.controlTimerActive = true;
297+ }
298+ }
299+ onTriggered: controlStates.controlTimerActive = false;
300+ }
301+
302+ Action {
303+ id: fullscreenAction
304+ enabled: rotationAction.enabled === false
305+ iconName: root.fullscreen ? "view-restore" : "view-fullscreen"
306+
307+ onTriggered: {
308+ root.fullscreen = !root.fullscreen
309+ if (root.fullscreen) {
310+ console.log("fullscreen: fullscreen")
311+ } else {
312+ console.log("fullscreen: windowed")
313+ }
314+ }
315+ }
316+
317+ Action {
318+ id: rotationAction
319+ enabled: root.fullscreen === true
320+ iconName: "orientation-lock"
321+
322+ property bool checked: false
323+ onTriggered: {
324+ checked = !checked;
325+ if (checked) {
326+ console.log("rotation: landscape")
327+ } else {
328+ console.log("rotation: portrait")
329+ }
330+ }
331+ }
332+}
qml/Components/MediaServices/MediaServicesControls.qmlnew file+182
@@ -0,0 +1,182 @@
1+import QtQuick 2.0
2+import QtQuick.Layouts 1.1
3+import QtMultimedia 5.4
4+import Ubuntu.Components 1.3
5+import Ubuntu.Components.ListItems 1.0 as ListItem
6+import Ubuntu.Components.Themes 1.0
7+import Ubuntu.Components.Popups 1.0
8+
9+Item {
10+ id: root
11+
12+ property alias component: loader.sourceComponent
13+
14+ signal actionClicked(string action)
15+ signal viewModeClicked
16+
17+ property list<Action> userActions
18+ property Action viewAction
19+
20+ Rectangle {
21+ anchors.fill: parent
22+ color: "#1B1B1B"
23+ opacity: 0.85
24+ }
25+
26+ RowLayout {
27+ anchors {
28+ left: parent.left
29+ right: parent.right
30+ }
31+ anchors.verticalCenter: parent.verticalCenter
32+ anchors.margins: units.gu(2)
33+ spacing: units.gu(2)
34+
35+ AbstractButton {
36+ id: actionButton
37+ Layout.preferredWidth: units.gu(3)
38+ Layout.preferredHeight: units.gu(3)
39+ Layout.alignment: Qt.AlignVCenter
40+ enabled: action && action.enabled
41+
42+ Action {
43+ id: popupAction
44+ iconName: "navigation-menu"
45+ onTriggered: userActionsPopup.createObject(root, { "anchors.bottom": root.top })
46+ }
47+
48+ action: {
49+ switch (userActions.length) {
50+ case 0:
51+ return null;
52+ case 1:
53+ return userActions[0];
54+ default:
55+ return popupAction;
56+ }
57+ }
58+
59+ Icon {
60+ anchors.fill: parent
61+ visible: actionButton.action && actionButton.action.iconSource !== "" || false
62+ source: actionButton.action ? actionButton.action.iconSource : ""
63+ color: "#F3F3E7"
64+ opacity: actionButton.action && actionButton.action.enabled ? 1.0 : 0.5
65+ }
66+ }
67+
68+ Loader {
69+ id: loader
70+ Layout.fillWidth: true
71+ Layout.preferredHeight: units.gu(3)
72+ }
73+
74+ AbstractButton {
75+ Layout.preferredWidth: units.gu(3)
76+ Layout.preferredHeight: units.gu(3)
77+ Layout.alignment: Qt.AlignVCenter
78+ enabled: viewAction.enabled
79+ action: viewAction
80+
81+ Icon {
82+ anchors.fill: parent
83+ visible: viewAction.iconSource !== ""
84+ source: viewAction.iconSource
85+ color: "#F3F3E7"
86+ opacity: viewAction.enabled ? 1.0 : 0.5
87+ }
88+ }
89+ }
90+
91+ Component {
92+ id: userActionsPopup
93+
94+ Rectangle {
95+ id: popup
96+ color: "#1B1B1B"
97+ width: userActionsColumn.width
98+ height: userActionsColumn.height
99+
100+ InverseMouseArea {
101+ id: eventGrabber
102+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
103+ anchors.fill: popup
104+ propagateComposedEvents: false
105+ onWheel: wheel.accepted = true
106+
107+ onPressed: popup.destroy()
108+ }
109+
110+ Column {
111+ id: userActionsColumn
112+ spacing: units.gu(1)
113+
114+ width: units.gu(31)
115+ onHeightChanged: console.log("height", height)
116+
117+ Repeater {
118+ id: actionRepeater
119+ model: userActions
120+ AbstractButton {
121+ action: modelData
122+
123+ onClicked: popup.destroy()
124+
125+ implicitHeight: units.gu(4) + bottomDividerLine.height
126+ width: parent ? parent.width : units.gu(31)
127+
128+ Rectangle {
129+ visible: parent.pressed
130+ anchors {
131+ left: parent.left
132+ right: parent.right
133+ top: parent.top
134+ }
135+ height: parent.height - bottomDividerLine.height
136+ opacity: 0.5
137+ }
138+
139+ Icon {
140+ id: actionIcon
141+ visible: "" !== action.iconSource
142+ source: action.iconSource
143+ color: "#F3F3E7"
144+ anchors {
145+ verticalCenter: parent.verticalCenter
146+ verticalCenterOffset: units.dp(-1)
147+ left: parent.left
148+ leftMargin: units.gu(2)
149+ }
150+ width: units.gu(2)
151+ height: units.gu(2)
152+ opacity: action.enabled ? 1.0 : 0.5
153+ }
154+
155+ Label {
156+ anchors {
157+ verticalCenter: parent.verticalCenter
158+ verticalCenterOffset: units.dp(-1)
159+ left: actionIcon.visible ? actionIcon.right : parent.left
160+ leftMargin: units.gu(2)
161+ right: parent.right
162+ }
163+ // In the tabs overflow panel there are no icons, and the font-size
164+ // is medium as opposed to the small font-size in the actions overflow panel.
165+ fontSize: actionIcon.visible ? "small" : "medium"
166+ elide: Text.ElideRight
167+ text: action.text
168+ color: "#F3F3E7"
169+ opacity: action.enabled ? 1.0 : 0.5
170+ }
171+
172+ ListItem.ThinDivider {
173+ id: bottomDividerLine
174+ anchors.bottom: parent.bottom
175+ visible: index !== actionRepeater.count - 1
176+ }
177+ }
178+ }
179+ }
180+ }
181+ }
182+}
qml/Components/MediaServices/MediaServicesHeader.qmlnew file+61
@@ -0,0 +1,61 @@
1+import QtQuick 2.3
2+import QtQuick.Layouts 1.1
3+import Ubuntu.Components 1.3
4+
5+Item {
6+ id: root
7+
8+ property alias title: _title.text
9+ property alias component: loader.sourceComponent
10+
11+ signal goPrevious
12+
13+ Rectangle {
14+ anchors.fill: parent
15+ color: "#1B1B1B"
16+ opacity: 0.85
17+ }
18+
19+ RowLayout {
20+ id: row
21+ anchors {
22+ left: parent.left
23+ right: parent.right
24+ }
25+ anchors.verticalCenter: parent.verticalCenter
26+ anchors.margins: units.gu(2)
27+ spacing: units.gu(2)
28+
29+ // eater
30+ AbstractButton {
31+ id: navigationButton
32+ Layout.preferredWidth: units.gu(3)
33+ Layout.preferredHeight: units.gu(3)
34+ Layout.alignment: Qt.AlignVCenter
35+
36+ Icon {
37+ anchors.fill: parent
38+ name: "go-previous"
39+ color: "#F3F3E7"
40+ }
41+
42+ onTriggered: {
43+ root.goPrevious();
44+ }
45+ }
46+
47+ Label {
48+ id: _title
49+ Layout.fillWidth: true
50+ Layout.alignment: Qt.AlignVCenter
51+ elide: Text.ElideRight
52+ color: "#F3F3E7"
53+ }
54+
55+ Loader {
56+ id: loader
57+ Layout.alignment: Qt.AlignVCenter
58+ Layout.preferredHeight: units.gu(3)
59+ }
60+ }
61+}
qml/Components/MediaServices/VideoPlayer.qmlnew file+228
@@ -0,0 +1,228 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtMultimedia 5.0
19+import Ubuntu.Components 0.1
20+import Ubuntu.Thumbnailer 0.1
21+import "../../Components"
22+
23+Item {
24+ id: root
25+
26+ property url screenshot: image.source
27+ property alias mediaPlayer: videoOutput.source
28+ property int orientation: Qt.PortraitOrientation
29+ property bool fixedHeight: false
30+
31+ implicitHeight: {
32+ if (parent && orientation == Qt.LandscapeOrientation) {
33+ return parent.height;
34+ }
35+ return content.height;
36+ }
37+
38+ signal clicked
39+ signal positionChanged
40+
41+ Rectangle {
42+ anchors.fill: root
43+// anchors.leftMargin: units.gu(2)
44+// anchors.rightMargin: units.gu(2)
45+ color: "#1B1B1B"
46+ opacity: 0.85
47+ visible: mediaPlayer.playbackState !== MediaPlayer.StoppedState
48+ }
49+
50+ Item {
51+ id: content
52+ anchors {
53+ left: parent.left
54+ right: parent.right
55+ verticalCenter: parent.verticalCenter
56+ }
57+ height: root.orientation == Qt.LandscapeOrientation || fixedHeight ? root.height : childrenRect.height;
58+
59+ LazyImage {
60+ id: image
61+ objectName: "screenshot"
62+ anchors {
63+ left: parent.left
64+ right: parent.right
65+ }
66+ anchors.verticalCenter: parent.verticalCenter
67+ scaleTo: "width"
68+ initialHeight: width * 10 / 16
69+ visible: mediaPlayer.playbackState === MediaPlayer.StoppedState
70+ }
71+
72+ VideoOutput {
73+ id: videoOutput
74+ anchors {
75+ left: parent.left
76+ right: parent.right
77+ }
78+ anchors.verticalCenter: parent.verticalCenter
79+ height: {
80+ if (root.orientation == Qt.LandscapeOrientation || fixedHeight) {
81+ return root.height;
82+ }
83+ return (mediaPlayer.metaData.resolution.height / mediaPlayer.metaData.resolution.width ) * width;
84+ }
85+
86+ source: mediaPlayer
87+ visible: mediaPlayer.playbackState !== MediaPlayer.StoppedState
88+ }
89+
90+// Loader {
91+// id: shaderLoader
92+// anchors.fill: videoOutput
93+// sourceComponent: fixedHeight ? shaderComponent : undefined
94+
95+// Component {
96+// id: shaderComponent
97+
98+// Item {
99+// ShaderEffect {
100+// id: leftVideo
101+// property variant source: ShaderEffectSource {
102+// sourceItem: videoOutput
103+// hideSource: true
104+// sourceRect: Qt.rect(0, 0, units.gu(2), videoOutput.height)
105+// }
106+// anchors {
107+// top: parent.top
108+// bottom: parent.bottom
109+// left: parent.left
110+// }
111+// width: units.gu(2)
112+// visible: mediaPlayer.playbackState !== MediaPlayer.StoppedState
113+
114+// property real itemOpacity: 0.5
115+
116+// fragmentShader: "
117+// varying highp vec2 qt_TexCoord0;
118+// uniform sampler2D source;
119+// uniform lowp float itemOpacity;
120+// void main(void)
121+// {
122+// highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
123+// sourceColor *= itemOpacity;
124+// gl_FragColor = sourceColor;
125+// }
126+// "
127+// }
128+
129+// ShaderEffect {
130+// id: middleVideo
131+// property variant source: ShaderEffectSource {
132+// sourceItem: videoOutput
133+// hideSource: true
134+// sourceRect: Qt.rect(units.gu(2), 0, videoOutput.width - units.gu(4), videoOutput.height)
135+// }
136+// anchors {
137+// top: parent.top
138+// bottom: parent.bottom
139+// left: leftVideo.right
140+// right: rightVideo.left
141+// }
142+// visible: mediaPlayer.playbackState !== MediaPlayer.StoppedState
143+
144+// property real itemOpacity: 1
145+
146+// fragmentShader: "
147+// varying highp vec2 qt_TexCoord0;
148+// uniform sampler2D source;
149+// uniform lowp float itemOpacity;
150+// void main(void)
151+// {
152+// highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
153+// sourceColor *= itemOpacity;
154+// gl_FragColor = sourceColor;
155+// }
156+// "
157+// }
158+
159+// ShaderEffect {
160+// id: rightVideo
161+// property variant source: ShaderEffectSource {
162+// sourceItem: videoOutput
163+// hideSource: true
164+// sourceRect: Qt.rect(videoOutput.width - units.gu(2), 0, units.gu(2), videoOutput.height)
165+// }
166+// anchors {
167+// top: parent.top
168+// bottom: parent.bottom
169+// right: parent.right
170+// }
171+// width: units.gu(2)
172+// visible: mediaPlayer.playbackState !== MediaPlayer.StoppedState
173+
174+// property real itemOpacity: 0.5
175+
176+// fragmentShader: "
177+// varying highp vec2 qt_TexCoord0;
178+// uniform sampler2D source;
179+// uniform lowp float itemOpacity;
180+// void main(void)
181+// {
182+// highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
183+// sourceColor *= itemOpacity;
184+// gl_FragColor = sourceColor;
185+// }
186+// "
187+// }
188+// }
189+// }
190+// }
191+ }
192+
193+ Rectangle {
194+ id: playButton
195+ readonly property bool bigButton: parent.width > units.gu(40)
196+ anchors.centerIn: content
197+ width: bigButton ? units.gu(10) : units.gu(8)
198+ height: width
199+ visible: mediaPlayer.playbackState !== MediaPlayer.PlayingState
200+ color: "#1B1B1B"
201+ opacity: 0.85
202+ radius: width/2
203+
204+ Behavior on width { UbuntuNumberAnimation {} }
205+
206+ Icon {
207+ name: "media-playback-start"
208+ anchors.fill: parent
209+ anchors.margins: units.gu(1)
210+ color: "#F3F3E7"
211+ }
212+ }
213+
214+ ActivityIndicator {
215+ anchors.centerIn: content
216+ running: mediaPlayer.status === MediaPlayer.Stalled ||
217+ (mediaPlayer.playbackState === MediaPlayer.PlayingState && (mediaPlayer.status === MediaPlayer.Loading || mediaPlayer.status === MediaPlayer.Loaded))
218+ }
219+
220+ MouseArea {
221+ id: contentMouseArea
222+ anchors.fill: content
223+ hoverEnabled: mediaPlayer.playbackState !== MediaPlayer.StoppedState
224+
225+ onClicked: root.clicked()
226+ onPositionChanged: root.positionChanged()
227+ }
228+}
qml/Components/MediaServices/VideoPlayerControls.qmlnew file+161
@@ -0,0 +1,161 @@
1+import QtQuick 2.0
2+import QtQuick.Layouts 1.1
3+import QtMultimedia 5.0
4+import Ubuntu.Components 1.3
5+import Ubuntu.Components.Themes 1.0
6+
7+MediaServicesControls {
8+ id: root
9+ property alias mediaPlayer: _mediaPlayer
10+ property bool interacting: false
11+
12+ QtObject {
13+ id: priv
14+
15+ function formatTime(position) {
16+ var pos = (position/1000).toFixed(0)
17+
18+ var m = Math.floor(pos/60);
19+ var ss = pos % 60;
20+ if (ss >= 10) {
21+ return m + ":" + ss;
22+ } else {
23+ return m + ":0" + ss;
24+ }
25+ }
26+ }
27+
28+ component: Item {
29+ Connections {
30+ target: mediaPlayer
31+ onPositionChanged: {
32+ if (slider.valueGuard) return;
33+
34+ slider.valueGuard = true;
35+ slider.value = mediaPlayer.position;
36+ slider.valueGuard = false;
37+ if (!slider.pressed) {
38+ positionLabel.text = priv.formatTime(_mediaPlayer.position);
39+ }
40+ }
41+ }
42+
43+ Binding {
44+ target: root
45+ property: "interacting"
46+ value: slider.pressed
47+ }
48+
49+ Label {
50+ id: positionLabel
51+ anchors {
52+ left: parent.left
53+ bottom: parent.bottom
54+ bottomMargin: -units.dp(3)
55+ }
56+ verticalAlignment: Text.AlignBottom
57+ fontSize: "x-small"
58+ color: "#F3F3E7"
59+
60+ text: priv.formatTime(_mediaPlayer.position)
61+ }
62+
63+ Slider {
64+ id: slider
65+ property bool valueGuard: false
66+
67+ anchors {
68+ left: parent.left
69+ right: parent.right
70+ }
71+ height: units.gu(2)
72+ live: true
73+ enabled: mediaPlayer.seekable && mediaPlayer.duration > 0
74+ minimumValue: 0
75+ maximumValue: mediaPlayer.duration > 0 ? mediaPlayer.duration : 1
76+ value: mediaPlayer.position
77+ theme: ThemeSettings {
78+ palette: Palette {
79+ normal.base: "#F3F3E7"
80+ }
81+ }
82+
83+ onValueChanged: {
84+ if (!pressed) return;
85+ if (slider.valueGuard) return;
86+
87+ slider.valueGuard = true;
88+ mediaPlayer.seek(value);
89+ slider.valueGuard = false;
90+ }
91+
92+ property bool wasPlaying: mediaPlayer.playbackState === MediaPlayer.PlayingState
93+ onPressedChanged: {
94+ if (pressed) {
95+ wasPlaying = mediaPlayer.playbackState === MediaPlayer.PlayingState
96+ mediaPlayer.pause();
97+ } else {
98+
99+ positionLabel.text = priv.formatTime(_mediaPlayer.position);
100+ if (wasPlaying) {
101+ mediaPlayer.play();
102+ }
103+ }
104+ }
105+
106+ function formatValue(value) {
107+ return priv.formatTime(value);
108+ }
109+ }
110+
111+ Label {
112+ anchors {
113+ right: parent.right
114+ bottom: parent.bottom
115+ bottomMargin: -units.dp(3)
116+ }
117+ verticalAlignment: Text.AlignBottom
118+ fontSize: "x-small"
119+ color: "#F3F3E7"
120+
121+ text: {
122+ var pos = (mediaPlayer.duration/1000).toFixed(0)
123+
124+ var m = Math.floor(pos/60);
125+ var ss = pos % 60;
126+ if (ss >= 10) {
127+ return m + ":" + ss;
128+ } else {
129+ return m + ":0" + ss;
130+ }
131+ }
132+ }
133+ }
134+
135+ function getPlaybackState(playbackState) {
136+ if (playbackState === MediaPlayer.PlayingState) return "PlayingState";
137+ else if (playbackState === MediaPlayer.PausedState) return "PausedState";
138+ else if (playbackState === MediaPlayer.StoppedState) return "StoppedState";
139+ return "";
140+ }
141+
142+ function getPlayerStatus(status) {
143+ if (status === MediaPlayer.NoMedia) return "NoMedia";
144+ else if (status === MediaPlayer.Loading) return "Loading";
145+ else if (status === MediaPlayer.Loaded) return "Loaded";
146+ else if (status === MediaPlayer.Buffering) return "Buffering";
147+ else if (status === MediaPlayer.Stalled) return "Stalled";
148+ else if (status === MediaPlayer.Buffered) return "Buffered";
149+ else if (status === MediaPlayer.EndOfMedia) return "EndOfMedia";
150+ else if (status === MediaPlayer.InvalidMedia) return "InvalidMedia";
151+ else if (status === MediaPlayer.UnknownStatus) return "UnknownStatus";
152+ return "";
153+ }
154+
155+ MediaPlayer {
156+ id: _mediaPlayer
157+
158+ onPlaybackStateChanged: console.log("PLAYBACK STATE", getPlaybackState(playbackState));
159+ onStatusChanged: console.log("PLAYER STATUS", getPlayerStatus(status));
160+ }
161+}
qml/Dash/DashApplication.qml+13−1
@@ -18,13 +18,25 @@ import QtQuick 2.2
18import Ubuntu.Components 1.0
18import Ubuntu.Components 1.0
19import Ubuntu.Thumbnailer 0.1 // Register support for image://thumbnailer/ and image://albumart/
19import Ubuntu.Thumbnailer 0.1 // Register support for image://thumbnailer/ and image://albumart/
20
20
21−MainView {
21+ MainView {
22+ id: mainView
22 width: applicationArguments.hasGeometry() ? applicationArguments.width() : units.gu(40)
23 width: applicationArguments.hasGeometry() ? applicationArguments.width() : units.gu(40)
23 height: applicationArguments.hasGeometry() ? applicationArguments.height() : units.gu(68)
24 height: applicationArguments.hasGeometry() ? applicationArguments.height() : units.gu(68)
24
25
25 useDeprecatedToolbar: false
26 useDeprecatedToolbar: false
26
27
28+ function pushPage(page) { pageStack.push(page); }
29+ function popPage(page) { pageStack.pop(); }
30+
31+ PageStack {
32+ id: pageStack
33+ anchors.fill: parent
34+ }
35+
27 Dash {
36 Dash {
37+ id: dash
28 anchors.fill: parent
38 anchors.fill: parent
39+
40+ Component.onCompleted: pushPage(dash)
29 }
41 }
30}
42}
qml/Dash/Previews/PreviewInlineVideo.qmlnew file+104
@@ -0,0 +1,104 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtMultimedia 5.0
19+import Ubuntu.Components 0.1
20+import Ubuntu.Thumbnailer 0.1
21+import Ubuntu.Content 0.1
22+import "../../Components/MediaServices"
23+
24+/*! \brief Preview widget for video.
25+
26+ This widget shows video contained in widgetData["source"],
27+ with a placeholder screenshot specified by widgetData["screenshot"].
28+ */
29+
30+PreviewWidget {
31+ id: root
32+ implicitWidth: units.gu(35)
33+ implicitHeight: services.height
34+
35+ property Item rootItem: QuickUtils.rootItem(root)
36+
37+ MediaServices {
38+ id: services
39+ width: parent.width
40+
41+ context: "video"
42+ sourceData: widgetData
43+ fullscreen: false
44+
45+ actions: [
46+ Action {
47+ text: i18n.tr("Share")
48+ iconSource: "image://theme/share"
49+ onTriggered: {
50+ console.log("ACTION Share")
51+ sharePicker.visible = true
52+ }
53+ }
54+ ]
55+
56+ states: [
57+ State {
58+ name: "fullscreen"
59+ when: services.fullscreen
60+ ParentChange { target: services; parent: rootItem; x: 0; y: 0; width: rootItem.width; height: rootItem.height}
61+ }
62+ ]
63+
64+ transitions: Transition {
65+ ParentAnimation {
66+ UbuntuNumberAnimation { properties: "x,y,width,height" }
67+ }
68+ }
69+ }
70+
71+ Component {
72+ id: contentItemComp
73+ ContentItem {
74+ url: widgetData["source"]
75+ }
76+ }
77+
78+ ContentPeerPicker {
79+ id: sharePicker
80+ objectName: "sharePickerEvents"
81+ anchors.fill: parent
82+ showTitle: false
83+ visible: false
84+ parent: rootItem
85+ z: 100
86+
87+ contentType: ContentType.Videos
88+ handler: ContentHandler.Share
89+
90+ onPeerSelected: {
91+ visible = false;
92+
93+ var curTransfer = peer.request();
94+ if (curTransfer.state === ContentTransfer.InProgress)
95+ {
96+ var medias = [ contentItemComp.createObject(parent) ]
97+ curTransfer.state = ContentTransfer.Charged;
98+ }
99+ }
100+ onCancelPressed: {
101+ visible = false;
102+ }
103+ }
104+}
qml/Dash/Previews/PreviewWidgetFactory.qml+1−1
@@ -60,7 +60,7 @@ Loader {
60 case "reviews": return "PreviewRatingDisplay.qml";
60 case "reviews": return "PreviewRatingDisplay.qml";
61 case "table": return "PreviewTable.qml";
61 case "table": return "PreviewTable.qml";
62 case "text": return "PreviewTextSummary.qml";
62 case "text": return "PreviewTextSummary.qml";
63− case "video": return "PreviewVideoPlayback.qml";
63+ case "video": return "PreviewInlineVideo.qml";
64 default: return "";
64 default: return "";
65 }
65 }
66 }
66 }
tests/qmltests/CMakeLists.txt+1
@@ -46,6 +46,7 @@ add_qml_test(Dash/Previews PreviewAudioPlayback)
46add_qml_test(Dash/Previews PreviewExpandable)
46add_qml_test(Dash/Previews PreviewExpandable)
47add_qml_test(Dash/Previews PreviewHeader)
47add_qml_test(Dash/Previews PreviewHeader)
48add_qml_test(Dash/Previews PreviewImageGallery)
48add_qml_test(Dash/Previews PreviewImageGallery)
49+add_qml_test(Dash/Previews PreviewInlineVideo IMPORT_PATHS ${CMAKE_BINARY_DIR}/tests/utils/modules ${CMAKE_BINARY_DIR}/plugins)
49add_qml_test(Dash/Previews PreviewPayments)
50add_qml_test(Dash/Previews PreviewPayments)
50add_qml_test(Dash/Previews PreviewProgress)
51add_qml_test(Dash/Previews PreviewProgress)
51add_qml_test(Dash/Previews PreviewRatingDisplay)
52add_qml_test(Dash/Previews PreviewRatingDisplay)
tests/qmltests/Dash/Previews/tst_PreviewInlineVideo.qmlnew file+67
@@ -0,0 +1,67 @@
1+/*
2+ * Copyright 2014 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtTest 1.0
19+import "../../../../qml/Dash/Previews"
20+import Unity.Test 0.1 as UT
21+import Ubuntu.Components 1.3
22+
23+Rectangle {
24+ id: mainView
25+ width: units.gu(40)
26+ height: units.gu(80)
27+ color: "lightgrey"
28+
29+ property var widgetData0: {
30+ "source": "video:///home/nick/Videos/test-mpeg.ogv",
31+ "screenshot": "/home/nick/Pictures/malta.jpg"
32+// "screenshot": "image://thumbnailer/https://dl.dropboxusercontent.com/u/85539674/team11.ogg"
33+ }
34+
35+ property var widgetData1: {
36+// "source": "https://dl.dropboxusercontent.com/u/85539674/test-mpeg.ogv",
37+// "screenshot": "/home/nick/Pictures/malta.jpg"
38+ }
39+
40+ function pushPage(page) { pageStack.push(page); }
41+ function popPage(page) { pageStack.pop(); }
42+
43+ PageStack {
44+ id: pageStack
45+ anchors.fill: parent
46+ anchors.margins: units.gu(5)
47+
48+ PreviewInlineVideo {
49+ id: videoPlayback
50+ width: parent.width
51+ widgetData: widgetData0
52+ }
53+ }
54+
55+ UT.UnityTestCase {
56+ name: "PreviewInlineVideoTest"
57+ when: windowShown
58+
59+ function test_loadScreenshot() {
60+ var screenshot = findChild(videoPlayback, "screenshot");
61+
62+ videoPlayback.widgetData = widgetData1;
63+ var screenshotSource = screenshot.source
64+ verify(screenshotSource.toString().indexOf("image://thumbnailer/") === 0)
65+ }
66+ }
67+}