दूसरा चरण: किसी मौजूदा वेब ऐप्लिकेशन को इंपोर्ट करना

इस चरण में, आपको इनके बारे में जानकारी मिलेगी:

  • किसी मौजूदा वेब ऐप्लिकेशन को Chrome ऐप्लिकेशन प्लैटफ़ॉर्म के हिसाब से बनाने का तरीका.
  • अपनी ऐप्लिकेशन स्क्रिप्ट को कॉन्टेंट की सुरक्षा नीति (सीएसपी) के मुताबिक बनाने का तरीका.
  • chrome.storage.local का इस्तेमाल करके, लोकल स्टोरेज को लागू करने का तरीका.

इस चरण को पूरा करने में लगने वाला अनुमानित समय: 20 मिनट.
इस चरण में आपको क्या करना है, इसकी झलक देखने के लिए, इस पेज पर सबसे नीचे जाएं ↓.

किसी मौजूदा Todo ऐप्लिकेशन को इंपोर्ट करना

शुरुआत में, TodoMVC का वैनिला JavaScript वर्शन इंपोर्ट करें. यह एक सामान्य मानदंड ऐप्लिकेशन है. इसे अपने प्रोजेक्ट में इंपोर्ट करें.

हमने todomvc फ़ोल्डर में, रेफ़रंस कोड zip में TodoMVC ऐप्लिकेशन का एक वर्शन शामिल किया है. todomvc से सभी फ़ाइलों (इनमें फ़ोल्डर भी शामिल हैं) को अपने प्रोजेक्ट फ़ोल्डर में कॉपी करें.

todomvc फ़ोल्डर को codelab फ़ोल्डर में कॉपी करना

आपसे index.html को बदलने के लिए कहा जाएगा. आगे बढ़ें और न्योता स्वीकार करें.

index.html को बदलना

अब आपके ऐप्लिकेशन फ़ोल्डर में, फ़ाइल का यह स्ट्रक्चर होना चाहिए:

नया प्रोजेक्ट फ़ोल्डर

नीले रंग से हाइलाइट की गई फ़ाइलें, todomvc फ़ोल्डर से हैं.

अपने ऐप्लिकेशन को अभी फिर से लोड करें (राइट क्लिक करें > ऐप्लिकेशन फिर से लोड करें). आपको बुनियादी यूज़र इंटरफ़ेस (यूआई) दिखेगा, लेकिन टास्क नहीं जोड़े जा सकेंगे.

स्क्रिप्ट को कॉन्टेंट की सुरक्षा के बारे में नीति (सीएसपी) के मुताबिक बनाना

DevTools कंसोल खोलें (राइट क्लिक > एलिमेंट की जांच करें, फिर कंसोल टैब चुनें). आपको इनलाइन स्क्रिप्ट को लागू न करने से जुड़ी गड़बड़ी दिखेगी:

सीएसपी कंसोल लॉग में गड़बड़ी वाला काम की सूची वाला ऐप्लिकेशन

आइए, ऐप्लिकेशन को कॉन्टेंट की सुरक्षा के बारे में नीति के मुताबिक बनाकर इस गड़बड़ी को ठीक करते हैं. सीएसपी के नियमों का पालन न करने की सबसे सामान्य वजहों में से एक, इनलाइन JavaScript है. इनलाइन JavaScript के उदाहरणों में, एचटीएमएल में मौजूद कॉन्टेंट के साथ <script> टैग और DOM एट्रिब्यूट (उदाहरण के लिए, <button onclick=''>) के तौर पर इवेंट हैंडलर शामिल हैं.

इसका समाधान आसान है: इनलाइन कॉन्टेंट को किसी नई फ़ाइल में ले जाएं.

1. index.html के सबसे नीचे, इनलाइन JavaScript को हटाएं और इसके बजाय, js/bootstrap.js शामिल करें:

<script src="bower_components/director/build/director.js"></script>
<script>
  // Bootstrap app data
  window.app = {};
</script>
<script src="js/bootstrap.js"></script>
<script src="js/helpers.js"></script>
<script src="js/store.js"></script>

