[Huawei Developers] muraliameakula:
Article Introduction

In this article we will develop Prayer Times application for Huawei Smart Watch device using Huawei DevEco Studio (HarmonyOS). We will fetch Location using HarmonyOS JS language API’s and use some of the npm libraries (adhan, moment, moment-timezone, tz-lookup) to develop complete Real world Prayer Times Calculation Application.

1. Create New Project

Let’s create Smart Watch Project and choosing ability template, Empty Ability (JS)

Define project name, package name and relevant directory where you want to save your project. Choose the Device type “wearable” for which we are developing the application.

2. Preparing Files and Permission

Let’s first add images and permissions which we will use for project.

All project images will be under common/images folder, check below screenshot.

Next we need to add Location and Internet permissions under config.json file.

"reqPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.LOCATION",
"reason": "get user location to show prayer time",
"usedScene": {
"ability": [
"default"
],
"when": "always"
}
}
]

3. NPM libraries installation

We need to install following NPM libraries in the application:

adhan

moment

moment-timezone

tz-lookup

First we need to open the terminal under our DevEco studio project.

We need to change directory to entry folder.

cd entry

Now we need to install all the required libraries for our project.

npm i adhan moment moment-timezone tz-lookup -s

After installation our package.json file look like below:

{
"dependencies": {
"adhan": "^4.1.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.33",
"tz-lookup": "^6.1.25"
}
}

4. Prayer Time App Development

In Prayer time screen development we will cover Location permission, Location fetching, location error layout, prayer timer screen and today all prayers dialog screen.

Let’s start development without wasting more time.

Styling:

index.css: (Common screen styling)

/* common styling */
.container {
background-color: black;
justify-content: center;
}

.container-sub {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
flex-direction: column;
padding-top: 24px;
}

.container-location-loading {
flex-direction: column;
padding-top: 0px;
padding-bottom: 0px;
height: 456px;
width: 456px;
}

.column {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
background-color: transparent;
}

.row {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 80%;
height: 25px;
background-color: transparent;
}

.title {
text-align: center;
display: flex;
font-size: 16px;
}

.center {
text-align: center;
}

.location_loading {
object-fit: contain;
height: 456px;
width: 240px;
text-align: center;
align-items: center;
}

.current_time {
font-size: 18px;
text-align: center;
}

.mosque {
margin-top: 5px;
text-align: center;
fit-original-size: true;
}

.prayer_name {
text-align: center;
font-size: 16px;
margin-top: 2px;
margin-bottom: 5px;
}

.remaining_timer {
text-align: center;
font-size: 14px;
}

.button-circle {
background-color: transparent;
}

index.css: (Prayer BG & Color styling)

/* prayer BG & Color */
.prayer_bg {
background-position: top center;
background-size: 100% 280px;
}

.fajr_bg {
background-image: url('/common/images/prayer_bg/fajr.jpg');
}

.fajr_color {
background-color: #30170d;
}

.dhuhr_bg {
background-image: url('/common/images/prayer_bg/dhuhr.jpg');
}

.dhuhr_color {
background-color: #021823;
}

.asr_bg {
background-image: url('/common/images/prayer_bg/asr.jpg');
}

.asr_color {
background-color: #172B34;
}

.maghrib_bg {
background-image: url('/common/images/prayer_bg/maghrib.jpg');
}

.maghrib_color {
background-color: #010101;
}

.isha_bg {
background-image: url('/common/images/prayer_bg/isha.jpg');
}

.isha_color {
background-color: #082C44;
}

.night_bg {
background-image: url('/common/images/prayer_bg/night.jpg');
}

.night_color {
background-color: #131C39;
}

index.css: (Dialog styling)

/*Dialog styling*/
.dialog-main {
width: 100%;
}

.dialog-div {
display: flex;
flex-direction: column;
align-items: center;
}

.inner-txt {
width: 100%;
height: 300px;
flex-direction: column;
align-items: center;
}

.inner-btn {
width: 100%;
height: 154px;
align-items: center;
}

