Lomiri
Infographics.qml
1 /*
2  * Copyright (C) 2013-2016 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 "Gradient.js" as Gradient
18 import QtQuick 2.12
19 import Lomiri.Components 1.3
20 
21 Item {
22  id: infographic
23 
24  property var model
25 
26  property int animDuration: 10
27 
28  property int currentWeekDay
29 
30  QtObject {
31  id: d
32  objectName: "infographicPrivate"
33  property bool useDotAnimation: true
34  property int circleModifier: useDotAnimation ? 1 : 2
35  property bool animating: dotHideAnimTimer.running
36  || dotShowAnimTimer.running
37  || circleChangeAnimTimer.running
38  }
39 
40  QtObject {
41  id: whiteTheme
42  property color main: "white"
43  property color start: "white"
44  property color end: "white"
45  }
46 
47  Connections {
48  target: model
49  ignoreUnknownSignals: model === undefined
50 
51  onDataAboutToAppear: startHideAnimation() // hide "no data" label
52  onDataAppeared: startShowAnimation()
53 
54  onDataAboutToChange: startHideAnimation()
55  onDataChanged: startShowAnimation()
56 
57  onDataAboutToDisappear: startHideAnimation()
58  onDataDisappeared: startShowAnimation() // show "no data" label
59  }
60 
61  LiveTimer {
62  frequency: LiveTimer.Hour
63  onTrigger: handleTimerTrigger()
64  }
65 
66  function handleTimerTrigger(){
67  var today = new Date().getDay()
68  if(infographic.currentWeekDay !== today){
69  infographic.currentWeekDay = today
70  reloadUserData();
71  }
72  }
73 
74  function reloadUserData(){
75  d.useDotAnimation = false
76  infographic.model.nextDataSource()
77  }
78 
79  function startShowAnimation() {
80  dotHideAnimTimer.stop()
81  notification.hideAnim.stop()
82 
83  if (d.useDotAnimation) {
84  dotShowAnimTimer.startFromBeginning()
85  }
86  notification.showAnim.start()
87  }
88 
89  function startHideAnimation() {
90  dotShowAnimTimer.stop()
91  circleChangeAnimTimer.stop()
92  notification.showAnim.stop()
93 
94  if (d.useDotAnimation) {
95  dotHideAnimTimer.startFromBeginning()
96  } else {
97  circleChangeAnimTimer.startFromBeginning()
98  }
99  notification.hideAnim.start()
100  }
101 
102  visible: model.username !== ""
103 
104  Component.onCompleted: {
105  currentWeekDay = new Date().getDay()
106  startShowAnimation()
107  }
108 
109  Item {
110  id: dataCircle
111  objectName: "dataCircle"
112 
113  property real divisor: 1.5
114 
115  width: Math.min(parent.height, parent.width) / divisor
116  height: width
117 
118  anchors.centerIn: parent
119 
120  Timer {
121  id: circleChangeAnimTimer
122 
123  property int pastCircleCounter
124  property int presentCircleCounter
125 
126  interval: notification.duration
127  running: false
128  repeat: true
129  onTriggered: {
130  if (pastCircleCounter < pastCircles.count) {
131  var nextCircle = pastCircles.itemAt(pastCircleCounter++)
132  if (nextCircle !== null) nextCircle.pastCircleChangeAnim.start()
133  }
134  if (pastCircleCounter > pastCircles.count / 2) {
135  var nextCircle = presentCircles.itemAt(presentCircleCounter++)
136  if (nextCircle !== null) nextCircle.presentCircleChangeAnim.start()
137  }
138  if (presentCircleCounter > infographic.model.currentDay && pastCircleCounter >= pastCircles.count) {
139  stop()
140  }
141  }
142 
143  function startFromBeginning() {
144  circleChangeAnimTimer.pastCircleCounter = 0
145  circleChangeAnimTimer.presentCircleCounter = 0
146  start()
147  }
148  }
149 
150  Repeater {
151  id: pastCircles
152  objectName: "pastCircles"
153  model: infographic.model.secondMonth
154 
155  delegate: ObjectPositioner {
156  property alias pastCircleChangeAnim: pastCircleChangeAnim
157 
158  index: model.index
159  count: pastCircles.count
160  radius: dataCircle.width / 2
161  halfSize: pastCircle.width / 2
162  posOffset: 0.0
163 
164  Circle {
165  id: pastCircle
166  objectName: "pastCircle" + index
167 
168  property real divisor: 1.8
169  property real circleOpacity: 0.1
170 
171  width: dataCircle.width / divisor
172  height: dataCircle.height / divisor
173  opacity: 0.0
174  circleScale: 0.0
175  visible: modelData !== undefined
176  color: "transparent"
177  centerCircle: dataCircle
178 
179  SequentialAnimation {
180  id: pastCircleChangeAnim
181 
182  loops: 1
183  ParallelAnimation {
184  PropertyAnimation {
185  target: pastCircle
186  property: "opacity"
187  to: pastCircle.circleOpacity
188  easing.type: Easing.OutCurve
189  duration: circleChangeAnimTimer.interval * d.circleModifier
190  }
191  PropertyAnimation {
192  target: pastCircle
193  property: "circleScale"
194  to: modelData
195  easing.type: Easing.OutCurve
196  duration: circleChangeAnimTimer.interval * d.circleModifier
197  }
198  ColorAnimation {
199  target: pastCircle
200  property: "color"
201  to: Gradient.threeColorByIndex(index, count, whiteTheme)
202  easing.type: Easing.OutCurve
203  duration: circleChangeAnimTimer.interval * d.circleModifier
204  }
205  }
206  }
207  }
208  }
209  }
210 
211  Repeater {
212  id: presentCircles
213  objectName: "presentCircles"
214  model: infographic.model.firstMonth
215 
216  delegate: ObjectPositioner {
217  property alias presentCircleChangeAnim: presentCircleChangeAnim
218 
219  index: model.index
220  count: presentCircles.count
221  radius: dataCircle.width / 2
222  halfSize: presentCircle.width / 2
223  posOffset: 0.0
224 
225  Circle {
226  id: presentCircle
227  objectName: "presentCircle" + index
228 
229  property real divisor: 1.8
230  property real circleOpacity: 0.3
231 
232  width: dataCircle.width / divisor
233  height: dataCircle.height / divisor
234  opacity: 0.0
235  circleScale: 0.0
236  visible: modelData !== undefined
237  color: "transparent"
238  centerCircle: dataCircle
239 
240  SequentialAnimation {
241  id: presentCircleChangeAnim
242 
243  loops: 1
244 
245  ParallelAnimation {
246  PropertyAnimation {
247  target: presentCircle
248  property: "opacity"
249  to: presentCircle.circleOpacity
250  easing.type: Easing.OutCurve
251  duration: circleChangeAnimTimer.interval * d.circleModifier
252  }
253  PropertyAnimation {
254  target: presentCircle
255  property: "circleScale"
256  to: modelData
257  easing.type: Easing.OutCurve
258  duration: circleChangeAnimTimer.interval * d.circleModifier
259  }
260  ColorAnimation {
261  target: presentCircle
262  property: "color"
263  to: Gradient.threeColorByIndex(index, infographic.model.currentDay, whiteTheme)
264  easing.type: Easing.OutCurve
265  duration: circleChangeAnimTimer.interval * d.circleModifier
266  }
267  }
268  }
269  }
270  }
271  }
272 
273  Timer {
274  id: dotShowAnimTimer
275 
276  property int dotCounter: 0
277 
278  interval: animDuration * 0.5; running: false; repeat: true
279  onTriggered: {
280  if (dotCounter < dots.count) {
281  var nextDot = dots.itemAt(dotCounter);
282  if (nextDot) {
283  nextDot.unlockAnimation.start();
284  if (++dotCounter == Math.round(dots.count / 2)) {
285  circleChangeAnimTimer.startFromBeginning();
286  }
287  }
288  } else {
289  stop()
290  }
291  }
292 
293  function startFromBeginning() {
294  if (!dotShowAnimTimer.running)
295  dotCounter = 0
296 
297  start()
298  }
299  }
300 
301  Timer {
302  id: dotHideAnimTimer
303 
304  property int dotCounter
305 
306  interval: animDuration * 0.5
307  running: false
308  repeat: true
309  onTriggered: {
310  if (dotCounter >= 0) {
311  var nextDot = dots.itemAt(dotCounter--)
312  nextDot.changeAnimation.start()
313  } else {
314  stop()
315  }
316  if (dotCounter == 0) {
317  infographic.model.readyForDataChange()
318  }
319  }
320 
321  function startFromBeginning() {
322  if (!dotHideAnimTimer.running)
323  dotCounter = dots.count - 1
324 
325  start()
326  }
327  }
328 
329  Repeater {
330  id: dots
331  objectName: "dots"
332 
333  model: infographic.model.firstMonth
334 
335  delegate: ObjectPositioner {
336  property alias unlockAnimation: dotUnlockAnim
337  property alias changeAnimation: dotChangeAnim
338 
339  property int currentDay: infographic.model.currentDay
340 
341  index: model.index
342  count: dots.count
343  radius: dataCircle.width / 2
344  halfSize: dot.width / 2
345  posOffset: radius / dot.width / 3
346  state: dot.state
347 
348  Dot {
349  id: dot
350  objectName: "dot" + index
351 
352  property real baseOpacity: 1
353 
354  width: units.dp(5) * parent.radius / 200
355  height: units.dp(5) * parent.radius / 200
356  opacity: 0.0
357  smooth: true
358  state: index < currentDay ? "filled" : index == currentDay ? "pointer" : "unfilled"
359 
360  PropertyAnimation {
361  id: dotUnlockAnim
362 
363  target: dot
364  property: "opacity"
365  to: dot.baseOpacity
366  duration: dotShowAnimTimer.interval
367  }
368 
369  PropertyAnimation {
370  id: dotChangeAnim
371 
372  target: dot
373  property: "opacity"
374  to: 0.0
375  duration: dotHideAnimTimer.interval
376  }
377  }
378  }
379  }
380 
381  Label {
382  id: notification
383  objectName: "label"
384 
385  property alias hideAnim: decreaseOpacity
386  property alias showAnim: increaseOpacity
387 
388  property real baseOpacity: 1
389  property real duration: dotShowAnimTimer.interval * 5
390 
391  height: 0.7 * dataCircle.width
392  width: notification.height
393  anchors.centerIn: parent
394 
395  text: infographic.model.label
396 
397  wrapMode: Text.WordWrap
398  horizontalAlignment: Text.AlignHCenter
399  verticalAlignment: Text.AlignVCenter
400  color: "white"
401 
402  PropertyAnimation {
403  id: increaseOpacity
404 
405  target: notification
406  property: "opacity"
407  from: 0.0
408  to: notification.baseOpacity
409  duration: notification.duration * dots.count
410  }
411 
412  PropertyAnimation {
413  id: decreaseOpacity
414 
415  target: notification
416  property: "opacity"
417  from: notification.baseOpacity
418  to: 0.0
419  duration: notification.duration * dots.count
420  onStopped: if (!d.useDotAnimation) infographic.model.readyForDataChange()
421  }
422  }
423  }
424 
425  MouseArea {
426  anchors.fill: dataCircle
427  enabled: notification.text != ""
428 
429  onDoubleClicked: {
430  if (!d.animating) {
431  reloadUserData()
432  }
433  }
434  }
435 }