2. js फ़ोल्डर में, bootstrap.js नाम की फ़ाइल बनाएं. पहले के इनलाइन कोड को इस फ़ाइल में ले जाएं:

// Bootstrap app data
window.app = {};

अगर आपने अभी ऐप्लिकेशन को फिर से लोड किया, तो भी Todo ऐप्लिकेशन काम नहीं करेगा. हालांकि, यह समस्या हल होने वाली है.

localStorage को chrome.storage.local में बदलें

अब DevTools कंसोल खोलने पर, पिछली गड़बड़ी नहीं दिखनी चाहिए. हालांकि, window.localStorage के उपलब्ध न होने से जुड़ी एक नई गड़बड़ी है:

localStorage कंसोल लॉग में गड़बड़ी वाला Todo ऐप्लिकेशन

Chrome ऐप्स localStorage का समर्थन नहीं करते, क्योंकि localStorage सिंक्रोनस है. सिंगल-थ्रेड वाले रनटाइम में, ब्लॉक करने वाले संसाधनों (I/O) का सिंक्रोनस ऐक्सेस, आपके ऐप्लिकेशन को अनरिस्पॉन्सिव बना सकता है.

Chrome ऐप्स में एक समान एपीआई होता है जो ऑब्जेक्ट को एसिंक्रोनस रूप से स्टोर कर सकता है. इससे कभी-कभी भारी ऑब्जेक्ट->स्ट्रिंग->ऑब्जेक्ट को क्रम में लगाने की प्रोसेस से बचने में मदद मिलती है.

हमारे ऐप्लिकेशन में गड़बड़ी का मैसेज ठीक करने के लिए, आपको localStorage को chrome.storage.local में बदलना होगा.

ऐप्लिकेशन की अनुमतियां अपडेट करना

chrome.storage.local का इस्तेमाल करने के लिए, आपको storage की अनुमति का अनुरोध करना होगा. manifest.json में, permissions कलेक्शन में "storage" जोड़ें:

"permissions": ["storage"],

local.storage.set() और local.storage.get() के बारे में जानें

'क्या-क्या करें' सूची में आइटम सेव करने और उन्हें वापस पाने के लिए, आपको chrome.storage एपीआई के set() और get() तरीकों के बारे में जानकारी होनी चाहिए.

set() तरीका, पहले पैरामीटर के तौर पर की-वैल्यू पेयर के ऑब्जेक्ट को स्वीकार करता है. दूसरा पैरामीटर, वैकल्पिक कॉलबैक फ़ंक्शन होता है. उदाहरण के लिए:

chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
  console.log("Secret message saved");
});

get() तरीके से डेटास्टोर की उन कुंजियों के लिए एक वैकल्पिक पहला पैरामीटर स्वीकार किया जाता है जिन्हें आपको फिर से वापस लाना है. एक कुंजी को स्ट्रिंग के तौर पर पास किया जा सकता है. कई कुंजियों को स्ट्रिंग के ऐरे या डिक्शनरी ऑब्जेक्ट में व्यवस्थित किया जा सकता है.

दूसरा पैरामीटर, जो ज़रूरी है वह कॉलबैक फ़ंक्शन है. लौटाए गए ऑब्जेक्ट में, सेव की गई वैल्यू को ऐक्सेस करने के लिए पहले पैरामीटर में अनुरोध की गई कुंजियों का इस्तेमाल करें. उदाहरण के लिए:

chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
  console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});

अगर आपको chrome.storage.local में मौजूद सभी चीज़ों को get() करना है, तो पहला पैरामीटर हटाएं:

chrome.storage.local.get(function(data) {
  console.log(data);
});

localStorage के विपरीत, DevTools के रिसॉर्स पैनल का इस्तेमाल करके, डिवाइस पर सेव किए गए आइटम की जांच नहीं की जा सकती. हालांकि, JavaScript कंसोल से chrome.storage के साथ इस तरह इंटरैक्ट किया जा सकता है:

chrome.storage को डीबग करने के लिए, कंसोल का इस्तेमाल करना

