Merge lp:~nik90/ubuntu-clock-app/world-clock-part1 into lp:ubuntu-clock-app/saucy

Proposed by Nekhelesh Ramananthan
Status: Merged
Approved by: Nekhelesh Ramananthan
Approved revision: 108
Merged at revision: 103
Proposed branch: lp:~nik90/ubuntu-clock-app/world-clock-part1
Merge into: lp:ubuntu-clock-app/saucy
Prerequisite: lp:~nik90/ubuntu-clock-app/initial-visual-design
Diff against target: 607 lines (+506/-59)
4 files modified
clock/ClockPage.qml (+220/-58)
clock/EasterEgg.qml (+1/-1)
clock/WorldClock.qml (+107/-0)
clock/WorldClockModel.qml (+178/-0)
To merge this branch: bzr merge lp:~nik90/ubuntu-clock-app/world-clock-part1
Reviewer Review Type Date Requested Status
Ubuntu Phone Apps Jenkins Bot continuous-integration Approve
Nekhelesh Ramananthan Pending
Review via email: mp+168537@code.launchpad.net

Commit message

Adds the first phase of the world clock's feature. It currently implements the following,
- Necessary UI to show the world clocks
- Search world clocks online using geoname.org API
- World Time is updated every minute locally using existing timer
- Add world clock local storage. Stores world clocks selected by the user
- Clicking on world clock sets it as the current location.
- Add Clock toolbar action

Description of the change

This MP achieves the following,
- Necessary UI to show the world clocks
- Search world clocks online using geoname.org API
- World Time is updated every minute locally using existing timer
- Add world clock local storage. Stores world clocks selected by the user
- Clicking on world clock sets it as the current location.