index.css: (List styling)

/*list styling*/
.list-wrapper {
width: 100%;
flex-direction: column;
}

.list-items {
width: 100%;
flex-direction: column;
padding: 0 24px;
}

.item-wrapper {
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
height: 34px;
margin: 8px 0;
}

.item-icon-wrapper {
width: 24px;
}

.item-icon {
width: 24px;
height: 24px;
object-fit: contain;
}

.item-name-description-wrapper {
flex-direction: column;
justify-content: center;
align-items: center;
flex-grow: 1;
flex-shrink: 1;
width: 50%;
margin-right: 24px;
margin-left: 24px;
}

.item-name {
text-align: left;
color: #DBFFFFFF;
font-size: 16px;
}

.item-description {
text-align: left;
opacity: 0.75;
color: #99FFFFFF;
font-size: 14px;
}

.item-right-part-wrapper {
flex-direction: row;
justify-content: flex-end;
align-items: center;
}

.item-right-text {
margin-right: 4px;
margin-left: 8px;
font-size: 14px;
opacity: 0.75;
}

.item-right-arrow {
width: 12px;
height: 24px;
object-fit: contain;
}

.line {
stroke-width: 1px;
width: 100%;
background-color: #33FFFFFF;
margin-left: 40px;
}

index.css: (Birds animation styling)

/* Birds animation */
.birds_animation {
object-fit: scale-down;
position: absolute;
top: 0px;
left: -200px;
animation-name: Fly;
animation-duration: 15s;
animation-timing-function: ease;
animation-iteration-count: infinite;
}

@keyframes Fly {
from {
transform: translateX(-200px);
}

to {
transform: translateX(1000px);
}
}

Layout:

Index.hml: (Location Loading Animation)

<div if="{{ isLocationLoading === true }}" class="container-location-loading">
<image src="common/images/location_animation.gif" class="location_loading"/>
</div>

Index.hml: (Location Loading Output):

Index.hml: (Location Error & Retry)

<div class="column" if="{{ isLocationLoading === false && isLocationError === true }}">
<text class="title">Location not fetch, please try again later.</text>
</div>

Index.hml: (Prayer timer UI)

<div class="container-sub prayer_bg {{ prayer_bg }}" if="{{ isLocationLoading === false && isLocationError === false }}">
<image src="common/images/birds.gif" class="birds_animation"></image>
<text class="current_time">{{ currentTime }}</text>
<image class="mosque" src="common/images/mosque.png"></image>
<text class="prayer_name">{{nextPrayer}} {{nextPrayerTime}}</text>
<text if="{{isShowTargetTime}}" class="remaining_timer">{{nextPrayerRemaining}}</text>
<button type="circle" class="button-circle"
ontouchend="showPrayer" icon="common/images/down-arrow.png"></button>
</div>

Index.hml: (Prayer timer UI Output)

Index.hml: (Dialog all Prayer times)

<dialog id="simpledialog" class="dialog-main">
<div class="dialog-div {{ dialog_bg }}">
<button type="circle" class="button-circle"
ontouchend="closePrayer" icon="common/images/close.png"></button>
<div class="inner-txt">
<div class="prayers-list">
<div class="list-items-left">
<list class="list-wrapper" initialindex="{{ initial_index_value }}">
<block for="{{ prayer_data }}">
<list-item class="list-items" @click="changeList($idx)" id="{{ $idx }}">
<div class="item-wrapper">
<div class="item-icon-wrapper">
<image class="item-icon" src="{{ $item.item_icon }}"></image>
</div>
<div class="item-name-description-wrapper">
<text class="item-name">{{ $item.item_name }}</text>
<text class="item-description">{{ $item.item_description }}</text>
</div>
<div class="item-right-part-wrapper">
<image class="item-right-arrow" src="common/images/right_arrow_dark_mode.png"></image>
</div>
</div>
<div class="divider-line">
<divider class="line"></divider>
</div>
</list-item>
</block>
</list>
</div>
</div>
</div>
</div>
</dialog>