एपीआई में किए गए ज़रूरी बदलावों की झलक देखें

Todo ऐप्लिकेशन को कन्वर्ज़न करने के बाकी ज़्यादातर चरणों में, एपीआई कॉल में छोटे बदलाव किए जाते हैं. उन सभी जगहों को बदलना ज़रूरी है जहां फ़िलहाल localStorage का इस्तेमाल हो रहा है. हालांकि, इसमें ज़्यादा समय लगता है और इसमें गड़बड़ी हो सकती है.

localStorage और chrome.storage के बीच मुख्य अंतर, chrome.storage के असाइन किए जाने के तरीके से जुड़े हैं:

  • आसान असाइनमेंट का इस्तेमाल करके localStorage में लिखने के बजाय, आपको वैकल्पिक कॉलबैक के साथ chrome.storage.local.set() का इस्तेमाल करना होगा.

    var data = { todos: [] };
    localStorage[dbName] = JSON.stringify(data);
    

    बनाम

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • localStorage[myStorageName] को सीधे ऐक्सेस करने के बजाय, आपको chrome.storage.local.get(myStorageName,function(storage){...}) का इस्तेमाल करना होगा. इसके बाद, कॉलबैक फ़ंक्शन में, दिखाए गए storage ऑब्जेक्ट को पार्स करना होगा.

    var todos = JSON.parse(localStorage[dbName]).todos;
    

    बनाम

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • .bind(this) फ़ंक्शन का इस्तेमाल सभी कॉलबैक पर किया जाता है. इससे यह पक्का किया जाता है कि this, Store प्रोटोटाइप के this से जुड़ा है. (बाउंड किए गए फ़ंक्शन के बारे में ज़्यादा जानकारी, MDN दस्तावेज़ों पर मिल सकती है: Function.prototype.bind().)

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'undefined'
      });
    }
    new Store();
    

    बनाम

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'inside Store'
      }.bind(this));
    }
    new Store();
    

इन मुख्य अंतरों को ध्यान में रखें, क्योंकि हम अगले सेक्शन में, 'क्या-क्या करें' सूची में मौजूद आइटम को वापस पाने, सेव करने, और हटाने के बारे में बताएंगे.

सूची में मौजूद आइटम वापस लाएं

Todo ऐप्लिकेशन में मौजूद आइटम वापस पाने के लिए, आइए उसे अपडेट करें:

1. Store कन्स्ट्रक्टर का तरीका, Todo ऐप्लिकेशन को डेटास्टोर में मौजूद सभी मौजूदा टास्क आइटम के साथ शुरू करने का ध्यान रखता है. यह तरीका सबसे पहले यह जांच करता है कि डेटास्टोर मौजूद है या नहीं. अगर ऐसा नहीं होता है, तो यह todos का खाली कलेक्शन बनाएगा और उसे डेटास्टोर में सेव कर देगा, ताकि रनटाइम के दौरान डेटा पढ़ने से जुड़ी कोई गड़बड़ी न हो.

js/store.js में, कंस्ट्रक्टर वाले तरीके में localStorage के इस्तेमाल को बदलें, ताकि आप इसके बजाय chrome.storage.local:

function Store(name, callback) {
  var data;
  var dbName;

  callback = callback || function () {};

  dbName = this._dbName = name;

  if (!localStorage[dbName]) {
    data = {
      todos: []
    };
    localStorage[dbName] = JSON.stringify(data);
  }
  callback.call(this, JSON.parse(localStorage[dbName]));

  chrome.storage.local.get(dbName, function(storage) {
    if ( dbName in storage ) {
      callback.call(this, storage[dbName].todos);
    } else {
      storage = {};
      storage[dbName] = { todos: [] };
      chrome.storage.local.set( storage, function() {
        callback.call(this, storage[dbName].todos);
      }.bind(this));
    }
  }.bind(this));
}

2. मॉडल से 'क्या-क्या करें' सूची पढ़ते समय, find() तरीके का इस्तेमाल किया जाता है. "सभी", "चालू" या "पूरा हो गया" में से किसी एक के हिसाब से फ़िल्टर करने पर, आपको अलग-अलग नतीजे मिलेंगे.