To post a comment you must log in.
Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'clock/ClockPage.qml'
2--- clock/ClockPage.qml 2013-06-09 10:23:09 +0000
3+++ clock/ClockPage.qml 2013-06-14 19:10:32 +0000
4@@ -20,75 +20,237 @@
5
6 import QtQuick 2.0
7 import Ubuntu.Components 0.1
8+import Ubuntu.Components.ListItems 0.1 as ListItem
9+import QtQuick.XmlListModel 2.0
10 import "../common/ClockUtils.js" as Utils
11 import "../common/Constants.js" as Constants
12-
13-Page {
14+import "../common"
15+
16+Page {
17+ id: clockPage;
18+
19 // Property to hold the formatted time string to show on the screen
20 property string currentTimeFormatted
21+ property real currentTimeStamp
22+ property real diff: new Date().getTimezoneOffset()
23
24 Component.onCompleted: {
25 Utils.log("ClockPage loaded");
26- currentTimeFormatted = Qt.formatTime(new Date());
27+ updateTime();
28 }
29
30 function onTimerUpdate(now) {
31- currentTimeFormatted = Qt.formatTime(now);
32+ currentTimeFormatted = Qt.formatTime(new Date(currentTimeStamp + (diff * 60000)), "hh:mm")
33+ now.setMinutes(now.getMinutes() + diff)
34 clockFace.timeChanged(now)
35- }
36-
37- // Label to show the current time
38- Label {
39- id: currentTimeLabel
40- objectName: "currentTimeLabel"
41-
42- z: clockFace.z + 1;
43- width: parent.width; height: parent.height / 4
44- anchors.centerIn: clockFace
45- horizontalAlignment: Text.AlignHCenter
46- verticalAlignment: Text.AlignVCenter
47- fontSize: "x-large"
48- color: Constants.brightWhite;
49-
50- text: currentTimeFormatted
51- }
52-
53- // Qml Element to draw the analogue clock face along with its hour, minute and second hands.
54- AnalogClockFace {
55- id: clockFace
56-
57- anchors.centerIn: parent
58- Component.onCompleted: easterEggCircle.reloadSunRiseModel()
59-
60- onClicked: {
61- if (easterEggCircle.isReady == 1) {
62- clockFace.state == "" ? clockFace.state = "easteregg" : clockFace.state = "";
63- } else {
64- Utils.log("Sunrise/Sunset times not yet loaded.")
65- }
66- }
67-
68- EasterEgg {
69- id: easterEggCircle
70- anchors.centerIn: parent
71+ if (now.getUTCSeconds() == 0) {
72+ updateTime();
73+ }
74+ }
75+
76+ function updateTime() {
77+ var now = new Date();
78+ currentTimeStamp = now.getTime();
79+ }
80+
81+ function getTimeDifference(data) {
82+ var worldTime = data.split(' ')[1].split(':'),
83+ hoursWT = parseInt(worldTime[0]),
84+ minutesWT = parseInt(worldTime[1]),
85+ now = new Date();
86+ return ((hoursWT - now.getHours()) * 60 + (minutesWT - now.getMinutes()));
87+ }
88+
89+ WorldClockModel {
90+ id: worldModel;
91+
92+ Component.onCompleted: {
93+ _loadDB();
94+ diff = worldModel.timeDiff;
95+ currentLocation.text = worldModel.city;
96+ currentTimeFormatted = Qt.formatTime(new Date(currentTimeStamp + (diff * 60000)), "hh:mm")
97+ }
98+ }
99+
100+ AnimationContainer {
101+ id: clockAnimationContainer
102+
103+ initYPos: units.gu(0);
104+ finalYPos: -listWorldClocks.height;
105+
106+ anchors.fill: parent
107+
108+ // Label to show the current time
109+ Rectangle {
110+ id: labelContainer;
111+
112+ z: clockFace.z + 1;
113 width: clockFace.innerDimension; height: width;
114- radius: width / 2;
115- }
116-
117- states: [
118- State {
119- name: "easteregg"
120- PropertyChanges { target: easterEggCircle; opacity: 1; }
121- PropertyChanges { target: currentTimeLabel; opacity: 0; }
122- // TODO: Do some caching of data here. Store data locally to reduce API calls.
123- PropertyChanges { target: easterEggCircle; sunriseLabel: easterEggCircle.getSunTime(easterEggCircle.model.get(0).sunrise); }
124- PropertyChanges { target: easterEggCircle; sunsetLabel: easterEggCircle.getSunTime(easterEggCircle.model.get(0).sunset); }
125- },
126- State {
127- name: ""
128- PropertyChanges { target: currentTimeLabel; opacity: 1; }
129- PropertyChanges { target: easterEggCircle; opacity: 0; }
130- }
131- ]
132+ anchors.centerIn: clockFace;
133+ color: "transparent"
134+
135+ Label {
136+ id: currentTimeLabel
137+ objectName: "currentTimeLabel"
138+
139+ anchors.centerIn: parent
140+ horizontalAlignment: Text.AlignHCenter
141+ verticalAlignment: Text.AlignVCenter
142+ fontSize: "x-large"
143+ color: Constants.brightWhite;
144+
145+ text: currentTimeFormatted
146+ }
147+ }
148+
149+ // Qml Element to draw the analogue clock face along with its hour, minute and second hands.
150+ AnalogClockFace {
151+ id: clockFace
152+
153+ anchors { top: parent.top; topMargin: units.gu(17); horizontalCenter: parent.horizontalCenter }
154+ Component.onCompleted: easterEggCircle.reloadSunRiseModel()
155+
156+ onClicked: {
157+ if (easterEggCircle.isReady == XmlListModel.Ready) {
158+ clockFace.state == "" ? clockFace.state = "easteregg" : clockFace.state = "";
159+ } else {
160+ Utils.log("Sunrise/Sunset times not yet loaded.")
161+ }
162+ }
163+
164+ EasterEgg {
165+ id: easterEggCircle
166+ anchors.centerIn: parent
167+ width: clockFace.innerDimension; height: width;
168+ radius: width / 2;
169+ }
170+
171+ states: [
172+ State {
173+ name: "easteregg"
174+ PropertyChanges { target: easterEggCircle; opacity: 1; }
175+ PropertyChanges { target: currentTimeLabel; opacity: 0; }
176+ // TODO: Do some caching of data here. Store data locally to reduce API calls.
177+ PropertyChanges { target: easterEggCircle; sunriseLabel: easterEggCircle.getSunTime(easterEggCircle.model.get(0).sunrise); }
178+ PropertyChanges { target: easterEggCircle; sunsetLabel: easterEggCircle.getSunTime(easterEggCircle.model.get(0).sunset); }
179+ },
180+ State {
181+ name: ""
182+ PropertyChanges { target: currentTimeLabel; opacity: 1; }
183+ PropertyChanges { target: easterEggCircle; opacity: 0; }
184+ }
185+ ]
186+ }
187+
188+ Column {
189+ id: savedWorldClock
190+
191+ height: childrenRect.height;
192+ anchors { left:parent.left; right:parent.right; top: clockFace.bottom; topMargin: units.gu(12)}
193+
194+ ListItem.ThinDivider {}
195+
196+ ListItem.Header { text: i18n.tr("Current Location") }
197+
198+ ListItem.SingleValue {
199+ id: currentLocation
200+ text: "UTC"
201+ value: Qt.formatTime(new Date(currentTimeStamp + (diff * 60000)), "hh:mm A");
202+ }
203+
204+ ListItem.Header { text: i18n.tr("World") }
205+
206+ ListView {
207+ id: listWorldClocks
208+
209+ anchors { left: parent.left; right: parent.right }
210+ height: units.gu(30)
211+ model: worldModel
212+ currentIndex: -1
213+
214+ delegate: ListItem.Standard {
215+ text: cityName;
216+
217+ Label {
218+ fontSize: "medium"
219+ text: Qt.formatTime(new Date(currentTimeStamp + (rawOffSet * 60000)), "hh:mm A");
220+ anchors { verticalCenter: parent.verticalCenter; right: parent.right; rightMargin: units.gu(2) }
221+ }
222+
223+ selected: listWorldClocks.currentIndex == index
224+
225+ onClicked: {
226+ currentLocation.text = cityName;
227+ diff = rawOffSet;
228+ worldModel.appendCurrentLocation(cityName, diff);
229+ }
230+ }
231+ }
232+ }
233+ }
234+
235+ WorldClock {
236+ id: worldClockList;
237+
238+ anchors { left: parent.left; right: parent.right; top: parent.top; topMargin: units.gu(2) }
239+
240+ // Xml model to retrieve timezone of searched city
241+ XmlListModel {
242+ id: cityDetailsModel;
243+
244+ source: worldClockList.getCityTimezoneUrl(worldClockList.lat, worldClockList.lng)
245+ query: "/geonames/timezone"
246+
247+ onStatusChanged: {
248+ if(status == XmlListModel.Ready && worldClockList.city != "null") {
249+ worldModel.appendPreset(worldClockList.city, getTimeDifference(cityDetailsModel.get(0).time));
250+ }
251+ }
252+
253+ XmlRole { name: "time"; query: "time/string()" }
254+ }
255+
256+ onClicked: {
257+ lat = searchModel.get(index).lat;
258+ lng = searchModel.get(index).lng;
259+ city = searchModel.get(index).city;
260+ cityDetailsModel.reload()
261+ clockPage.state = "NORMAL"
262+ }
263+ }
264+
265+ states: [
266+ State {
267+ name: "ADDCITY"
268+ PropertyChanges { target: worldClockList; visible: true }
269+ PropertyChanges { target: clockAnimationContainer; visible: false }
270+ PropertyChanges { target: addCity; visible: false }
271+ },
272+
273+ State {
274+ name: "NORMAL"
275+ PropertyChanges { target: worldClockList; visible: false }
276+ PropertyChanges { target: clockAnimationContainer; visible: true }
277+ PropertyChanges { target: addCity; visible: true }
278+ }
279+ ]
280+
281+ tools: ToolbarActions {
282+ id: toolbarClock
283+
284+ Action {
285+ id: addCity
286+
287+ iconSource: Qt.resolvedUrl("../images/add_icon.png")
288+ text: i18n.tr("Add City")
289+ visible: true;
290+
291+ onTriggered: clockPage.state = "ADDCITY";
292+ }
293+
294+ back {
295+ visible: true;
296+ onTriggered: clockPage.state = "NORMAL";
297+ }
298 }
299 }
300
301=== modified file 'clock/EasterEgg.qml'
302--- clock/EasterEgg.qml 2013-06-09 10:23:09 +0000
303+++ clock/EasterEgg.qml 2013-06-14 19:10:32 +0000
304@@ -52,7 +52,7 @@
305 function getUrlString (latitude, longitude) {
306 var base_url,url;
307 base_url = "http://api.openweathermap.org/data/2.5/weather?";
308- url = base_url + "lat=" + latitude + "&lon=" + longitude + "&mode=xml";
309+ url = base_url + "lat=" + latitude + "&lon=" + longitude + "&mode=xml";
310 return url;
311 }
312
313
314=== added file 'clock/WorldClock.qml'
315--- clock/WorldClock.qml 1970-01-01 00:00:00 +0000
316+++ clock/WorldClock.qml 2013-06-14 19:10:32 +0000
317@@ -0,0 +1,107 @@
318+import QtQuick 2.0
319+import Ubuntu.Components 0.1
320+import Ubuntu.Components.ListItems 0.1 as ListItem
321+import QtQuick.XmlListModel 2.0
322+import "../common/ClockUtils.js" as Utils
323+import "../common/Constants.js" as Constants
324+
325+Column {
326+ id: worldClocks
327+
328+ // Properties to store the user searched city details
329+ property string city: "null";
330+ property real lng: 0;
331+ property real lat: 0;
332+ property real rawOffSet: 0;
333+
334+ // Property to store the city searched by the user
335+ property string search_string: "london";
336+
337+ // Properties to store the information related to api.geonames.org
338+ readonly property string username: "krnekhelesh";
339+ readonly property string search_base_url: "http://api.geonames.org/search?q=";
340+ readonly property string timezone_base_url: "http://api.geonames.org/timezone?lat=";
341+ readonly property int no_of_results: 5;
342+
343+ property alias searchModel: searchCityModel;
344+ property alias index: worldList.currentIndex;
345+
346+ signal clicked()
347+
348+ function searchCityUrl (city) {
349+ return (search_base_url + search_string + "&maxRows="+ no_of_results + "&username=" + username);
350+ }
351+
352+ function getCityTimezoneUrl (lat, lng) {
353+ return (timezone_base_url + lat + "&lng=" + lng + "&username=" + username);
354+ }
355+
356+ height: childrenRect.height;
357+ visible: false;
358+
359+ // Xml model to search city online and retrieve latitude and longitude of searched city
360+ XmlListModel {
361+ id: searchCityModel;
362+
363+ source: searchCityUrl(city)
364+ query: "/geonames/geoname"
365+
366+ XmlRole { name: "city"; query: "toponymName/string()"; isKey: true }
367+ XmlRole { name: "lat"; query: "lat/string()"; isKey: true }
368+ XmlRole { name: "lng"; query: "lng/string()"; isKey: true }
369+ }
370+
371+ Row {
372+ id: searchBox;
373+
374+ spacing: units.gu(1)
375+ anchors.horizontalCenter: parent.horizontalCenter;
376+ height: childrenRect.height;
377+
378+ TextField {
379+ id: searchLabel
380+
381+ hasClearButton: true
382+ placeholderText: i18n.tr("Search");
383+ }
384+
385+ Button {
386+ id: searchButton;
387+
388+ width: units.gu(12); height: searchLabel.height;
389+ color: Constants.green;
390+ text: i18n.tr("Search")
391+
392+ onClicked: {
393+ if (searchLabel.text != "") {
394+ search_string = searchLabel.text;
395+ searchCityModel.reload();
396+ }
397+ }
398+ }
399+
400+ ActivityIndicator {
401+ running: searchCityModel.status === XmlListModel.Loading
402+ }
403+ }
404+
405+ ListView {
406+ id: worldList
407+
408+ anchors { left: parent.left; right: parent.right }
409+ height: units.gu(35)
410+ model: searchCityModel
411+ currentIndex: -1
412+
413+ delegate: ListItem.Standard {
414+ id: delegateTest;
415+ text: searchCityModel.status == XmlListModel.Ready ? searchCityModel.get(index).city : i18n.tr("Loading...");
416+ selected: worldList.currentIndex == index;
417+
418+ onClicked: {
419+ worldList.currentIndex = index;
420+ worldClocks.clicked()
421+ }
422+ }
423+ }
424+}
425
426=== added file 'clock/WorldClockModel.qml'
427--- clock/WorldClockModel.qml 1970-01-01 00:00:00 +0000
428+++ clock/WorldClockModel.qml 2013-06-14 19:10:32 +0000
429@@ -0,0 +1,178 @@
430+/*
431+ * Copyright (C) 2013 Canonical Ltd
432+ *
433+ * This program is free software: you can redistribute it and/or modify
434+ * it under the terms of the GNU General Public License version 3 as
435+ * published by the Free Software Foundation.
436+ *
437+ * This program is distributed in the hope that it will be useful,
438+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
439+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
440+ * GNU General Public License for more details.
441+ *
442+ * You should have received a copy of the GNU General Public License
443+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
444+ *
445+ * Authored by: Nekhelesh Ramananthan <krnekhelesh@gmail.com>
446+ */
447+
448+import QtQuick 2.0
449+import QtQuick.LocalStorage 2.0
450+import "../common/ClockUtils.js" as Utils
451+
452+ListModel {
453+ id: model
454+
455+ readonly property string dbName: "ubuntu-clock-app"
456+ readonly property string dbVersion: "0.1"
457+ readonly property string dbDescription: "World Clock"
458+
459+ property real timeDiff: new Date().getTimezoneOffset()
460+ property string city: "UTC";
461+
462+ property var _db: null
463+
464+ function appendCurrentLocation(cityname, rawOffSet)
465+ {
466+ try {
467+ _db.transaction(function(tx){
468+ var currentValue;
469+ var res = tx.executeSql('SELECT * FROM CurrentClock');
470+ if (res.rows.length > 0) {
471+ currentValue = res.rows.item(0).rawOffSet;
472+ }
473+
474+ if (currentValue !== rawOffSet) {
475+ // Update existing value or insert if none
476+ Utils.log("Outer loop");
477+ if (currentValue !== undefined) {
478+ res = tx.executeSql('UPDATE CurrentClock SET rawOffSet = ?, cityName = ?', [rawOffSet, cityname]);
479+ Utils.log("Existing record found. Hence updating it!");
480+ } else {
481+ res = tx.executeSql('INSERT INTO CurrentClock VALUES(?, ?)', [cityname, rawOffSet]);
482+ Utils.log("No City found. Creating a new entry.");
483+ }
484+ }
485+ });
486+ } catch (err) {
487+ Utils.error("Error setting current location '" + "': " + err);
488+ return false;
489+ }
490+ }
491+
492+ function appendPreset(cityName, rawOffSet)
493+ {
494+ if (_appendDB(cityName, rawOffSet)) {
495+ model.append({"cityName" : cityName, "rawOffSet" : rawOffSet})
496+ }
497+ }
498+
499+ function removePreset(index)
500+ {
501+ var cityName = get(index).cityName
502+ if (_removeDB(cityName)) {
503+ model.remove(index);
504+ }
505+ }
506+
507+ function _createDB() {
508+ if (_db == null) return false;
509+ try {
510+ _db.transaction(function(tx){
511+ tx.executeSql('CREATE TABLE IF NOT EXISTS WorldClock(cityName TEXT, rawOffSet REAL)');
512+ });
513+ } catch (err) {
514+ Utils.error("Error creating WorldClock table in db:" + err);
515+ return false;
516+ }
517+ try {
518+ _db.transaction(function(tx){
519+ tx.executeSql('CREATE TABLE IF NOT EXISTS CurrentClock(cityName TEXT, rawOffSet REAL)');
520+ });
521+ } catch (err) {
522+ Utils.error("Error creating CurrentClock table in db:" + err);
523+ return false;
524+ }
525+ return true;
526+ }
527+
528+ function _loadDB() {
529+ _db = LocalStorage.openDatabaseSync(model.dbName, model.dbVersion, model.dbDescription, 1000);
530+ if (_db == null) return false;
531+ if (!_createDB()) {
532+ _db = null;
533+ return false;
534+ }
535+ try {
536+ _db.readTransaction(function(tx){
537+ var res = tx.executeSql('SELECT * FROM WorldClock');
538+ if (res.rows.length > 0) {
539+ for (var i = 0; i < res.rows.length; i++) {
540+ model.append({"cityName" : res.rows.item(i).cityName,
541+ "rawOffSet" : res.rows.item(i).rawOffSet});
542+ }
543+ }
544+ });
545+ } catch (err) {
546+ Utils.error("Error opening database: " + err);
547+ _db = null
548+ return false;
549+ }
550+ try {
551+ _db.readTransaction(function(tx){
552+ var currentstate = tx.executeSql('SELECT * FROM CurrentClock ORDER BY cityName');
553+ if (currentstate.rows.length > 0) {
554+ city = currentstate.rows.item(0).cityName;
555+ timeDiff = currentstate.rows.item(0).rawOffSet
556+ Utils.log("Inside reading")
557+ }
558+ });
559+ } catch (err) {
560+ Utils.error("Error opening database: " + err);
561+ _db = null
562+ return false;
563+ }
564+ return true;
565+ }
566+
567+ function _appendDB(cityName, rawOffSet) {
568+ if (_db == null) return false;
569+
570+ try {
571+ _db.transaction(function(tx){
572+ var currentValue;
573+ var res = tx.executeSql('SELECT rawOffSet FROM WorldClock WHERE cityName=?', cityName);
574+ if (res.rows.length > 0) {
575+ currentValue = res.rows.item(0).rawOffSet;
576+ }
577+
578+ if (currentValue !== rawOffSet) {
579+ // Update existing value or insert if none
580+ if (currentValue !== undefined) {
581+ res = tx.executeSql('UPDATE WorldClock SET rawOffSet = ? WHERE cityName = ?', [rawOffSet, cityName]);
582+ } else {
583+ res = tx.executeSql('INSERT INTO WorldClock VALUES(?, ?)', [cityName, rawOffSet]);
584+ }
585+ }
586+ });
587+ } catch (err) {
588+ Utils.error("Error setting labeltxt '"+ label + "': " + err);
589+ return false;
590+ }
591+ return true;
592+ }
593+
594+ function _removeDB(cityName) {
595+ if (_db == null) return false;
596+
597+ try {
598+ _db.transaction(function(tx){
599+ tx.executeSql('DELETE FROM WorldClock WHERE cityName = ?', [cityName]);
600+ });
601+ } catch (err) {
602+ Utils.error("Error delete WorldClock'" + cityName + "': " + err);
603+ return false;
604+ }
605+ return true;
606+ }
607+}

Subscribers

People subscribed via source and target branches