Index.hml: (Dialog all Prayer times Ouput)

Index.hml: (Complete code)

<div class="container {{ (isLocationLoading === false) ? 'column' : '' }}">
<div if="{{ isLocationLoading === true }}" class="container-location-loading">
<image src="common/images/location_animation.gif" class="location_loading"/>
</div>
<div class="column" if="{{ isLocationLoading === false && isLocationError === true }}">
<text class="title">Location not fetch, please try again later.</text>
</div>
<div class="container-sub prayer_bg {{ prayer_bg }}" if="{{ isLocationLoading === false && isLocationError === false }}">
<image src="common/images/birds.gif" class="birds_animation"></image>
<text class="current_time">{{ currentTime }}</text>
<image class="mosque" src="common/images/mosque.png"></image>
<text class="prayer_name">{{nextPrayer}} {{nextPrayerTime}}</text>
<text if="{{isShowTargetTime}}" class="remaining_timer">{{nextPrayerRemaining}}</text>
<button type="circle" class="button-circle"
ontouchend="showPrayer" icon="common/images/down-arrow.png"></button>
</div>

<dialog id="simpledialog" class="dialog-main">
<div class="dialog-div {{ dialog_bg }}">
<button type="circle" class="button-circle"
ontouchend="closePrayer" icon="common/images/close.png"></button>
<div class="inner-txt">
<div class="prayers-list">
<div class="list-items-left">
<list class="list-wrapper" initialindex="{{ initial_index_value }}">
<block for="{{ prayer_data }}">
<list-item class="list-items" @click="changeList($idx)" id="{{ $idx }}">
<div class="item-wrapper">
<div class="item-icon-wrapper">
<image class="item-icon" src="{{ $item.item_icon }}"></image>
</div>
<div class="item-name-description-wrapper">
<text class="item-name">{{ $item.item_name }}</text>
<text class="item-description">{{ $item.item_description }}</text>
</div>
<div class="item-right-part-wrapper">
<image class="item-right-arrow" src="common/images/right_arrow_dark_mode.png"></image>
</div>
</div>
<div class="divider-line">
<divider class="line"></divider>
</div>
</list-item>
</block>
</list>
</div>
</div>
</div>
</div>
</dialog>

</div>

JS code:

index.js: (Structural - Code)

import geolocation from '@system.geolocation';

import adhan from 'adhan';
import moment from 'moment';
import tz from 'moment-timezone';
var tzlookup = require("tz-lookup");

const TAG = 'app_log [index]';

export default {}

index.js: (Data - Code)

data: {
config: {
isTesting: true,
locationCoordinates: {
"latitude": 24.65382908421087,
"longitude": 46.73552629355017
},
timeZone: "Asia/Riyadh",
fakeDateTime: "2021-06-12 18:13:01"
},
prayer_data: [
{
item_id: "fajr",
item_icon: "common/images/prayer_icon/fajr.png",
item_name: 'Fajr',
item_description: ''
},
{
item_id: "dhuhr",
item_icon: "common/images/prayer_icon/dhuhr.png",
item_name: 'Dhuhr',
item_description: ''
},
{
item_id: "asr",
item_icon: "common/images/prayer_icon/asr.png",
item_name: 'Asr',
item_description: ''
},
{
item_id: "maghrib",
item_icon: "common/images/prayer_icon/maghrib.png",
item_name: 'Maghrib',
item_description: ''
},
{
item_id: "isha",
item_icon: "common/images/prayer_icon/isha.png",
item_name: 'Isha',
item_description: ''
},
],
defaultPrayerSetting: {
allowNotification: false,
prayerSetting: {
Madhab: 'Shafi',
calculationMethod: 'UmmAlQura',
adjustments: {
fajr: "0",
sunrise: "0",
dhuhr: "0",
asr: "0",
maghrib: "0",
isha: "0"
}
}
},
initial_index_value: 2,
isLocationError: false,
isLocationLoading: true,
locationCoordinates: null,
currentTime: null,
timeUpdateTimer: null,
nextPrayer: 'Night',
nextPrayerTime: '',
nextPrayerRemaining: '',
isShowTargetTime: true,
date: moment().toDate(),
prayer_bg: "night_bg",
dialog_bg: "night_color"
},