chrome.storage.local का इस्तेमाल करने के लिए find() को बदलें:

Store.prototype.find = function (query, callback) {
  if (!callback) {
    return;
  }

  var todos = JSON.parse(localStorage[this._dbName]).todos;

  callback.call(this, todos.filter(function (todo) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var todos = storage[this._dbName].todos.filter(function (todo) {
      for (var q in query) {
         return query[q] === todo[q];
      }
      });
    callback.call(this, todos);
  }.bind(this));
  }));
};

3. find() की तरह ही, findAll() को मॉडल से सभी 'क्या-क्या करें' मिलते हैं. findAll() को chrome.storage.local में बदलने के लिए:

Store.prototype.findAll = function (callback) {
  callback = callback || function () {};
  callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
  chrome.storage.local.get(this._dbName, function(storage) {
    var todos = storage[this._dbName] && storage[this._dbName].todos || [];
    callback.call(this, todos);
  }.bind(this));
};

काम की सूची सेव करें

save() का मौजूदा तरीका चुनौती भरा है. यह दो असाइन किए गए ऑपरेशन (get और set) पर निर्भर करता है, जो हर बार पूरे मोनोलिथिक JSON स्टोरेज पर काम करते हैं. अगर एक से ज़्यादा टास्क आइटम पर एक साथ अपडेट किए जाते हैं, जैसे कि "सभी टास्क को 'पूरा हो गया' के तौर पर मार्क करें", तो डेटा से जुड़ी एक समस्या हो सकती है. इसे लिखने के बाद पढ़ना कहा जाता है. अगर हम IndexedDB जैसे बेहतर डेटा स्टोरेज का इस्तेमाल कर रहे होते, तो यह समस्या नहीं होती. हालांकि, हम इस कोडलैब के लिए कन्वर्ज़न की कोशिश को कम से कम करने की कोशिश कर रहे हैं.

इसे ठीक करने के कई तरीके हैं, इसलिए हम इस अवसर का इस्तेमाल करके, save() में थोड़ा सुधार करेंगे. इसके लिए, हम काम की सूची वाले कई आईडी लेंगे, जिन्हें एक साथ अपडेट किया जाएगा:

1. शुरू करने के लिए, save() में पहले से मौजूद सभी चीज़ों को chrome.storage.local.get() कॉलबैक के साथ रैप करें:

Store.prototype.save = function (id, updateData, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    // ...
    if (typeof id !== 'object') {
      // ...
    }else {
      // ...
    }
  }.bind(this));
};

2. localStorage के सभी इंस्टेंस को chrome.storage.local में बदलें:

Store.prototype.save = function (id, updateData, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    var data = storage[this._dbName];
    var todos = data.todos;

    callback = callback || function () {};

    // If an ID was actually given, find the item and update each property
    if ( typeof id !== 'object' ) {
      // ...

      localStorage[this._dbName] = JSON.stringify(data);
      callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
      chrome.storage.local.set(storage, function() {
        chrome.storage.local.get(this._dbName, function(storage) {
          callback.call(this, storage[this._dbName].todos);
        }.bind(this));
      }.bind(this));
    } else {
      callback = updateData;

      updateData = id;

      // Generate an ID
      updateData.id = new Date().getTime();

      localStorage[this._dbName] = JSON.stringify(data);
      callback.call(this, [updateData]);
      chrome.storage.local.set(storage, function() {
        callback.call(this, [updateData]);
      }.bind(this));
    }
  }.bind(this));
};

3. इसके बाद, लॉजिक को अपडेट करें, ताकि वह किसी एक आइटम के बजाय ऐरे पर काम कर सके:

Store.prototype.save = function (id, updateData, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = storage[this._dbName];
    var todos = data.todos;

    callback = callback || function () {};

    // If an ID was actually given, find the item and update each property
    if ( typeof id !== 'object' || Array.isArray(id) ) {
      var ids = [].concat( id );
      ids.forEach(function(id) {
        for (var i = 0; i < todos.length; i++) {
          if (todos[i].id == id) {
            for (var x in updateData) {
              todos[i][x] = updateData[x];
            }
          }
        }
      });

      chrome.storage.local.set(storage, function() {
        chrome.storage.local.get(this._dbName, function(storage) {
          callback.call(this, storage[this._dbName].todos);
        }.bind(this));
      }.bind(this));
    } else {
      callback = updateData;

      updateData = id;

      // Generate an ID
      updateData.id = new Date().getTime();

      todos.push(updateData);
      chrome.storage.local.set(storage, function() {
        callback.call(this, [updateData]);
      }.bind(this));
    }
  }.bind(this));
};

'क्या-क्या करें' सूची में मौजूद आइटम को 'पूरा हो गया' के तौर पर मार्क करना

अब ऐप्लिकेशन ऐरे पर काम कर रहा है. इसलिए, आपको यह बदलना होगा कि ऐप्लिकेशन, पूरा हो चुका है (#) बटन पर क्लिक करने वाले उपयोगकर्ता को कैसे मैनेज करता है:

1. controller.js में, काम की सूची को एक-एक करके 'पूरा हुआ' के तौर पर मार्क करने के बजाय, toggleAll() को सिर्फ़ एक बार toggleComplete() को कॉल करने के लिए अपडेट करें. इसमें काम की सूची शामिल करें. _filter() पर किए गए कॉल को भी मिटाएं, क्योंकि आपको toggleComplete _filter() में बदलाव करना है.

Controller.prototype.toggleAll = function (e) {
  var completed = e.target.checked ? 1 : 0;
  var query = 0;
  if (completed === 0) {
    query = 1;
  }
  this.model.read({ completed: query }, function (data) {
    var ids = [];
    data.forEach(function (item) {
      this.toggleComplete(item.id, e.target, true);
      ids.push(item.id);
    }.bind(this));
    this.toggleComplete(ids, e.target, false);
  }.bind(this));

  this._filter();
};

2. अब toggleComplete() को अपडेट करें, ताकि यह एक टास्क या कई टास्क, दोनों को स्वीकार कर सके. इसमें filter() को update() के बाहर से अंदर ले जाना शामिल है.

Controller.prototype.toggleComplete = function (ids, checkbox, silent) {
  var completed = checkbox.checked ? 1 : 0;
  this.model.update(ids, { completed: completed }, function () {
    if ( ids.constructor != Array ) {
      ids = [ ids ];
    }
    ids.forEach( function(id) {
      var listItem = $$('[data-id="' + id + '"]');
      
      if (!listItem) {
        return;
      }
      
      listItem.className = completed ? 'completed' : '';
      
      // In case it was toggled from an event and not by clicking the checkbox
      listItem.querySelector('input').checked = completed;
    });

    if (!silent) {
      this._filter();
    }

  }.bind(this));
};

Count todo items

After switching to async storage, there is a minor bug that shows up when getting the number of todos. You'll need to wrap the count operation in a callback function:

1. In model.js, update getCount() to accept a callback:

  Model.prototype.getCount = function (callback) {
  var todos = {
    active: 0,
    completed: 0,
    total: 0
  };
  this.storage.findAll(function (data) {
    data.each(function (todo) {
      if (todo.completed === 1) {
        todos.completed++;
      } else {
        todos.active++;
      }
      todos.total++;
    });
    if (callback) callback(todos);
  });
  return todos;
};

2. Back in controller.js, update _updateCount() to use the async getCount() you edited in the previous step:

Controller.prototype._updateCount = function () {
  var todos = this.model.getCount();
  this.model.getCount(function(todos) {
    this.$todoItemCounter.innerHTML = this.view.itemCounter(todos.active);

    this.$clearCompleted.innerHTML = this.view.clearCompletedButton(todos.completed);
    this.$clearCompleted.style.display = todos.completed > 0 ? 'block' : 'none';

    this.$toggleAll.checked = todos.completed === todos.total;

    this._toggleFrame(todos);
  }.bind(this));

};

You are almost there! If you reload the app now, you will be able to insert new todos without any console errors.

Remove todos items

Now that the app can save todo items, you're close to being done! You still get errors when you attempt to remove todo items:

Todo app with localStorage console log error

1. In store.js, convert all the localStorage instances to use chrome.storage.local:

a) To start off, wrap everything already inside remove() with a get() callback:

Store.prototype.remove = function (id, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    var todos = data.todos;

    for (var i = 0; i < todos.length; i++) {
      if (todos[i].id == id) {
        todos.splice(i, 1);
        break;
      }
    }

    localStorage[this._dbName] = JSON.stringify(data);
    callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
  }.bind(this));
};

b) Then convert the contents within the get() callback:

Store.prototype.remove = function (id, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = JSON.parse(localStorage[this._dbName]);
    var data = storage[this._dbName];
    var todos = data.todos;

    for (var i = 0; i < todos.length; i++) {
      if (todos[i].id == id) {
        todos.splice(i, 1);
        break;
      }
    }

    localStorage[this._dbName] = JSON.stringify(data);
    callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
    chrome.storage.local.set(storage, function() {
      callback.call(this, todos);
    }.bind(this));
  }.bind(this));
};

2. The same Read-After-Write data hazard issue previously present in the save() method is also present when removing items so you will need to update a few more places to allow for batch operations on a list of todo IDs.

a) Still in store.js, update remove():

Store.prototype.remove = function (id, callback) {
  chrome.storage.local.get(this._dbName, function(storage) {
    var data = storage[this._dbName];
    var todos = data.todos;

    var ids = [].concat(id);
    ids.forEach( function(id) {
      for (var i = 0; i < todos.length; i++) {
        if (todos[i].id == id) {
          todos.splice(i, 1);
          break;
        }
      }
    });

    chrome.storage.local.set(storage, function() {
      callback.call(this, todos);
    }.bind(this));
  }.bind(this));
};

b) In controller.js, change removeCompletedItems() to make it call removeItem() on all IDs at once:

Controller.prototype.removeCompletedItems = function () {
  this.model.read({ completed: 1 }, function (data) {
    var ids = [];
    data.forEach(function (item) {
      this.removeItem(item.id);
      ids.push(item.id);
    }.bind(this));
    this.removeItem(ids);
  }.bind(this));

  this._filter();
};

c) Finally, still in controller.js, change the removeItem() to support removing multiple items from the DOM at once, and move the _filter() call to be inside the callback:

Controller.prototype.removeItem = function (id) {
  this.model.remove(id, function () {
    var ids = [].concat(id);
    ids.forEach( function(id) {
      this.$todoList.removeChild($$('[data-id="' + id + '"]'));
    }.bind(this));
    this._filter();
  }.bind(this));
  this._filter();
};

'क्या-क्या करें' सेक्शन में मौजूद सभी आइटम हटाना

store.js में localStorage का इस्तेमाल करके, एक और तरीका है:

Store.prototype.drop = function (callback) {
  localStorage[this._dbName] = JSON.stringify({todos: []});
  callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};

इस तरीके को मौजूदा ऐप्लिकेशन में कॉल नहीं किया जा रहा है. इसलिए, अगर आपको अतिरिक्त चुनौती चाहिए, तो इसे खुद ही लागू करें. अहम जानकारी: chrome.storage.local.clear() पर एक नज़र डालें.

अपना पूरा किया गया Todo ऐप्लिकेशन लॉन्च करना

दूसरा चरण पूरा हो गया! अपने ऐप्लिकेशन को फिर से लोड करें. इसके बाद, आपको TodoMVC का पूरी तरह से काम करने वाला, Chrome के पैकेज वाला वर्शन दिखेगा.

अधिक जानकारी के लिए

इस चरण में पेश किए गए कुछ एपीआई के बारे में ज़्यादा जानकारी के लिए, यहां जाएं:

क्या आप अगले चरण पर जाने के लिए तैयार हैं? तीसरा चरण - अलार्म और सूचनाएं जोड़ना » पर जाएं