index.js: (Common - Code)

onInit() {
console.log(TAG + 'onInit');
if(this.config.isTesting === true){
this.locationCoordinates = this.config.locationCoordinates
moment.tz.setDefault(this.config.timeZone);
this.date = moment(this.config.fakeDateTime).toDate();
}
this.currentTime = moment(this.date).format('ddd LT');
this.timeUpdateTimer = setInterval(this.updateTimer, 2000);

},
onReady() {
console.log(TAG + 'onReady');
var _this = this;
if (this.locationCoordinates !== null) {
setTimeout(() => {
_this.calculatePrayerTime();
_this.isLocationLoading = false;
_this.isLocationError = false;
}, 4000);
} else {
this.locationLoading().then(result => {
_this.locationCoordinates = result;
console.info(TAG + "Location: " + result);
_this.calculatePrayerTime();
_this.isLocationLoading = false;
_this.isLocationError = false;
}, error => {
console.info(TAG + "Location: error ->" + error);
_this.isLocationLoading = false;
_this.isLocationError = true;
});
}
},
onShow() {
console.log(TAG + 'onShow');
},
onDestroy() {
console.log(TAG + 'onDestroy');
clearInterval(this.timeUpdateTimer);
this.timeUpdateTimer = null;

clearInterval(this.countDownTimer);
this.countDownTimer = null;
},
updateTimer() {
this.currentTime = moment().format('ddd LT');
if(this.config.isTesting === true){
this.currentTime = moment(this.config.fakeDateTime).format('ddd LT');
}
},

index.js: (Dialog - Code)

showPrayer(e) {
this.$element('simpledialog').show();
},
closePrayer(e) {
this.$element('simpledialog').close();
},

index.js: (Location Fetching - Code)

locationLoading() {
return new Promise(function (resolve, reject) {
return geolocation.getLocation({
success: function (data) {
console.log('success get location data. latitude:' + data.latitude + 'long:' + data.longitude);
return resolve({
latitude: data.latitude,
longitude: data.longitude
});
},
fail: function (data, code) {
console.log('fail to get location. code:' + code + ', data:' + data);
return reject({
error: 'fail to get location. code:' + code + ', data:' + data
});
},
});
});
},

index.js: (Prayer times - Code)

calculatePrayerTime() {
var _this = this;
var prayerSettings = this.defaultPrayerSetting;
console.log(TAG + 'prayer_setting: getPrayerSetting() ' + JSON.stringify(prayerSettings));
if (prayerSettings !== null) {
this.prayerSettings = prayerSettings;
var params = this.getPrayerParameter(this.prayerSettings);
var coordinates = new adhan.Coordinates(_this.locationCoordinates.latitude, _this.locationCoordinates.longitude);
var date = this.date;

var prayerTimes = new adhan.PrayerTimes(coordinates, date, params);
console.info(TAG + 'locationCoordinates ' + JSON.stringify(_this.locationCoordinates));

var timezone = tzlookup(_this.locationCoordinates.latitude, _this.locationCoordinates.longitude)
if(this.config.isTesting === true){
timezone = this.config.timeZone
}
console.log(TAG + "timezone: " + timezone);

var nextPrayer = prayerTimes.nextPrayer(date);
var currentPrayer = prayerTimes.currentPrayer(date);
console.info(TAG + 'nextPrayer ' + nextPrayer);
console.info(TAG + 'currentPrayer ' + currentPrayer);

if (nextPrayer.toString() === "none") {
_this.isShowTargetTime = false
_this.nextPrayer = "Night";
_this.managePrayerTime(prayerTimes, timezone, nextPrayer, currentPrayer)
} else {
_this.isShowTargetTime = true
_this.nextPrayer = nextPrayer;
var nextPrayerTime = prayerTimes.timeForPrayer(nextPrayer);
_this.nextPrayerTime = moment(nextPrayerTime).tz(timezone).format('h:mm A');

_this.setTimeInfo(nextPrayerTime.getTime());
_this.managePrayerTime(prayerTimes, timezone, nextPrayer, currentPrayer)
}
}
},
managePrayerTime(prayerTimes, timezone, nextPrayer, currentPrayer) {
var _this = this;
var fajrTime = moment(prayerTimes.fajr).tz(timezone).format('h:mm A');
var sunriseTime = moment(prayerTimes.sunrise).tz(timezone).format('h:mm A');
var dhuhrTime = moment(prayerTimes.dhuhr).tz(timezone).format('h:mm A');
var asrTime = moment(prayerTimes.asr).tz(timezone).format('h:mm A');
var maghribTime = moment(prayerTimes.maghrib).tz(timezone).format('h:mm A');
var ishaTime = moment(prayerTimes.isha).tz(timezone).format('h:mm A');

_this.prayer_data.map(item => {
if (item.item_id === "fajr") {
item.item_description = fajrTime;
}
if (item.item_id === "dhuhr") {
item.item_description = dhuhrTime;
}
if (item.item_id === "asr") {
item.item_description = asrTime;
}
if (item.item_id === "maghrib") {
item.item_description = maghribTime;
}
if (item.item_id === "isha") {
item.item_description = ishaTime;
}
if (nextPrayer.toString().toLowerCase() === item.item_id) {
_this.prayer_bg = item.item_id + "_bg";
_this.dialog_bg = item.item_id + "_color";
}
});
},
getPrayerParameter(prayerSettings) {
var params = adhan.CalculationMethod.UmmAlQura();
var prayerSetting = prayerSettings.prayerSetting;
if (prayerSetting.calculationMethod === 'MuslimWorldLeagueMuslimWorldLeague') {
params = adhan.CalculationMethod.MuslimWorldLeague();
} else if (prayerSetting.calculationMethod === 'Egyptian') {
params = adhan.CalculationMethod.Egyptian();
} else if (prayerSetting.calculationMethod === 'Karachi') {
params = adhan.CalculationMethod.Karachi();
} else if (prayerSetting.calculationMethod === 'Dubai') {
params = adhan.CalculationMethod.Dubai();
} else if (prayerSetting.calculationMethod === 'MoonsightingCommittee') {
params = adhan.CalculationMethod.MoonsightingCommittee();
} else if (prayerSetting.calculationMethod === 'NorthAmerica') {
params = adhan.CalculationMethod.NorthAmerica();
} else if (prayerSetting.calculationMethod === 'Kuwait') {
params = adhan.CalculationMethod.Kuwait();
} else if (prayerSetting.calculationMethod === 'Qatar') {
params = adhan.CalculationMethod.Qatar();
} else if (prayerSetting.calculationMethod === 'Singapore') {
params = adhan.CalculationMethod.Singapore();
} else if (prayerSetting.calculationMethod === 'Other') {
params = adhan.CalculationMethod.Other();
}

if (prayerSetting.Madhab === 'Shafi') {
params.madhab = adhan.Madhab.Shafi;
} else {
params.madhab = adhan.Madhab.Hanafi;
}

params.adjustments.fajr = parseInt(prayerSetting.adjustments.fajr) || 0;
params.adjustments.sunrise = parseInt(prayerSetting.adjustments.sunrise) || 0;
params.adjustments.dhuhr = parseInt(prayerSetting.adjustments.dhuhr) || 0;
params.adjustments.asr = parseInt(prayerSetting.adjustments.asr) || 0;
params.adjustments.maghrib = parseInt(prayerSetting.adjustments.maghrib) || 0;
params.adjustments.isha = parseInt(prayerSetting.adjustments.isha) || 0;

return params;
},

index.js: (Count down timer - Code)

setTimeInfo(next_time) {
console.log(TAG + "next_time: " + next_time);
this.CaculateTime(next_time);
this.countDownTimer = setInterval(() => {
this.CaculateTime(next_time);
}, 1000);
},
CaculateTime(timeObj) {
var myDate = new Date();
if (this.config.isTesting === true) {
this.date = moment(this.date).add(500, 'milliseconds').toDate();
myDate = this.date;
}
let currentTime = myDate.getTime();
var targetTime = parseInt(timeObj);
var remainTime = parseInt(targetTime - currentTime);
if (remainTime > 0) {
this.isShowTargetTime = true;
this.setRemainTime(remainTime);
//this.setTargetTime(targetTime);
}
},
setRemainTime(remainTime) {
let days = this.addZero(Math.floor(remainTime / (24 * 3600 * 1000))); // Calculate the number of days
let leavel = remainTime % (24 * 3600 * 1000); // Time remaining after counting days
let hours = this.addZero(Math.floor(leavel / (3600 * 1000))); // Calculate the number of hours remaining
let leavel2 = leavel % (3600 * 1000); // Number of milliseconds remaining after calculating the remaining hours
let minutes = this.addZero(Math.floor(leavel2 / (60 * 1000))); // Calculate the number of minutes remaining

// Calculate the difference seconds.
let leavel3 = leavel2 % (60 * 1000); // Number of milliseconds remaining after minutes are calculated
let seconds = this.addZero(Math.round(leavel3 / 1000));
this.nextPrayerRemaining = hours + ':' + minutes + ':' + seconds;
},
setTargetTime(targetTime) {
var _this = this
var times = new Date(targetTime);

let date = times.toLocaleDateString(); //Gets the current date
var tempSetHours = times.getHours(); //Gets the current number of hours (0 - 23)
let hours = this.addZero(tempSetHours)
var tempSetMinutes = times.getMinutes(); //Gets the current number of minutes (0 - 59)
let minutes = this.addZero(tempSetMinutes)
var tempSetSeconds = times.getSeconds(); //Gets the current number of seconds (0 - 59)
let seconds = this.addZero(tempSetSeconds)
this.targetTime = `${hours}:${minutes}:${seconds}`;
},
addZero: function (i) {
return i < 10 ? "0" + i : i + "";
},

Prayer time Screen Notes:

To manage different state of application on single screen, we can able to use logic layouts using if=”true/false” or show=”true/false” conditions on containers.

For testing of custom date and time developer need to modify config data variable (isTesting: true).

For production we need to apply isTesting: false and relay on real date and time.

For prayer time parameter we implement adhan npm libraries, developer can have access on prayer time adjustment (plus/minus) in minutes.

For better management of prayer time parameters always use local storage (key/value), to save user preferences in storage and adjust prayer time.

5. Result

Tips & Tricks:

HarmonyOS JS project while installing any NPM libraries, in terminal must be on entry folder of your project module.

For testing of any date and time, developer need to modify config data variable (isTesting: true)

For production or realtime device, developer need to modify config data variable (isTesting: false)

For prayer time adjustment developer can modify defaultPrayerSetting data variable and could store user preference in storage.

Requesting some data from internet, you must need to add Internet permission in config.json file.

Fetching Location data, you must need to add Internet Permission in config.json file.

Use Dev Eco Studio Previewer to check the screen layout and design. Previewer is developer friendly to Hot release changes on fly.

For better management of big application it’s a good practice to centralize you common scripts and common style in common folder. Add images folder for complete application images.

In JS script when you make some variable, in callback functions you can store the reference of this to some variable and then call reference variable. Like var _this = this.

References:

HarmonyOS JS API Official Documentation: https://developer.harmonyos.com/en/docs/documentation/doc-references/js-apis-overview-0000001056361791

Geographic Location Documentation: https://developer.harmonyos.com/en/docs/documentation/doc-references/js-apis-system-location-0000000000626089

Conclusion:

Developers can able to develop real world Prayer time calculation application, while fetch user location data and using npm ready-made libraries. While developing application for HarmonyOS developer can get benefit for both JS and JAVA language. Benefit for developing JS based HarmonyOS application developer can able to use npm based libraries and can reduce development time.

Original Source