News Application Directory Structure

Warning

Some of the JavaScript code has been broken to accommodate the book layout, and may not execute as broken. It’s best to retrieve the code from the oreilly.com link above.

news
  app
    assistants
      app-assistant.js
      dashboard-assistant.js
      feedList-assistant.js
      preferences-assistant.js
      stage-assistant.js
      storyList-assistant.js
      storyView-assistant.js
    models
      cookie.js
      feeds.js
    views
      dashboard
        dashboard-scene.html
        item-info.html
      feedList
        feedRowTemplate.html
        feedListTemplate.html
        addFeed-dialog.html
        feedList-scene.html
      preferences
        preferences-scene.html
      storyList
        storyList-scene.html
        storyListTemplate.html
        storyRowTemplate.html
      storyView
        storyView-scene.html
  appinfo.json
  framework_config.json
  icon.png
  images
    cal-selector-header-gray.png
    dashboard-icon-news.png
    details-closed-arrow.png
    details-open-arrow.png
    feedlist-newitem.png
    filter-search-light-bg.png
    header-icon-news.png
    icon-rssfeed.png
    info-icon.png
    list-icon-rssfeed.png
    menu-icon-back.png
    menu-icon-forward.png
    menu-icon-web.png
    news-icon.png
    palm-drawer-background-2.png
    url-icon.png
  index.html
  resources
     es_us
       appinfo.json
       strings.json
       views
         feedList
           addFeed-dialog.html
           feedList-scene.html
         preferences
           preferences-scene.html
  sources.json
  stylesheets
    News.css

news/app/assistants/app-assistant.js

/*  AppAssistant - NEWS

    Copyright 2009 Palm, Inc.  All rights reserved.

    Responsible for app startup, handling launch points and updating news feeds.
    Major components:
    - setup; app startup including preferences, initial load of feed data
        from the Depot and setting alarms for periodic feed updates
    - handleLaunch; launch entry point for initial launch, feed update
        alarm, dashboard or banner tap
    - handleCommand; handles app menu selections

    Data structures:
    - globals; set of persistant data used throughout app
    - Feeds Model; handles all feedlist updates, db handling and default data
    - Cookies Model; handles saving and restoring preferences
    App architecture:
    - AppAssistant; handles startup, feed list management and app menu management
    - FeedListAssistant; handles feedList navigation, search, feature feed
    - StoryListAssistant; handles single feed navigation
    - StoryViewAssistant; handles single story navigation
    - PreferencesAssistant; handles preferences display and changes
    - DashboardAssistant; displays latest new story and new story count
*/

//  ---------------------------------------------------------------
//    GLOBALS
//  ---------------------------------------------------------------

//  News namespace
News = {};

// Constants
News.unreadStory = "unReadStyle";
News.versionString = "1.0";
News.MainStageName = "newsStage";
News.DashboardStageName = "newsDashboard";
News.errorNone =    "0";                    // No error, success
News.invalidFeedError = "1";                // Not RSS2, RDF (RSS1), or ATOM

// Global Data Structures

// Persistent Globals - will be saved across app launches
News.featureFeedEnable = true;              // Enables feed rotation
News.featureStoryInterval = 5000;           // Feature Interval (in ms)
News.notificationEnable = true;             // Enables notifcations
News.feedUpdateBackgroundEnable = false;    // Enable device wakeup
News.feedUpdateInterval = "00:15:00";       // Feed update interval

//  Session Globals - not saved across app launches
News.feedListChanged = false;               // Triggers update to Depot db
News.feedListUpdateInProgress = false;      // Feed update is in progress
News.featureStoryTimer = null;              // Timer for story rotations
News.dbUpdate = "";                         // Default is no update
News.wakeupTaskId = 0;                      // Id for wakeup tasks

// Setup App Menu for all scenes; all menu actions handled in
//  AppAssistant.handleCommand()
News.MenuAttr = {omitDefaultItems: true};

News.MenuModel = {
    visible: true,
    items: [
        {label: $L("About News..."), command: "do-aboutNews"},
        Mojo.Menu.editItem,
        {label: $L("Update All Feeds"), checkEnabled: true,
          command: "do-feedUpdate"},
        {label: $L("Preferences..."), command: "do-newsPrefs"},
        Mojo.Menu.helpItem
    ]
};

function AppAssistant (appController) {

}

//  -------------------------------------------------------
//  setup - all startup actions:
//    - Setup globals with preferences
//    - Set up application menu; used in every scene
//    - Open Depot and use contents for feedList
//    - Initiate alarm for first feed update

AppAssistant.prototype.setup = function() {

    // initialize the feeds model
    this.feeds = new Feeds();
    this.feeds.loadFeedDb();

    // load preferences and globals from saved cookie
    News.Cookie.initialize();

    // Set up first timeout alarm
    this.setWakeup();

};

//  -------------------------------------------------------
//  handleLaunch - called by the framework when the application is asked to launch
//    - First launch; create card stage and first first scene
//    - Update; after alarm fires to update feeds
//    - Notification; after user taps banner or dashboard
//
AppAssistant.prototype.handleLaunch = function (launchParams) {
    Mojo.Log.info("ReLaunch");

    var cardStageController =
      this.controller.getStageController(News.MainStageName);
    var appController = Mojo.Controller.getAppController();

    if (!launchParams)  {
        // FIRST LAUNCH
        // Look for an existing main stage by name.
        if (cardStageController) {
            // If it exists, just bring it to the front by focusing its window.
            Mojo.Log.info("Main Stage Exists");
            cardStageController.popScenesTo("feedList");
            cardStageController.activate();
        } else {
            // Create a callback function to set up the new main stage
            // once it is done loading. It is passed the new stage controller
            // as the first parameter.
            var pushMainScene = function(stageController) {
                stageController.pushScene("feedList", this.feeds);
            };
            Mojo.Log.info("Create Main Stage");
            var stageArguments = {name: News.MainStageName, lightweight: true};
            this.controller.createStageWithCallback(stageArguments,
                pushMainScene.bind(this), "card");
        }
    }
    else  {
        Mojo.Log.info("com.palm.app.news -- Wakeup Call", launchParams.action);
        switch (launchParams.action) {

           // UPDATE FEEDS
           case "feedUpdate"  :
               // Set next wakeup alarm
               this.setWakeup();

              // Update the feed list
              Mojo.Log.info("Update FeedList");
              this.feeds.updateFeedList();
           break;

           // NOTIFICATION
           case "notification"  :
                Mojo.Log.info("com.palm.app.news -- Notification Tap");
               if (cardStageController) {

                   // If it exists, find the appropriate story list and activate it.
                   Mojo.Log.info("Main Stage Exists");
                   cardStageController.popScenesTo("feedList");
                   cardStageController.pushScene("storyList", this.feeds.list,
                       launchParams.index);
                   cardStageController.activate();
               } else {

                   // Create a callback function to set up a new main stage,
                   // push the feedList scene and then the appropriate story list
                   var pushMainScene2 = function(stageController) {
                      stageController.pushScene("feedList", this.feeds);
                      stageController.pushScene("storyList", this.feeds.list,
                          launchParams.index);
                   };
                   Mojo.Log.info("Create Main Stage");
                   var stageArguments2 = {name: News.MainStageName,
                     lightweight: true};
                   this.controller.createStageWithCallback(stageArguments2,
                     pushMainScene2.bind(this), "card");
               }
           break;

        }
    }
};

// -----------------------------------------
// handleCommand - called to handle app menu selections
//
AppAssistant.prototype.handleCommand = function(event) {
    var stageController = this.controller.getActiveStageController();
    var currentScene = stageController.activeScene();

    if (event.type == Mojo.Event.commandEnable) {
        if (News.feedListUpdateInProgress && (event.command == "do-feedUpdate")) {
            event.preventDefault();
        }
    }

    else {

        if(event.type == Mojo.Event.command) {
            switch(event.command) {

                case "do-aboutNews":
                    currentScene.showAlertDialog({
                       onChoose: function(value) {},
                       title: $L("News — v#{version}").interpolate
                         ({version: News.versionString}),
                       message: $L("Copyright 2008-2009, Palm Inc."),
                       choices:[
                           {label:$L("OK"), value:""}
                       ]
                     });
                break;

                case "do-newsPrefs":
                    stageController.pushScene("preferences");
                break;

                case "do-feedUpdate":
                    this.feeds.updateFeedList();
                break;
            }
        }
    }
};

// ------------------------------------------------------------------------
// setWakeup - called to setup the wakeup alarm for background feed updates
//   if preferences are not set for a manual update (value of "00:00:00")
AppAssistant.prototype.setWakeup = function() {
        if (News.feedUpdateInterval !== "00:00:00")   {
            this.wakeupRequest =
              new Mojo.Service.Request("palm://com.palm.power/timeout", {
                method: "set",
                parameters: {
                    "key": "com.palm.app.news.update",
                    "in": News.feedUpdateInterval,
                    "wakeup": News.feedUpdateBackgroundEnable,
                    "uri": "palm://com.palm.applicationManager/open",
                    "params": {
                        "id": "com.palm.app.news",
                        "params": {"action": "feedUpdate"}
                    }
                },
                onSuccess:  function(response){
                    Mojo.Log.info("Alarm Set Success", response.returnValue);
                    News.wakeupTaskId = Object.toJSON(response.taskId);
                },
                onFailure:  function(response){
                    Mojo.Log.info("Alarm Set Failure",
                        response.returnValue, response.errorText);
                }
            });
           Mojo.Log.info("Set Update Timeout");
        }
};

news/app/assistants/dashboard-assistant.js

/*  Dashboard Assistant - NEWS

    Copyright 2009 Palm, Inc.  All rights reserved.

    Responsible for posting that last feed with new stories,
    including the new story count and the latest story headline.

    Arguments:
    - feedlist; News feed list
    - selectedFeedIndex; target feed

    Other than posting the new story, the dashboard will call the
    News apps handleLaunch with a "notification" action when the
    dashboard is tapped, and the dashboard window will be closed.
*/

function DashboardAssistant(feedlist, selectedFeedIndex) {
    this.list = feedlist;
    this.index = selectedFeedIndex;
    this.title = this.list[this.index].title;
    this.message = this.list[this.index].stories[0].title;
    this.count = this.list[this.index].newStoryCount;
}

DashboardAssistant.prototype.setup = function() {
    this.displayDashboard(this.title, this.message, this.count);
    this.switchHandler = this.launchMain.bindAsEventListener(this);
    this.controller.listen("dashboardinfo", Mojo.Event.tap, this.switchHandler);

    this.stageDocument = this.controller.stageController.document;
    this.activateStageHandler = this.activateStage.bindAsEventListener(this);
    Mojo.Event.listen(this.stageDocument, Mojo.Event.stageActivate,
        this.activateStageHandler);
    this.deactivateStageHandler = this.deactivateStage.bindAsEventListener(this);
    Mojo.Event.listen(this.stageDocument, Mojo.Event.stageDeactivate,
        this.deactivateStageHandler);
};

DashboardAssistant.prototype.cleanup = function() {
    // Release event listeners
    this.controller.stopListening("dashboardinfo", Mojo.Event.tap,
        this.switchHandler);
    Mojo.Event.stopListening(this.stageDocument, Mojo.Event.stageActivate,
        this.activateStageHandler);
    Mojo.Event.stopListening(this.stageDocument, Mojo.Event.stageDeactivate,
        this.deactivateStageHandler);
};

DashboardAssistant.prototype.activateStage = function() {
    Mojo.Log.info("Dashboard stage Activation");
    this.storyIndex = 0;
    this.showStory();
};

DashboardAssistant.prototype.deactivateStage = function() {
    Mojo.Log.info("Dashboard stage Deactivation");
    this.stopShowStory();
};

// Update scene contents, using render to insert the object into an HTML template
DashboardAssistant.prototype.displayDashboard = function(title, message, count) {
    var info = {title: title, message: message, count: count};
    var renderedInfo = Mojo.View.render({
        object: info,
        template: "dashboard/item-info"
    });
    var infoElement = this.controller.get("dashboardinfo");
    infoElement.innerHTML = renderedInfo;
};

DashboardAssistant.prototype.launchMain = function() {
    Mojo.Log.info("Tap to Dashboard");
    var appController = Mojo.Controller.getAppController();
    appController.assistant.handleLaunch({action:"notification",
     index:this.index});
    this.controller.window.close();
};

// showStory - rotates stories shown in dashboard panel, every 3 seconds.
DashboardAssistant.prototype.showStory = function() {
    Mojo.Log.info("Dashboard Story Rotation", this.timer, this.storyIndex);

    this.interval = 3000;
    //    If timer is null, just restart the timer and use the most recent story
    //  or the last one displayed;
    if (!this.timer)    {
        this.timer = this.controller.window.setInterval(this.showStory.bind(this),
            this.interval);
    }

    // Else, get next story in list and update the story in the dashboard display
    else {
        // replace with test for unread story
        this.storyIndex = this.storyIndex+1;
        if(this.storyIndex >= this.list[this.index].stories.length) {
            this.storyIndex = 0;
        }

        this.message = this.list[this.index].stories[this.storyIndex].title;
        this.displayDashboard(this.title, this.message, this.count);
    }
};

DashboardAssistant.prototype.stopShowStory = function() {
    if (this.timer) {
        this.controller.window.clearInterval(this.timer);
        this.timer = undefined;
    }
};

// Update dashboard scene contents - external method
DashboardAssistant.prototype.updateDashboard = function(selectedFeedIndex) {
    this.index = selectedFeedIndex;
    this.title = this.list[this.index].title;
    this.message = this.list[this.index].stories[0].title;
    this.count = this.list[this.index].newStoryCount;
    this.displayDashboard(this.title, this.message, this.count);
};

news/app/assistants/feedList-assistant.js

/*  FeedListAssistant - NEWS

    Copyright 2009 Palm, Inc.  All rights reserved.

    Main scene for News app. Includes AddDialog-assistant for handling
    feed entry and then feedlist-assistant and supporting functions.

    Major components:
    - AddDialogAssisant; Scene assistant for add feed dialog and handlers
    - FeedListAssistant; manages feedlists
    - List Handlers - delete, reorder and add feeds
    - Feature Feed - functions for rotating and showing feature stories
    - Search - functions for searching across the entire feedlist database

    Arguments:
    - feeds; Feeds object

*/


// ------------------------------------------------------------------------
// AddDialogAssistant - simple controller for adding new feeds to the list
//   when the "Add..." is selected on the feedlist. The dialog will
//   allow the user to enter the feed's url and optionally a name. When
//   the "Ok" button is tapped, the new feed will be loaded. If no errors
//   are encountered, the dialog will close otherwise the error will be
//   posted and the user encouraged to try again.
//
function AddDialogAssistant(sceneAssistant, feeds, index) {
    this.feeds = feeds;
    this.sceneAssistant = sceneAssistant;

    //  If an index is provided then this is an edit feed, not add feed
    //  so provide the existing title, url and modify the dialog title
    if (index !== undefined) {
        this.title = this.feeds.list[index].title;
        this.url = this.feeds.list[index].url;
        this.feedIndex = index;
        this.dialogTitle = $L("Edit News Feed");
    }
    else {
        this.title = "";
        this.url = "";
        this.feedIndex = null;
        this.dialogTitle = $L("Add News Feed Source");
    }
}

AddDialogAssistant.prototype.setup = function(widget) {
        this.widget = widget;

        // Set the dialog title to either Edit or Add Feed
        var addFeedTitleElement =
          this.sceneAssistant.controller.get("add-feed-title");
        addFeedTitleElement.innerHTML = this.dialogTitle;

        // Setup text field for the new feed's URL
        this.sceneAssistant.controller.setupWidget(
            "newFeedURL",
            {
                  hintText: $L("RSS or ATOM feed URL"),
                  autoFocus: true,
                  autoReplace: false,
                  textCase: Mojo.Widget.steModeLowerCase,
                  enterSubmits: false
            },
            this.urlModel = {value : this.url});

        // Setup text field for the new feed's name
        this.sceneAssistant.controller.setupWidget(
            "newFeedName",
            {
                  hintText: $L("Title (Optional)"),
                  autoReplace: false,
                  textCase: Mojo.Widget.steModeTitleCase,
                  enterSubmits: false
            },
            this.nameModel = {value : this.title});

        // Setup OK  & Cancel buttons
        //   OK button is an activity button which will be active
        //   while processing and adding feed. Cancel will just
        //   close the scene
        this.okButtonModel = {label: $L("OK"), disabled: false};
        this.sceneAssistant.controller.setupWidget("okButton",
          {type: Mojo.Widget.activityButton}, this.okButtonModel);
        this.okButtonActive = false;
        this.okButton = this.sceneAssistant.controller.get("okButton");
        this.checkFeedHandler = this.checkFeed.bindAsEventListener(this);
        this.sceneAssistant.controller.listen("okButton", Mojo.Event.tap,
          this.checkFeedHandler);

        this.cancelButtonModel = {label: $L("Cancel"), disabled: false};
        this.sceneAssistant.controller.setupWidget("cancelButton",
          {type: Mojo.Widget.defaultButton}, this.cancelButtonModel);
        this.sceneAssistant.controller.listen("cancelButton", Mojo.Event.tap,
          this.widget.mojo.close);
};

// checkFeed  - called when OK button is clicked implying a valid feed URL
// has been entered.
AddDialogAssistant.prototype.checkFeed = function() {

        if (this.okButtonActive === true)  {
            // Shouldn't happen, but log event if it does and exit
            Mojo.Log.info("Multiple Check Feed requests");
            return;
        }

        // Check entered URL and name to confirm a valid and supported feedlist
        Mojo.Log.info("New Feed URL Request: ", this.urlModel.value);

        // Check for "http://" on front or other prefix; assume any string of
        // 1 to 5 alpha characters followed by ":" is ok, else prepend "http://"
        var url = this.urlModel.value;
        if (/^[a-z]{1,5}:/.test(url) === false)    {
            // Strip any leading slashes
            url = url.replace(/^/{1,2}/,"");
            url = "http://"+url;
        }

        // Update the entered URL & model
        this.urlModel.value = url;
        this.sceneAssistant.controller.modelChanged(this.urlModel);

        // If the url is the same, then assume that it's just a title change,
        // update the feed title and close the dialog. Otherwise update the feed.
        if (this.feedIndex && this.feeds.list[this.feedIndex].url ==
          this.urlModel.value) {
            this.feeds.list[this.feedIndex].title = this.nameModel.value;
            this.sceneAssistant.feedWgtModel.items = this.feeds.list;
            this.sceneAssistant.controller.modelChanged(
              this.sceneAssistant.feedWgtModel);
            this.widget.mojo.close();
        }
        else {

            this.okButton.mojo.activate();
            this.okButtonActive = true;
            this.okButtonModel.label = "Updating Feed";
            this.okButtonModel.disabled = true;
            this.sceneAssistant.controller.modelChanged(this.okButtonModel);

            var request = new Ajax.Request(url, {
                method: "get",
                evalJSON: "false",
                onSuccess: this.checkSuccess.bind(this),
                onFailure: this.checkFailure.bind(this)
            });
        }
};

// checkSuccess - Ajax request success
AddDialogAssistant.prototype.checkSuccess = function(transport) {
    Mojo.Log.info("Valid URL - HTTP Status", transport.status);

    // DEBUG - Work around due occasional Ajax XML error in response.
    if (transport.responseXML === null && transport.responseText !== null) {
            Mojo.Log.info("Request not in XML format - manually converting");
            transport.responseXML =
              new DOMParser().parseFromString(transport.responseText, "text/xml");
     }

    var feedError = News.errorNone;

    //  If a new feed, push the entered feed data on to the feedlist and
    //  call processFeed to evaluate it.
    if (this.feedIndex === null) {
        this.feeds.list.push({title:this.nameModel.value, url:this.urlModel.value,
            type:"", value:false, numUnRead:0, stories:[]});
        // processFeed - index defaults to last entry
        feedError = this.feeds.processFeed(transport);
    }
    else    {
        this.feeds.list[this.feedIndex] = {title:this.nameModel.value,
          url:this.urlModel.value,
            type:"", value:false, numUnRead:0, stories:[]};
        feedError = this.feeds.processFeed(transport, this.feedIndex);
    }

    // If successful processFeed returns errorNone
    if (feedError === News.errorNone)    {
        // update the widget, save the DB and exit
        this.sceneAssistant.feedWgtModel.items = this.feeds.list;
        this.sceneAssistant.controller.modelChanged(this.sceneAssistant.feedWgtModel);
        this.feeds.storeFeedDb();
        this.widget.mojo.close();
    }
    else    {
        // Feed can't be processed - remove it but keep the dialog open
        this.feeds.list.pop();
        if (feedError == News.invalidFeedError)    {
            Mojo.Log.warn("Feed ",
                this.urlModel.value, " isn't a supported feed type.");
            var addFeedTitleElement = this.controller.get("add-feed-title");
            addFeedTitleElement.innerHTML = $L("Invalid Feed Type - Please Retry");
        }

        this.okButton.mojo.deactivate();
        this.okButtonActive = false;
        this.okButtonModel.buttonLabel = "OK";
        this.okButtonModel.disabled = false;
        this.sceneAssistant.controller.modelChanged(this.okButtonModel);
    }
};

// checkFailure  - Ajax request failure
AddDialogAssistant.prototype.checkFailure = function(transport) {
    // Log error and put message in status area
    Mojo.Log.info("Invalid URL - HTTP Status", transport.status);
    var addFeedTitleElement = this.controller.get("add-feed-title");
    addFeedTitleElement.innerHTML = $L("Invalid Feed Type - Please Retry");
};

// cleanup  - remove listeners
AddDialogAssistant.prototype.cleanup = function() {
    this.sceneAssistant.controller.stopListening("okButton", Mojo.Event.tap,
        this.checkFeedHandler);
    this.sceneAssistant.controller.stopListening("cancelButton", Mojo.Event.tap,
        this.widget.mojo.close);
};



//    ---------------------------------------------------------------------------
//
//    FeedListAssistant - main scene handler for news feedlists
//
function FeedListAssistant(feeds) {
    this.feeds = feeds;
    this.appController = Mojo.Controller.getAppController();
    this.stageController = this.appController.getStageController(News.MainStageName);
}

FeedListAssistant.prototype.setup =  function() {

    // Setup App Menu
    this.controller.setupWidget(Mojo.Menu.appMenu, News.MenuAttr, News.MenuModel);

    // Setup the search filterlist and handlers;
    this.controller.setupWidget("startSearchField",
        {
            itemTemplate: "storyList/storyRowTemplate",
            listTemplate: "storyList/storyListTemplate",
            filterFunction: this.searchList.bind(this),
            renderLimit: 70,
            delay: 350
        },
        this.searchFieldModel = {
            disabled: false
        });

    this.viewSearchStoryHandler = this.viewSearchStory.bindAsEventListener(this);
    this.controller.listen("startSearchField", Mojo.Event.listTap,
        this.viewSearchStoryHandler);
    this.searchFilterHandler = this.searchFilter.bindAsEventListener(this);
    this.controller.listen("startSearchField", Mojo.Event.filter,
        this.searchFilterHandler, true);

    // Setup header, drawer, scroller and handler for feature feeds

    this.featureDrawerHandler = this.toggleFeatureDrawer.bindAsEventListener(this);
    this.controller.listen("featureDrawer", Mojo.Event.tap,
        this.featureDrawerHandler);

    this.controller.setupWidget("featureFeedDrawer", {},
      this.featureFeedDrawer = {open:News.featureFeedEnable});

    this.featureScrollerModel = {
        scrollbars: false,
        mode: "vertical"
        };

    this.controller.setupWidget("featureScroller", this.featureScrollerModel);
    this.readFeatureStoryHandler =
      this.readFeatureStory.bindAsEventListener(this);
    this.controller.listen("featureStoryDiv", Mojo.Event.tap,
        this.readFeatureStoryHandler);

            // If feature story is enabled, then set the icon to open
    if (this.featureFeedDrawer.open === true) {
        this.controller.get("featureDrawer").className = "featureFeed-open";
    } else  {
        this.controller.get("featureDrawer").className = "featureFeed-close";
    }

    // Setup the feed list, but it's empty
    this.controller.setupWidget("feedListWgt",
         {
            itemTemplate:"feedList/feedRowTemplate",
            listTemplate:"feedList/feedListTemplate",
            addItemLabel:$L("Add..."),
            swipeToDelete:true,
            renderLimit: 40,
            reorderable:true
        },
        this.feedWgtModel = {items: this.feeds.list});

    // Setup event handlers: list selection, add, delete and reorder feed entry
    this.showFeedHandler = this.showFeed.bindAsEventListener(this);
    this.controller.listen("feedListWgt", Mojo.Event.listTap,
        this.showFeedHandler);
    this.addNewFeedHandler = this.addNewFeed.bindAsEventListener(this);
    this.controller.listen("feedListWgt", Mojo.Event.listAdd,
        this.addNewFeedHandler);
    this.listDeleteFeedHandler = this.listDeleteFeed.bindAsEventListener(this);
    this.controller.listen("feedListWgt", Mojo.Event.listDelete,
        this.listDeleteFeedHandler);
    this.listReorderFeedHandler = this.listReorderFeed.bindAsEventListener(this);
    this.controller.listen("feedListWgt", Mojo.Event.listReorder,
        this.listReorderFeedHandler);

    // Setup spinner for feedlist updates
    this.controller.setupWidget("feedSpinner", {property: "value"});

    // Setup listeners for minimize/maximize events
    this.activateWindowHandler = this.activateWindow.bindAsEventListener(this);
    Mojo.Event.listen(this.controller.stageController.document,
      Mojo.Event.activate, this.activateWindowHandler);
    this.deactivateWindowHandler = this.deactivateWindow.bindAsEventListener(this);
    Mojo.Event.listen(this.controller.stageController.document,
      Mojo.Event.deactivate, this.deactivateWindowHandler);

    // Setup up feature story index to first story of the first feed
    this.featureIndexFeed = 0;
    this.featureIndexStory = 0;
};

// activate - handle portrait/landscape orientation, feature feed layout and rotation
FeedListAssistant.prototype.activate =  function() {

    // Set Orientation to free to allow rotation
    if (this.controller.stageController.setWindowOrientation) {
        this.controller.stageController.setWindowOrientation("free");
    }

    if (News.feedListChanged === true)    {
        this.feedWgtModel.items = this.feeds.list;
        this.controller.modelChanged(this.feedWgtModel, this);
        this.controller.modelChanged(this.searchFieldModel, this);

        // Don't update the database here; it's slow enough that it lags the UI;
        // wait for a feature story update to mask the update effect
    }

    // If there's some stories in the feed list, then start
    // the story rotation even if the featureFeed is disabled as we'll use
    // the rotation timer to update the DB
    if(this.feeds.list[this.featureIndexFeed].stories.length > 0) {
        var splashScreenElement = this.controller.get("splashScreen");
        splashScreenElement.hide();
        this.showFeatureStory();
    }
};

// deactivate - always turn off feature timer
FeedListAssistant.prototype.deactivate =  function() {
    Mojo.Log.info("FeedList deactivating");
    this.clearTimers();
};

// cleanup - always turn off timers, and save this.feeds.list contents
FeedListAssistant.prototype.cleanup =  function() {
    Mojo.Log.info("FeedList cleaning up");

    // Save the feed list on close, as a precaution; shouldn't be needed;
    //don't wait for results
    this.feeds.storeFeedDb();

    // Clear feature story timer and activity indicators
    this.clearTimers();

    // Remove event listeners
    this.controller.stopListening("startSearchField", Mojo.Event.listTap,
        this.viewSearchStoryHandler);
    this.controller.stopListening("startSearchField", Mojo.Event.filter,
        this.searchFilterHandler, true);
    this.controller.stopListening("featureDrawer", Mojo.Event.tap,
        this.featureDrawerHandler);
    this.controller.stopListening("feedListWgt", Mojo.Event.listTap,
        this.showFeedHandler);
    this.controller.stopListening("feedListWgt", Mojo.Event.listAdd,
        this.addNewFeedHandler);
    this.controller.stopListening("feedListWgt", Mojo.Event.listDelete,
        this.listDeleteFeedHandler);
    this.controller.stopListening("feedListWgt", Mojo.Event.listReorder,
        this.listReorderFeedHandler);
    Mojo.Event.stopListening(this.controller.stageController.document,
      Mojo.Event.activate, this.activateWindowHandler);
    Mojo.Event.stopListening(this.controller.stageController.document,
      Mojo.Event.deactivate, this.deactivateWindowHandler);
};

FeedListAssistant.prototype.activateWindow = function() {
    Mojo.Log.info("Activate Window");
    this.feedWgtModel.items = this.feeds.list;
    this.controller.modelChanged(this.feedWgtModel);

    // If stories exist in the this.featureIndexFeed, start the rotation
    // if not started
    if ((this.feeds.list[this.featureIndexFeed].stories.length > 0) &&
     (News.featureStoryTimer === null)) {
        var splashScreenElement = this.controller.get("splashScreen");
        splashScreenElement.hide();
        this.showFeatureStory();
    }
};

FeedListAssistant.prototype.deactivateWindow = function() {
    Mojo.Log.info("Deactivate Window");
    this.clearTimers();
};

// ------------------------------------------------------------------------
// List functions for Delete, Reorder and Add
//
// listDeleteFeed - triggered by deleting a feed from the list and updates
// the feedlist to reflect the deletion
//
FeedListAssistant.prototype.listDeleteFeed =  function(event) {
    Mojo.Log.info("News deleting ", event.item.title,".");

    var deleteIndex = this.feeds.list.indexOf(event.item);
    this.feeds.list.splice(deleteIndex, 1);
    News.feedListChanged = true;

    // Adjust the feature story index if needed:
    // - feed that falls before feature story feed is deleted
    // - feature story feed itself is deleted (default back to first feed)
    if (deleteIndex == this.featureIndexFeed)    {
        this.featureIndexFeed = 0;
        this.featureIndexStory = 0;
    } else    {
        if (deleteIndex < this.featureIndexFeed)    {
            this.featureIndexFeed--;
        }
    }
};

// listReorderFeed - triggered re-ordering feed list and updates the
// feedlist to reflect the changed order
FeedListAssistant.prototype.listReorderFeed =  function(event) {
    Mojo.Log.info("com.palm.app.news - News moving ", event.item.title,".");

    var fromIndex = this.feeds.list.indexOf(event.item);
    var toIndex = event.toIndex;
    this.feeds.list.splice(fromIndex, 1);
    this.feeds.list.splice(toIndex, 0, event.item);
    News.feedListChanged = true;

    // Adjust the feature story index if needed:
    // - feed that falls after featureIndexFeed is moved before it
    // - feed before is moved after
    // - the feature story feed itself is moved
    if (fromIndex > this.featureIndexFeed && toIndex <= this.featureIndexFeed) {
        this.featureIndexFeed++;
    } else {
      if (fromIndex < this.featureIndexFeed && toIndex > this.featureIndexFeed) {
          this.featureIndexFeed--;
      }    else    {
            if (fromIndex == this.featureIndexFeed)    {
                this.featureIndexFeed = toIndex;
           }
      }
    }
};

// addNewFeed - triggered by "Add..." item in feed list
FeedListAssistant.prototype.addNewFeed = function() {

        this.controller.showDialog({
            template: "feedList/addFeed-dialog",
            assistant: new AddDialogAssistant(this, this.feeds)
        });

};

// ------------------------------------------------------------------------------
// clearTimers - clears timers used in this scene when exiting the scene
FeedListAssistant.prototype.clearTimers = function()    {
    if(News.featureStoryTimer !== null) {
        this.controller.window.clearInterval(News.featureStoryTimer);
        News.featureStoryTimer = null;
    }

    // Clean up any active update spinners
    for (var i=0; i<this.feeds.list.length; i++) {
            this.feeds.list[i].value = false;
    }
    this.controller.modelChanged(this.feedWgtModel);

};

// ---------------------------------------------------------------------
// considerForNotification - called by the framework when a notification
//  is issued; look for notifications of feed updates and update the
//  feedWgtModel to reflect changes, update the feed's spinner model
FeedListAssistant.prototype.considerForNotification = function(params){
    if (params && (params.type == "update"))    {
        this.feedWgtModel.items = this.feeds.list;
        this.feeds.list[params.feedIndex].value = params.update;
        this.controller.modelChanged(this.feedWgtModel);

        // If stories exist, start the rotation if not started
        if ((this.feeds.list[this.featureIndexFeed].stories.length > 0) &&
         (News.featureStoryTimer === null)) {
            var splashScreenElement = this.controller.get("splashScreen");
            splashScreenElement.hide();
            this.showFeatureStory();

        }
    }
    return undefined;
};

// -----------------------------------------------------------------------------
// Feature story functions
//
// showFeatureStory - simply rotate the stories within the
// featured feed, which the user can set in their preferences.
FeedListAssistant.prototype.showFeatureStory = function() {

    // If timer is null, either initial story or restarting. Start with
    // previous story..
    if (News.featureStoryTimer === null)    {
        News.featureStoryTimer =
          this.controller.window.setInterval(this.showFeatureStory.bind(this),
            News.featureStoryInterval);
    }

    else {
        this.featureIndexStory = this.featureIndexStory+1;
        if(this.featureIndexStory >=
          this.feeds.list[this.featureIndexFeed].stories.length) {
            this.featureIndexStory = 0;
            this.featureIndexFeed = this.featureIndexFeed+1;
            if (this.featureIndexFeed >= this.feeds.list.length)    {
                this.featureIndexFeed = 0;
            }
        }
    }

    var summary = this.feeds.list[this.featureIndexFeed].
      stories[this.featureIndexStory].text.replace(/(<([^>]+)>)/ig,"");
    summary = summary.replace(/http:S+/ig,"");
    var featureStoryTitleElement = this.controller.get("featureStoryTitle");
    featureStoryTitleElement.innerHTML = 
      unescape(this.feeds.list[this.featureIndexFeed].
      stories[this.featureIndexStory].title);
    var featureStoryElement = this.controller.get("featureStory");
    featureStorySummaryElement.innerHTML = summary;

    // Because this is periodic and not tied to a screen transition, use
    // this to update the db when changes have been made

    if (News.feedListChanged === true)    {
        this.feeds.storeFeedDb();
        News.feedListChanged = false;
    }

};

// readFeatureStory - handler when user taps on feature story; will push storyView
//  with the current feature story.
FeedListAssistant.prototype.readFeatureStory = function() {
    this.stageController.pushScene("storyView",
      this.feeds.list[this.featureIndexFeed], this.featureIndexStory);
};

// toggleFeatureDrawer - handles taps to the featureFeed drawer. Toggle
//   drawer and icon class to reflect drawer state.
FeedListAssistant.prototype.toggleFeatureDrawer =  function(event) {
       var featureDrawer = this.controller.get("featureDrawer");
       if (this.featureFeedDrawer.open === true) {
           this.featureFeedDrawer.open = false;
           News.featureFeedEnable = false;
           featureDrawer.className = "featureFeed-close";
       } else {
           this.featureFeedDrawer.open = true;
           News.featureFeedEnable = true;
           featureDrawer.className = "featureFeed-open";
       }
       this.controller.modelChanged(this.featureFeedDrawer);
       News.Cookie.storeCookie();                // Update News saved preferences
};

// ---------------------------------------------------------------------
// Search Functions
//
// searchFilter - triggered by entry into search field. First entry will
//  hide the main feedList scene - clearing the entry will restore the scene.
//
FeedListAssistant.prototype.searchFilter = function(event)    {
    Mojo.Log.info("Got search filter: ", event.filterString);
    var feedListMainElement = this.controller.get("feedListMain");
    if (event.filterString !== "")    {
        //    Hide rest of feedList scene to make room for search results
        feedListMainElement.hide();
    }    else    {
        //    Restore scene when search string is null
        feedListMainElement.show();
    }

};

// viewSearchStory - triggered by tapping on an entry in the search results
// list  will push the storyView scene with the tapped story.
//
FeedListAssistant.prototype.viewSearchStory = function(event)    {
    var searchList = {title: $L("Search for: ")+this.filter,
      stories: this.entireList};
    var storyIndex = this.entireList.indexOf(event.item);

    Mojo.Log.info("Search display selected story with title = ",
        searchList.title, "; Story index - ", storyIndex);
    this.stageController.pushScene("storyView", searchList, storyIndex);

};

// searchList - filter function called from search field widget to update the
//  results list. This function will build results list by matching the
//  filterstring to the story titles and text content, and then return the
//  subset of the list based on offset and size requested by the widget.
//
FeedListAssistant.prototype.searchList = function(filterString, listWidget,
   offset, count)    {

    var subset = [];
    var totalSubsetSize = 0;

    this.filter = filterString;

    //    If search string is null, return empty list, else build results
    if (filterString !== "")    {

        // Search database for stories with the search string; push matches
        var items = [];

        // Comparison function for matching strings in next for loop
        var hasString = function(query, s) {
            if(s.text.toUpperCase().indexOf(query.toUpperCase())>=0) {
                return true;
            }
            if(s.title.toUpperCase().indexOf(query.toUpperCase())>=0) {
                return true;
            }
            return false;
        };

        for (var i=0; i<this.feeds.list.length; i++) {
            for (var j=0; j<this.feeds.list[i].stories.length; j++) {
                if(hasString(filterString, this.feeds.list[i].stories[j])) {
                    var sty = this.feeds.list[i].stories[j];
                    items.push(sty);
                }
            }
        }

    this.entireList = items;
    Mojo.Log.info("Search list asked for items: filter=",
        filterString, " offset=", offset, " limit=", count);

    // Cut down results to just the window asked for by the widget
    var cursor = 0;
        while (true) {
            if (cursor >= this.entireList.length) {
                break;
            }

            if (subset.length < count && totalSubsetSize >= offset) {
                subset.push(this.entireList[cursor]);
            }
            totalSubsetSize++;
            cursor++;
        }
    }

    // Update List
    listWidget.mojo.noticeUpdatedItems(offset, subset);

    // Update filter field count of items found
    listWidget.mojo.setLength(totalSubsetSize);
    listWidget.mojo.setCount(totalSubsetSize);

};

// ------------------------------------------------------------------------------
// Show feed and popup menu handler
//
// showFeed - triggered by tapping a feed in the this.feeds.list.
//   Detects taps on the unReadCount icon; anywhere else,
//   the scene for the list view is pushed. If the icon is tapped,
//   put up a submenu for the feedlist options
FeedListAssistant.prototype.showFeed = function(event) {
        var target = event.originalEvent.target.id;
        if (target !== "info") {
          this.stageController.pushScene("storyList", this.feeds.list,
            event.index);
        }
        else  {
            var myEvent = event;
            var findPlace = myEvent.originalEvent.target;
            this.popupIndex = event.index;
            this.controller.popupSubmenu({
              onChoose:  this.popupHandler,
              placeNear: findPlace,
              items: [
                {label: $L("All Unread"), command: "feed-unread"},
                {label: $L("All Read"), command: "feed-read"},
                {label: $L("Edit Feed"), command: "feed-edit"},
                {label: $L("New Card"), command: "feed-card"}
                ]
              });
        }
};

// popupHandler - choose function for feedPopup
FeedListAssistant.prototype.popupHandler = function(command) {
        var popupFeed=this.feeds.list[this.popupIndex];
        switch(command) {
            case "feed-unread":
                Mojo.Log.info("Popup - unread for feed:", popupFeed.title);

                for (var i=0; i<popupFeed.stories.length; i++ ) {
                    popupFeed.stories[i].unreadStyle = News.unreadStory;
                }
                popupFeed.numUnRead = popupFeed.stories.length;
                this.controller.modelChanged(this.feedWgtModel);
                break;

            case "feed-read":
                Mojo.Log.info("Popup - read for feed:", popupFeed.title);
                for (var j=0; j<popupFeed.stories.length; j++ ) {
                    popupFeed.stories[j].unreadStyle = "";
                }
                popupFeed.numUnRead = 0;
                this.controller.modelChanged(this.feedWgtModel);
                break;

            case "feed-edit":
                Mojo.Log.info("Popup edit for feed:", popupFeed.title);
                this.controller.showDialog({
                    template: "feedList/addFeed-dialog",
                    assistant: new AddDialogAssistant(this, this.feeds,
                      this.popupIndex)
                });
                break;

            case "feed-card":
                Mojo.Log.info("Popup tear off feed to new card:",
                  popupFeed.title);

                var newCardStage = "newsCard"+this.popupIndex;
                var cardStage = this.appController.getStageController(newCardStage);
                var feedList = this.feeds.list;
                var feedIndex = this.popupIndex;
                if(cardStage) {
                    Mojo.Log.info("Existing Card Stage");
                    cardStage.popScenesTo();
                    cardStage.pushScene("storyList", this.feeds.list, feedIndex);
                    cardStage.activate();
                } else  {
                    Mojo.Log.info("New Card Stage");
                    var pushStoryCard = function(stageController){
                        stageController.pushScene("storyList", feedList, feedIndex);
                    };
                    this.appController.createStageWithCallback({
                          name: newCardStage,
                          lightweight: true
                        },
                        pushStoryCard, "card");
                }
                break;

       }
};

news/app/assistants/preferences-assistant.js

/*  Preferences - NEWS

    Copyright 2009 Palm, Inc.  All rights reserved.

    Preferences - Handles preferences scene, where the user can:
      - select the featured feed rotation interval
      - select the interval for feed updates
      - enable or disable background feed notifications
      - enable or disable device wakeup

    App Menu is disabled in this scene.

*/

function PreferencesAssistant() {

}

PreferencesAssistant.prototype.setup = function() {

    // Setup Integer Picker to pick feature feed rotation interval
    this.controller.setupWidget("featureFeedDelay",
        {
            label:    $L("Rotation (in seconds)"),
            modelProperty:    "value",
            min: 1,
            max: 20
        },
        this.featureDelayModel = {
            value : News.featureStoryInterval/1000
        });

    this.changeFeatureDelayHandler =
      this.changeFeatureDelay.bindAsEventListener(this);
    this.controller.listen("featureFeedDelay", Mojo.Event.propertyChange,
        this.changeFeatureDelayHandler);

    // Setup list selector for UPDATE INTERVAL
    this.controller.setupWidget("feedCheckIntervalList",
        {
            label: $L("Interval"),
            choices: [
                {label: $L("Manual Updates"),   value: "00:00:00"},
                {label: $L("5 Minutes"),        value: "00:05:00"},
                {label: $L("15 Minutes"),       value: "00:15:00"},
                {label: $L("1 Hour"),           value: "01:00:00"},
                {label: $L("4 Hours"),          value: "04:00:00"},
                {label: $L("1 Day"),            value: "23:59:59"}
            ]
        },
        this.feedIntervalModel = {
            value : News.feedUpdateInterval
        });

    this.changeFeedIntervalHandler =
      this.changeFeedInterval.bindAsEventListener(this);
    this.controller.listen("feedCheckIntervalList", Mojo.Event.propertyChange,
        this.changeFeedIntervalHandler);

    // Toggle for enabling notifications for new stories during feed updates
    this.controller.setupWidget("notificationToggle",
        {},
        this.notificationToggleModel = {
            value: News.notificationEnable
        });
    this.changeNotificationHandler =
      this.changeNotification.bindAsEventListener(this);
    this.controller.listen("notificationToggle", Mojo.Event.propertyChange,
        this.changeNotificationHandler);

    // Toggle for enabling feed updates while the device is asleep
    this.controller.setupWidget("bgUpdateToggle",
        {},
        this.bgUpdateToggleModel = {
            value: News.feedUpdateBackgroundEnable
        });

    this.changeBgUpdateHandler =
      this.changeBgUpdate.bindAsEventListener(this);
    this.controller.listen("bgUpdateToggle", Mojo.Event.propertyChange,
        this.changeBgUpdate);
};

// Deactivate - save News preferences and globals
PreferencesAssistant.prototype.deactivate = function() {
    News.Cookie.storeCookie();
};

// Cleanup - remove listeners
PreferencesAssistant.prototype.cleanup = function() {
this.controller.stopListening("featureFeedDelay",
        Mojo.Event.propertyChange, this.changeFeatureDelayHandler);
    this.controller.stopListening("feedCheckIntervalList",
        Mojo.Event.propertyChange, this.changeFeedIntervalHandler);
    this.controller.stopListening("notificationToggle",
        Mojo.Event.propertyChange, this.changeNotificationHandler);
    this.controller.stopListening("bgUpdateToggle",
        Mojo.Event.propertyChange, this.changeBgUpdate); };

//    changeFeatureDelay - Handle changes to the feature feed interval
PreferencesAssistant.prototype.changeFeatureDelay = function(event) {
    Mojo.Log.info("Preferences Feature Delay Handler; value = ",
        this.featureDelayModel.value);

    //  Interval is in milliseconds
    News.featureStoryInterval = this.featureDelayModel.value*1000;

    // If timer is active, restart with new value
    if(News.featureStoryTimer !== null) {
        this.controller.window.clearInterval(News.featureStoryTimer);
        News.featureStoryTimer = null;
    }
};

//    changeFeedInterval    - Handle changes to the feed update interva;
PreferencesAssistant.prototype.changeFeedInterval = function(event) {
    Mojo.Log.info("Preferences Feed Interval Handler; value = ",
        this.feedIntervalModel.value);
    News.feedUpdateInterval = this.feedIntervalModel.value;
};

//    changeNotification - disables/enables notifications
PreferencesAssistant.prototype.changeNotification = function(event) {
    Mojo.Log.info("Preferences Notification Toggle Handler; value = ",
        this.notificationToggleModel.value);
    News.notificationEnable = this.notificationToggleModel.value;
};

//    changeBgUpdate - disables/enables background wakeups
PreferencesAssistant.prototype.changeBgUpdate = function(event) {
    Mojo.Log.info("Preferences Background Update Toggle Handler; value = ",
        this.bgUpdateToggleModel.value);
    News.feedUpdateBackgroundEnable = this.bgUpdateToggleModel.value;
};

news/app/assistants/storyList-assistant.js

/*  StoryListAssistant - NEWS

    Copyright 2009 Palm, Inc.  All rights reserved.

    Displays the feed's stories in a list, user taps display the
    selected story in the storyView scene. Major components:
    - Setup view menu to move to next or previous feed
    - Search filter; perform keyword search within feed list
    - Story View; push story scene when a story is tapped
    - Update; handle notifications if feedlist has been updated

    Arguments:
    - feedlist; Feeds.list array of all feeds
    - selectedFeedIndex; Feed to be displayed
*/

function StoryListAssistant(feedlist, selectedFeedIndex) {
    this.feedlist = feedlist;
    this.feed = feedlist[selectedFeedIndex];
    this.feedIndex = selectedFeedIndex;
    Mojo.Log.info("StoryList entry = ", this.feedIndex);
    Mojo.Log.info("StoryList feed = " + Object.toJSON(this.feed));
}


StoryListAssistant.prototype.setup =  function() {
    this.stageController = this.controller.stageController;
    // Setup scene header with feed title and next/previous feed buttons. If
    // this is the first feed, suppress Previous menu; if last, suppress Next menu
    var    feedMenuPrev = {};
    var    feedMenuNext = {};

    if (this.feedIndex > 0)    {
        feedMenuPrev = {
            icon: "back",
            command: "do-feedPrevious"
        };
    } else    {
        // Push empty menu to force menu bar to draw on left (label is the force)
        feedMenuPrev = {icon: "", command: "", label: "  "};
    }

    if (this.feedIndex < this.feedlist.length-1)    {
        feedMenuNext = {
            iconPath: "images/menu-icon-forward.png",
            command: "do-feedNext"
        };
    } else    {
        // Push empty menu to force menu bar to draw on right (label is the force)
        feedMenuNext = {icon: "", command: "", label: "  "};
    }

    this.feedMenuModel =     {
        visible: true,
        items:     [{
            items: [
                feedMenuPrev,
                { label: this.feed.title, width: 200 },
                feedMenuNext
            ]
        }]
    };

    this.controller.setupWidget(Mojo.Menu.viewMenu,
        { spacerHeight: 0, menuClass:"no-fade" }, this.feedMenuModel);

    // Setup App Menu
    this.controller.setupWidget(Mojo.Menu.appMenu, News.MenuAttr, News.MenuModel);

    // Setup the search filterlist and handlers;
    this.controller.setupWidget("storyListSearch",
        {
            itemTemplate: "storyList/storyRowTemplate",
            listTemplate: "storyList/storyListTemplate",
            filterFunction: this.searchList.bind(this),
            renderLimit: 70,
            delay: 350
        },
        this.searchFieldModel = {
            disabled: false
        });

    this.viewSearchStoryHandler = this.viewSearchStory.bindAsEventListener(this);
    this.controller.listen("storyListSearch", Mojo.Event.listTap,
        this.viewSearchStoryHandler);
    this.searchFilterHandler = this.searchFilter.bindAsEventListener(this);
    this.controller.listen("storyListSearch", Mojo.Event.filter,
        this.searchFilterHandler, true);

    // Setup story list with standard news list templates.
    this.controller.setupWidget("storyListWgt",
        {
            itemTemplate: "storyList/storyRowTemplate",
            listTemplate: "storyList/storyListTemplate",
            swipeToDelete: false,
            renderLimit: 40,
            reorderable: false
        },
        this.storyModel = {
            items: this.feed.stories
        }
    );

    this.readStoryHandler = this.readStory.bindAsEventListener(this);
    this.controller.listen("storyListWgt", Mojo.Event.listTap,
        this.readStoryHandler);
};

StoryListAssistant.prototype.activate =  function() {
    // Update list models in case unReadCount has changed
    this.controller.modelChanged(this.storyModel);
};

StoryListAssistant.prototype.cleanup =  function() {
    // Remove event listeners
    this.controller.stopListening("storyListSearch", Mojo.Event.listTap,
        this.viewSearchStoryHandler);
    this.controller.stopListening("storyListSearch", Mojo.Event.filter,
        this.searchFilterHandler, true);
    this.controller.stopListening("storyListWgt", Mojo.Event.listTap,
        this.readStoryHandler);
};

// readStory - when user taps on displayed story, push storyView scene
StoryListAssistant.prototype.readStory = function(event) {
    Mojo.Log.info("Display selected story = ", event.item.title,
        "; Story index = ", event.index);
    this.stageController.pushScene("storyView", this.feed, event.index);
};

// handleCommand - handle next and previous commands
StoryListAssistant.prototype.handleCommand = function(event) {
    if(event.type == Mojo.Event.command) {
        switch(event.command) {
            case "do-feedNext":
                this.nextFeed();
                break;
            case "do-feedPrevious":
                this.previousFeed();
                break;
        }
    }
};

// nextFeed - Called when the user taps the next menu item
StoryListAssistant.prototype.nextFeed = function(event) {
    this.stageController.swapScene(
        {
            transition: Mojo.Transition.crossFade,
            name: "storyList"
        },
        this.feedlist,
        this.feedIndex+1);
};

// previousFeed - Called when the user taps the previous menu item
StoryListAssistant.prototype.previousFeed = function(event) {
    this.stageController.swapScene(
        {
            transition: Mojo.Transition.crossFade,
            name: "storyList"
        },
        this.feedlist,
        this.feedIndex-1);
};

// searchFilter - triggered by entry into search field. First entry will
//   hide the main storyList scene and clearing the entry will restore the scene.
StoryListAssistant.prototype.searchFilter = function(event)    {
    var storyListSceneElement = this.controller.get("storyListScene");
    if (event.filterString !== "")    {
        //    Hide rest of storyList scene to make room for search results
        storyListSceneElement.hide();
    }    else    {
        //    Restore scene when search string is null
        storyListSceneElement.show();
    }

};

// viewSearchStory - triggered by tapping on an entry in the search results list.
StoryListAssistant.prototype.viewSearchStory = function(event)    {
    var searchList =
      {title: $L("Search for: #{filter}").interpolate({filter: this.filter}),
      stories: this.entireList};

    var storyIndex = this.entireList.indexOf(event.item);

    this.stageController.pushScene("storyView", searchList, storyIndex);

};

// searchList - filter function called from search field widget to update
// results list. This function will build results list by matching the
// filterstring to story titles and text content, and return the subset
// of list based on offset and size requested by the widget.t.
StoryListAssistant.prototype.searchList = function(filterString, listWidget,
  offset, count)    {

    var subset = [];
    var totalSubsetSize = 0;

    this.filter = filterString;

    // If search string is null, then return empty list, else build results list
    if (filterString !== "")    {

        // Search database for stories with the search string
        // and push on to the items array
        var items = [];

        // Comparison function for matching strings in next for loop
        var hasString = function(query, s) {
            if(s.text.toUpperCase().indexOf(query.toUpperCase())>=0) {
                return true;
            }
            if(s.title.toUpperCase().indexOf(query.toUpperCase())>=0) {
                return true;
            }
            return false;
        };

        for (var j=0; j<this.feed.stories.length; j++) {
            if(hasString(filterString, this.feed.stories[j])) {
                var sty = this.feed.stories[j];
                items.push(sty);
            }
        }

        this.entireList = items;

        Mojo.Log.info("Search list asked for items: filter=", filterString,
            " offset=", offset, " limit=", count);

        // Cut down the list results to just the window asked for by the widget
        var cursor = 0;
        while (true) {
            if (cursor >= this.entireList.length) {
                break;
            }

            if (subset.length < count && totalSubsetSize >= offset) {
                subset.push(this.entireList[cursor]);
            }
            totalSubsetSize++;
            cursor++;
        }
    }

    // Update List
    listWidget.mojo.noticeUpdatedItems(offset, subset);

    // Update filter field count of items found
    listWidget.mojo.setLength(totalSubsetSize);
    listWidget.mojo.setCount(totalSubsetSize);
};

// considerForNotification - called when a notification is issued; if this
// feed has been changed, then update it.
StoryListAssistant.prototype.considerForNotification = function(params){
    if (params && (params.type == "update"))    {
        if ((params.feedIndex == this.feedIndex) && (params.update === false)) {
            this.storyModel.items = this.feed.stories;
            this.controller.modelChanged(this.storyModel);
        }
    }
    return undefined;
};

news/app/assistants/storyView-assistant.js

/*  StoryViewAssistant - NEWS

    Copyright 2009 Palm, Inc.  All rights reserved.

    Passed a story element, displays that element in a full scene view and offers
    options for next story (right command menu button), previous story (left
    command menu button) and to launch story URL in the browser (view menu) or
    share story via email or messaging. Major components:
    - StoryView; display story in main scene
    - Next/Previous; command menu options to go to next or previous story
    - Web; command menu option to display original story in browser
    - Share; command menu option to share story by messaging or email

    Arguments:
    - storyFeed; Selected feed from which the stories are being viewed
    - storyIndex; Index of selected story to be put into the view
*/

function StoryViewAssistant(storyFeed, storyIndex) {
    this.storyFeed = storyFeed;
    this.storyIndex = storyIndex;
}

// setup - set up menus
StoryViewAssistant.prototype.setup = function() {
    this.stageController = this.controller.stageController;

    this.storyMenuModel = {
        items: [
            {iconPath: "images/url-icon.png", command: "do-webStory"},
            {},
            {items: []},
            {},
            {icon: "send", command: "do-shareStory"}
        ]};

        if (this.storyIndex > 0)    {
           this.storyMenuModel.items[2].items.push({
             icon: "back",
             command: "do-viewPrevious"
           });
        } else {
           this.storyMenuModel.items[2].items.push({
             icon: "",
              command: "",
              label: "  "
            });
        }

        if (this.storyIndex < this.storyFeed.stories.length-1)    {
            this.storyMenuModel.items[2].items.push({
              icon: "forward",
              command: "do-viewNext"}
            );
        } else {
            this.storyMenuModel.items[2].items.push({
              icon: "",
              command: "",
              label: "  "
            });
        }

    this.controller.setupWidget(Mojo.Menu.commandMenu, undefined,
      this.storyMenuModel);

    // Setup App Menu
    this.controller.setupWidget(Mojo.Menu.appMenu, News.MenuAttr, News.MenuModel);

    // Update story title in header and summary
    var storyViewTitleElement = this.controller.get("storyViewTitle");
    var storyViewSummaryElement = this.controller.get("storyViewSummary");
    storyViewTitleElement.innerHTML = this.storyFeed.stories[this.storyIndex].title;
    storyViewSummaryElement.innerHTML = this.storyFeed.stories[this.storyIndex].text;

};

// activate - display selected story
StoryViewAssistant.prototype.activate = function(event) {
    Mojo.Log.info("Story View Activated");

    // Update unreadStyle string and unReadCount in case it's changed
    if (this.storyFeed.stories[this.storyIndex].unreadStyle == News.unreadStory) {
        this.storyFeed.numUnRead--;
        this.storyFeed.stories[this.storyIndex].unreadStyle = "";
        News.feedListChanged = true;
    }

};

// ---------------------------------------------------------------------
// Handlers to go to next and previous stories, display web view
// or share via messaging or email.
StoryViewAssistant.prototype.handleCommand = function(event) {
    if(event.type == Mojo.Event.command) {
        switch(event.command) {
            case "do-viewNext":
                this.stageController.swapScene(
                    {
                        transition: Mojo.Transition.crossFade,
                        name: "storyView"
                    },
                    this.storyFeed, this.storyIndex+1);
                break;
            case "do-viewPrevious":
                this.stageController.swapScene(
                    {
                        transition: Mojo.Transition.crossFade,
                        name: "storyView"
                    },
                    this.storyFeed, this.storyIndex-1);
                break;
            case "do-shareStory":
                var myEvent = event;
                var findPlace = myEvent.originalEvent.target;
                this.controller.popupSubmenu({
                    onChoose:  this.shareHandler,
                    placeNear: findPlace,
                    items: [
                        {label: $L("Email"), command: "do-emailStory"},
                        {label: $L("SMS/IM"), command: "do-messageStory"}
                        ]
                    });
                break;
             case "do-webStory":
                this.controller.serviceRequest(
                  "palm://com.palm.applicationManager", {
                       method: "open",
                       parameters: {
                           id: "com.palm.app.browser",
                           params: {
                             target: this.storyFeed.stories[this.storyIndex].url
                           }
                       }
                });
                break;
          }
    }
};

// shareHandler - choose function for share submenu
StoryViewAssistant.prototype.shareHandler = function(command) {
        switch(command) {
            case "do-emailStory":
                this.controller.serviceRequest(
                  "palm://com.palm.applicationManager", {
                       method: "open",
                       parameters:  {
                           id: "com.palm.app.email",
                           params: {
                            summary: $L("Check out this News story..."),
                            text: this.storyFeed.stories[this.storyIndex].url
                        }
                    }
                });
                break;
            case "do-messageStory":
                this.controller.serviceRequest(
                  "palm://com.palm.applicationManager", {
                       method: "open",
                       parameters: {
                           id: "com.palm.app.messaging",
                           params: {
                               messageText: $L("Check this out: ")
                                 +this.storyFeed.stories[this.storyIndex].url
                           }
                       }
                });
                break;
       }
};

news/app/models/cookies.js

/*  Cookie - NEWS

    Copyright 2009 Palm, Inc.  All rights reserved.

    Handler for cookieData, a stored version of News preferences.
    Will load or create cookieData, migrate preferences and update cookieData
    when called.

    Functions:
    initialize - loads or creates newsCookie; updates preferences withcontents
        of stored cookieData and migrates any preferences due version changes
    store - updates stored cookieData with current global preferences
*/

News.Cookie = ({

  initialize: function()  {
    // Update globals with preferences or create it.
    this.cookieData = new Mojo.Model.Cookie("comPalmAppNewsPrefs");
    var oldNewsPrefs = this.cookieData.get();
    if (oldNewsPrefs) {
      // If current version, just update globals & prefs
      if (oldNewsPrefs.newsVersionString == News.versionString)    {
        News.featureFeedEnable = oldNewsPrefs.featureFeedEnable;
        News.featureStoryInterval = oldNewsPrefs.featureStoryInterval;
        News.feedUpdateInterval = oldNewsPrefs.feedUpdateInterval;
        News.versionString = oldNewsPrefs.newsVersionString;
        News.notificationEnable = oldNewsPrefs.notificationEnable;
        News.feedUpdateBackgroundEnable = oldNewsPrefs.feedUpdateBackgroundEnable;
      } else {
        // migrate old preferences here on updates of News app
      }
    }

    this.storeCookie();

  },

  //  store - function to update stored cookie with global values
  storeCookie: function() {
    this.cookieData.put(    {
      featureFeedEnable: News.featureFeedEnable,
      feedUpdateInterval: News.feedUpdateInterval,
      featureStoryInterval: News.featureStoryInterval,
      newsVersionString: News.versionString,
      notificationEnable: News.notificationEnable,
      feedUpdateBackgroundEnable: News.feedUpdateBackgroundEnable
    });
  }

});

news/app/models/feeds.js

/*  Feeds - NEWS

    Copyright 2009 Palm, Inc.  All rights reserved.

    The primary data model for the News app. Feeds includes the primary
        data structure for the newsfeeds, which are structured as a list of lists:

    Feeds.list entry is:
      list[x].title           String    Title entered by user
      list[x].url             String    Feed source URL in unescaped form
      list[x].type            String    Feed type: either rdf, rss, or atom
      list[x].value           Boolean   Spinner model for feed update indicator
      list[x].numUnRead       Integer   How many stories are still unread
      list[x].newStoryCount   Integer   For each update, how many new stories
      list[x].stories         Array     Each entry is a complete story

    list.stories entry is:
       stories[y].title       String    Story title or headline
       stories[y].text        String    Story text
       stories[y].summary     String    Story text, stripped of markup
       stories[y].unreadStyle String    Null when Read
       stories[y].url         String    Story url

    Methods:
    initialize(test) - create default and test feed lists
    getDefaultList() - returns the default feed list as an array
    getTestList() - returns both the default and test feed lists as a single array
    loadFeedDb() - loads feed database depot, or creates default feed list
        if no existing depot
    processFeed(transport, index) - function to process incoming feeds that are
        XML encoded in an Ajax object and stores them in the Feeds.list. Supports
        RSS, RDF and Atom feed formats.
    storeFeedDb() - writes contents of Feeds.list array to feed database depot
    updateFeedList(index) - updates entire feed list starting with this.feedIndex.
*/

var Feeds = Class.create ({

    //  Default Feeds.list
    defaultList: [
            {
                title:"Huffington Post",
                url:"http://feeds.huffingtonpost.com/huffingtonpost/raw_feed",
                type:"atom", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"Google",
                url:"http://news.google.com/?output=atom",
                type:"atom", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"BBC News",
                url:"http://newsrss.bbc.co.uk/rss/newsonline_world_edition/
                  front_page/rss.xml",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"New York Times",
                url:"http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"MSNBC",
                url:"http://rss.msnbc.msn.com/id/3032091/device/rss/rss.xml",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"National Public Radio",
                url:"http://www.npr.org/rss/rss.php?id=1004",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"Slashdot",
                url:"http://rss.slashdot.org/Slashdot/slashdot",
                type:"rdf", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"Engadget",
                url:"http://www.engadget.com/rss.xml",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"The Daily Dish",
                url:"http://feeds.feedburner.com/andrewsullivan/rApM?format=xml",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"Guardian UK",
                url:"http://feeds.guardian.co.uk/theguardian/rss",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"Yahoo Sports",
                url:"http://sports.yahoo.com/top/rss.xml",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"ESPN",
                url:"http://sports-ak.espn.go.com/espn/rss/news",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"Ars Technica",
                url:"http://feeds.arstechnica.com/arstechnica/index?format=xml",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            },{
                title:"Nick Carr",
                url:"http://feeds.feedburner.com/roughtype/unGc",
                type:"rss", value:false, numUnRead:0, newStoryCount:0, stories:[]
            }
        ],

        // Additional test feeds
        testList: [
            {
                title:"Hacker News",
                url:"http://news.ycombinator.com/rss",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
                title:"Ken Rosenthal",
                url:"http://feeds.feedburner.com/foxsports/rss/rosenthal",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
                title:"George Packer",
                url:"http://www.newyorker.com/online/blogs/georgepacker/rss.xml",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
                title:"Palm Open Source",
                url:"http://www.palmopensource.com/tmp/news.rdf",
                type:"rdf", value:false, numUnRead:0, stories:[]
            },{
                title:"Baseball Prospectus",
                url:"http://www.baseballprospectus.com/rss/feed.xml",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
                title:"The Page",
                url:"http://feedproxy.google.com/time/thepage?format=xml",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
                title:"Salon",
                url:"http://feeds.salon.com/salon/index",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
                title:"Slate",
                url:"http://feedproxy.google.com/slate?format=xml",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
             title:"SoSH",
                url:"http://sonsofsamhorn.net/index.php?act=rssout&id=1",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
                title:"Talking Points Memo",
                url:"http://feeds.feedburner.com/talking-points-memo",
                type:"atom", value:false, numUnRead:0, stories:[]
            },{
                title:"Whatever",
                url:"http://scalzi.com/whatever/?feed=rss2",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
                title:"Baseball America",
                url:"http://www.baseballamerica.com/today/rss/rss.xml",
                type:"rss", value:false, numUnRead:0, stories:[]
            },{
                title:"Test RDF Feed",
                url:"http://foobar.blogalia.com/rdf.xml",
                type:"rdf", value:false, numUnRead:0, stories:[]
            },{
                title:"Daily Kos",
                url:"http://feeds.dailykos.com/dailykos/index.html",
                type:"rss", value:false, numUnRead:0, stories:[]
            }
        ],
    // initialize - Assign default data to the feedlist
    initialize: function(test)  {
        this.feedIndex = 0;
        if (!test)  {
            this.list = this.getDefaultList();
        } else {
            this.list = this.getTestList();
        }
    },

    // getDefaultList - returns the default feed list as an array
    getDefaultList: function() {
        var returnList = [];
        for (var i=0; i<this.defaultList.length; i++)   {
            returnList[i] = this.defaultList[i];
        }

        return returnList;
    },

    // getTestList - returns the default and tests feeds in one array
    getTestList: function() {
        var returnList = [];
        var defaultLength = this.defaultList.length;
        for (var i=0; i<defaultLength; i++)   {
            returnList[i] = this.defaultList[i];
        }

        for (var j=0; j<this.testList.length; j++)   {
            returnList[j+defaultLength] = this.testList[j];
        }

        return returnList;
    },

    // loadFeedDb - loads feed db depot, or creates it with default list
    // if it doesn't already exist
    loadFeedDb: function()  {

      // Open the database to get the most recent feed list
      // DEBUG - replace is true to recreate db every time; false for release
        this.db = new Mojo.Depot(
            {name:"feedDB", version:1, estimatedSize: 100000, replace: false},
            this.loadFeedDbOpenOk.bind(this),
            function(result) {
                Mojo.Log.warn("Can't open feed database: ", result);
            }
        );
    },

    // dbOpenOK - Callback for successful db request in setup. Get stored db or
    // fallback to using default list
    loadFeedDbOpenOk: function() {
        Mojo.Log.info("Database opened OK");
        this.db.simpleGet("feedList", this.loadFeedDbGetSuccess.bind(this),
          this.loadFeedDbUseDefault.bind(this));
    },

    // loadFeedDbGetSuccess - successful retrieval of db. Call
    //  useDefaultList if the feedlist empty or null or initiate an update
    //  to the list by calling updateFeedList.
    loadFeedDbGetSuccess: function(fl) {

        Mojo.Log.info("Database Retrieved OK");
        if (fl === null) {
            Mojo.Log.warn("Retrieved empty or null list from DB");
            this.loadFeedDbUseDefault();

        } else {
            Mojo.Log.info("Retrieved feedlist from DB");
            this.list = fl;

            // If update, then convert from older versions

            this.updateFeedList();
        }
    },

    // loadFeedDbUseDefault() - Callback for failed DB retrieval meaning no list
    loadFeedDbUseDefault: function() {
      // Couldn't get the list of feeds. Maybe its never been set up, so
      // initialize it here to the default list and then initiate an update
      // with this feed list
        Mojo.Log.warn("Database has no feed list. Will use default.");
        this.list = this.getDefaultList();
        this.updateFeedList();
    },

    // processFeed (transport, index) - process incoming feeds that
    //   are XML encoded in an Ajax object and stores them in Feeds.list.
    //   Supports RSS, RDF and Atom feed formats.
    processFeed: function(transport, index) {
      // Used to hold feed list as it's processed from the Ajax request
        var listItems = [];
      // Variable to hold feed type tags
        var feedType = transport.responseXML.getElementsByTagName("rss");

        if (index === undefined)    {
            //    Default index is at end of the list
            index = this.list.length-1;
        }

        // Determine whether RSS 2, RDF (RSS 1) or ATOM feed
        if (feedType.length > 0)    {
            this.list[index].type = "rss";
        }
        else    {
            feedType = transport.responseXML.getElementsByTagName("RDF");
            if (feedType.length > 0)    {
                this.list[index].type = "RDF";
            }
            else {
                feedType = transport.responseXML.getElementsByTagName("feed");
                if (feedType.length > 0)    {
                    this.list[index].type = "atom";
                }
                else {

                // If none of those then it can't be processed, set an error code
                // in the result, log the error and return
                Mojo.Log.warn("Unsupported feed format in feed ",
                    this.list[index].url);
                return News.invalidFeedError;
                }
            }
        }

        // Process feeds; retain title, text content and url
        switch(this.list[index].type) {
        case "atom":
            // Temp object to hold incoming XML object
            var atomEntries = transport.responseXML.getElementsByTagName("entry");
            for (var i=0; i<atomEntries.length; i++) {
                listItems[i] = {
                    title: unescape(atomEntries[i].getElementsByTagName("title").
                      item(0).textContent),
                    text: atomEntries[i].getElementsByTagName("content").
                      item(0).textContent,
                    unreadStyle: News.unreadStory,
                    url: atomEntries[i].getElementsByTagName("link").
                      item(0).getAttribute("href")
                };

            // Strip HTML from text for summary and shorten to 100 characters
            listItems[i].summary = listItems[i].text.replace(/(<([^>]+)>)/ig,"");
            listItems[i].summary = listItems[i].summary.replace(/http:S+/ig,"");
            listItems[i].summary = listItems[i].summary.replace(/#[a-z]+/ig,"{");
            listItems[i].summary =
              listItems[i].summary.replace(/({([^}]+)})/ig,"");
            listItems[i].summary =
              listItems[i].summary.replace(/digg_url .../,"");
            listItems[i].summary = unescape(listItems[i].summary);
            listItems[i].summary = listItems[i].summary.substring(0,101);
            }
            break;

        case "rss":
            // Temp object to hold incoming XML object
            var rssItems = transport.responseXML.getElementsByTagName("item");
            for (i=0; i<rssItems.length; i++) {

                listItems[i] = {
                    title: unescape(rssItems[i].getElementsByTagName("title").
                      item(0).textContent),
                    text: rssItems[i].getElementsByTagName("description").
                      item(0).textContent,
                    unreadStyle: News.unreadStory,
                    url: rssItems[i].getElementsByTagName("link").
                      item(0).textContent
                };

            // Strip HTML from text for summary and shorten to 100 characters
            listItems[i].summary = listItems[i].text.replace(/(<([^>]+)>)/ig,"");
            listItems[i].summary = listItems[i].summary.replace(/http:S+/ig,"");
            listItems[i].summary = listItems[i].summary.replace(/#[a-z]+/ig,"{");
            listItems[i].summary =
              listItems[i].summary.replace(/({([^}]+)})/ig,"");
            listItems[i].summary =
              listItems[i].summary.replace(/digg_url .../,"");
            listItems[i].summary = unescape(listItems[i].summary);
            listItems[i].summary = listItems[i].summary.substring(0,101);
            }
            break;

        case "RDF":
            // Temp object to hold incoming XML object
            var rdfItems = transport.responseXML.getElementsByTagName("item");
            for (i=0; i<rdfItems.length; i++) {

                listItems[i] = {
                    title: unescape(rdfItems[i].getElementsByTagName("title").
                      item(0).textContent),
                    text: rdfItems[i].getElementsByTagName("description").
                      item(0).textContent,
                    unreadStyle: News.unreadStory,
                    url: rdfItems[i].getElementsByTagName("link").
                      item(0).textContent
                };

            // Strip HTML from text for summary and shorten to 100 characters
            listItems[i].summary = listItems[i].text.replace(/(<([^>]+)>)/ig,"");
            listItems[i].summary = listItems[i].summary.replace(/http:S+/ig,"");
            listItems[i].summary = listItems[i].summary.replace(/#[a-z]+/ig,"{");
            listItems[i].summary =
              listItems[i].summary.replace(/({([^}]+)})/ig,"");
            listItems[i].summary =
              listItems[i].summary.replace(/digg_url .../,"");
            listItems[i].summary = unescape(listItems[i].summary);
            listItems[i].summary = listItems[i].summary.substring(0,101);
            }
            break; 
       }

        // Update read items by comparing new stories with stories last
        // in the feed. For all old stories, use the old unreadStyle value,
        // otherwise set unreadStyle to News.unreadStory.
        // Count number of unread stories and store value.
        // Determine if any new stories when URLs don't match a previously
        // downloaded story.
        //
        var numUnRead = 0;
        var newStoryCount = 0;
        var newStory = true;
        for (i = 0; i < listItems.length; i++) {
            var unreadStyle = News.unreadStory;
            var j;
            for (j=0; j<this.list[index].stories.length; j++ ) {
                if(listItems[i].url == this.list[index].stories[j].url) {
                    unreadStyle = this.list[index].stories[j].unreadStyle;
                    newStory = false;
                }
            }

            if(unreadStyle == News.unreadStory) {
                numUnRead++;
            }

            if (newStory) {
                newStoryCount++;
            }

            listItems[i].unreadStyle = unreadStyle;
        }

        // Save updated feed in global feedlist
        this.list[index].stories = listItems;
        this.list[index].numUnRead  = numUnRead;
        this.list[index].newStoryCount = newStoryCount;

        // If new feed, the user may not have entered a name; if so, set the
        // name to the feed title
        if (this.list[index].title === "")    {
            //    Will return multiple hits, but the first is the feed name
            var titleNodes = transport.responseXML.getElementsByTagName("title");
            this.list[index].title = titleNodes[0].textContent;
        }
        return News.errorNone;
    },

    // storeFeedDb() - writes contents of Feeds.list array to feed database depot
    storeFeedDb: function() {
        Mojo.Log.info("FeedList save started");
        this.db.simpleAdd("feedList", this.list,
                function() {Mojo.Log.info("FeedList saved OK");},
                this.storeFeedDBFailure);
    },


    // storeFeedDbFailure(transaction, result) - handles save failure, usually an
    //    out of memory error
    storeFeedDbFailure: function(result) {
        Mojo.Log.warn("Database save error: ", result);
    },

    // updateFeedList(index) - called to cycle through feeds. This is called
    //   once per update cycle.
    updateFeedList: function(index) {
        News.feedListUpdateInProgress = true;

        // request fresh copies of all stories
        this.currentFeed = this.list[this.feedIndex];
        this.updateFeedRequest(this.currentFeed);
    },

    // feedRequest - function called to setup and make a feed request
    updateFeedRequest: function(currentFeed) {
        Mojo.Log.info("URL Request: ", currentFeed.url);

        // Notify the chain that there is an update in progress
        Mojo.Controller.getAppController().sendToNotificationChain({
          type: "update", update: true, feedIndex: this.feedIndex});

        var request = new Ajax.Request(currentFeed.url, {
            method: "get",
            evalJSON: "false",
            onSuccess: this.updateFeedSuccess.bind(this),
            onFailure: this.updateFeedFailure.bind(this)
        });
    },

    // updateFeedFailure - Callback routine from a failed AJAX feed request;
    //   post a simple failure error message with the http status code.
    updateFeedFailure: function(transport) {
        // Prototype template to generate a string from the return status.
        var t = new Template(
          $L("Status #{status} returned from newsfeed request."));
        var m = t.evaluate(transport);

        //    Post error alert and log error
        Mojo.Log.info("Invalid feed - http failure, check feed: ", m);

        // Notify the chain that this update is complete
        Mojo.Controller.getAppController().sendToNotificationChain({
          type: "update", update: false, feedIndex: this.feedIndex});
    },

    // updateFeedSuccess - Successful AJAX feed request (feedRequest);
    //   uses this.feedIndex and this.list
    updateFeedSuccess: function(transport) {

        var t = new Template($L({key: "newsfeed.status",
          value: "Status #{status} returned from newsfeed request."}));
        Mojo.Log.info("Feed Request Success: ", t.evaluate(transport));

        // Work around due to occasional XML errors
        if (transport.responseXML === null && transport.responseText !== null) {
            Mojo.Log.info("Request not in XML format - manually converting");
            transport.responseXML =
              new DOMParser().parseFromString(transport.responseText, "text/xml");
         }

        // Process the feed, passing in transport holding the updated feed data
        var feedError = this.processFeed(transport, this.feedIndex);

        // If successful processFeed returns News.errorNone
        if (feedError == News.errorNone)    {
            var appController = Mojo.Controller.getAppController();
            var stageController =
              appController.getStageController(News.MainStageName);
            var dashboardStageController =
              appController.getStageProxy(News.DashboardStageName);

            // Post a notification if new stories and application is minimized
            if (this.list[this.feedIndex].newStoryCount > 0)   {
                Mojo.Log.info("New Stories: ",
                  this.list[this.feedIndex].title," : ",
                  this.list[this.feedIndex].newStoryCount, " New Items");
                if (!stageController.isActiveAndHasScenes() &&
                  News.notificationEnable) {
                    var bannerParams = {
                        messageText: Mojo.Format.formatChoice(
                            this.list[this.feedIndex].newStoryCount,
                            $L("0##{title} : No New Items|
                                1##{title} : 1 New Item|
                                1>##{title} : #{count} New Items"),
                            {
                              title: this.list[this.feedIndex].title,
                              count: this.list[this.feedIndex].newStoryCount
                            }
                        )
                    };

                    appController.showBanner(bannerParams, {
                        action: "notification",
                        index: this.feedIndex
                      },
                      this.list[this.feedIndex].url);

                    // Create or update dashboard
                    var feedlist = this.list;
                    var selectedFeedIndex = this.feedIndex;

                    if(!dashboardStageController) {
                        Mojo.Log.info("New Dashboard Stage");
                        var pushDashboard = function(stageController){
                            stageController.pushScene("dashboard", feedlist,
                              selectedFeedIndex);
                        };
                        appController.createStageWithCallback({
                              name: News.DashboardStageName,
                              lightweight: true
                            },
                            pushDashboard, "dashboard");
                    }
                    else {
                        Mojo.Log.info("Existing Dashboard Stage");
                        dashboardStageController.delegateToSceneAssistant(
                          "updateDashboard", selectedFeedIndex);
                    }
                }
            }
        } else     {

            // There was a feed process error; unlikely, but could happen if the
            // feed was changed by the feed service. Log the error.
            if (feedError == News.invalidFeedError)    {
                Mojo.Log.info("Feed ", this.nameModel.value,
                  " is not a supported feed type.");
            }
        }

        // Notify the chain that this update is done
        Mojo.Controller.getAppController().sendToNotificationChain({
          type: "update", update: false, feedIndex: this.feedIndex});
        News.feedListChanged = true;

        // If NOT the last feed then update the feedsource and request next feed
        this.feedIndex++;
        if(this.feedIndex < this.list.length) {
            this.currentFeed = this.list[this.feedIndex];

            // Notify the chain that there is a new update in progress
            Mojo.Controller.getAppController().sendToNotificationChain({
                type: "update",
              update: true,
              feedIndex: this.feedIndex
            });

            // Request an update for the next feed
            this.updateFeedRequest(this.currentFeed);
        } else {

            // Otherwise, this update is done. Reset index to 0 for next update
            this.feedIndex = 0;
            News.feedListUpdateInProgress = false;

        }
    }
});

news/app/assistants/views/dashboard/dashboard-scene.html

<div id="dashboardinfo" class="dashboardinfo"></div>

news/app/assistants/views/dashboard/item-info.html

<div class="dashboard-notification-module">
    <div class="palm-dashboard-icon-container">
        <div class="dashboard-newitem">
            <span>#{count}</span>
        </div>
         <div id="dashboard-icon" class="palm-dashboard-icon dashboard-icon-news">
         </div>
    </div>
    <div class="palm-dashboard-text-container">
         <div class="dashboard-title">
            #{title}
         </div>
        <div id="dashboard-text" class="palm-dashboard-text">#{message}</div>
    </div>
</div

news/app/assistants/views/feedList/addFeed-dialog.html

<div id="palm-dialog-content" class="palm-dialog-content">
    <div id="add-feed-title" class="palm-dialog-title">
        Add Feed
    </div>
    <div class="palm-dialog-separator"></div>
    <div class="textfield-group" x-mojo-focus-highlight="true">
        <div class="title">
           <div x-mojo-element="TextField" id="newFeedURL"></div>
        </div>
    </div>
    <div class="textfield-group" x-mojo-focus-highlight="true">
        <div class="title">
           <div x-mojo-element="TextField" id="newFeedName"></div>
        </div>
    </div>

    <div class="palm-dialog-buttons">
        <div x-mojo-element="Button" id="okButton">
        <div x-mojo-element="Button" id="cancelButton">
    </div>
</div>

news/app/assistants/views/feedList/feedList-scene.html

<div id="feedListScene">

    <!--    Search Field                                         -->
    <div id="searchFieldContainer">
        <div x-mojo-element="FilterList" id="startSearchField"></div>
    </div>

    <div id="feedListMain">

        <!--    Rotating Feature Story                           -->
        <div id="feedList_view_header" class="palm-header left">
            Latest News
            <div id="featureDrawer" class="featureFeed-close"></div>
        </div>
        <div class="palm-header-spacer"></div>
        <div x-mojo-element="Drawer" id="featureFeedDrawer">
            <div x-mojo-element="Scroller" id="featureScroller" >
                <div id="featureStoryDiv" class="featureScroller">
                    <div id="splashScreen" class="splashScreen">
                        <div class="update-image"></div>
                        <div class="title">News v0.8<span>#{version}</span>
                            <div class="palm-body-text">Copyright 2009, Palm®</div>
                        </div>
                    </div>
                    <div id="featureStoryTitle" class="palm-body-title">
                    </div>
                    <div id="featureStory" class="palm-body-text">
                    </div>
                </div>
            </div>
        </div>

        <!--    Feed List                                         -->
        <div class="palm-list">
            <div x-mojo-element="List" id="feedListWgt"></div>
        </div>
    </div>
</div>

news/app/assistants/views/feedList/feedListTemplate.html

<div class="palm-list">#{-listElements}</div>

news/app/assistants/views/feedList/feedRowTemplate.html

<div class="palm-row" x-mojo-touch-feedback="delayed">
  <div class="palm-row-wrapper textfield-group">
    <div class="title">

      <div class="palm-dashboard-icon-container feedlist-icon-container">
        <div class="dashboard-newitem feedlist-newitem">
          <span class="unReadCount">#{numUnRead}</span>
        </div>
        <div id="dashboard-icon" class="palm-dashboard-icon feedlist-icon">
        </div>
      </div>

      <div class="feedlist-info icon right" id="info"></div>
      <div x-mojo-element="Spinner" class="right" name="feedSpinner"</div>
      <div class="feedlist-title truncating-text">#{title}</div>
      <div class="feedlist-url truncating-text">#{-url}</div>

    </div>
  </div>
</div>

news/app/assistants/views/preferences/preferences-scene.html

<div class="palm-page-header">
    <div class="palm-page-header-wrapper">
        <div class="icon news-mini-icon"></div>
        <div class="title">News Preferences</div>
    </div>
</div>

<div class="palm-group">
    <div class="palm-group-title"><span>Feature Feed</span></div>
        <div class="palm-list">
            <div x-mojo-element="IntegerPicker" id="featureFeedDelay"></div>
        </div>
    </div>
</div>

<div class="palm-group">
    <div class="palm-group-title"><span>Feed Updates</span></div>
           <div class="palm-list">
            <div class="palm-row first">
                <div class="palm-row-wrapper">
                    <div x-mojo-element="ListSelector" id="feedCheckIntervalList">
                    </div>
                </div>
            </div>
            <div class="palm-row">
                <div class="palm-row-wrapper">
                       <div x-mojo-element="ToggleButton" id="notificationToggle">
                       </div>
                       <div class="title left">Show Notification</div>
                </div>
            </div>
            <div class="palm-row last">
                <div class="palm-row-wrapper">
                       <div x-mojo-element="ToggleButton" id="bgUpdateToggle">
                       </div>
                       <div class="title left">Wake Device</div>
                </div>
            </div>
        </div>
    </div>
</div>

news/app/assistants/views/storyList/storyList-scene.html

<div class="palm-header-spacer"></div>
<div id="storyListScene" class="storyListScene">
    <div x-mojo-element="List" id="storyListWgt" ></div>
</div>
<div class="storyList-filter">
    <div x-mojo-element="FilterList" id="storyListSearch" class="palm-list"></div>
</div>

news/app/assistants/views/storyList/storyListTemplate.html

<div class="palm-list">#{-listElements}</div>

news/app/assistants/views/storyList/storyRowTemplate.html

<div class="palm-row" x-mojo-touch-feedback="delayed">
    <div class="palm-row-wrapper">
        <div id="storyTitle" class="title truncating-text #{unreadStyle}">
            #{title}
        </div>
        <div id="storySummary" class="news-subtitle truncating-text">
            #{summary}
        </div>
    </div>
</div>

news/app/assistants/views/storyView/storyView-scene.html

<div id="storyViewScene">
    <div class="palm-page-header multi-line">
        <div class="palm-page-header-wrapper">
            <div id="storyViewTitle" class="title left">
            </div>
        </div>
    </div>
    <div class="palm-text-wrapper">
        <div id="storyViewSummary" class="palm-body-text">
        </div>
    </div>
</div>

news/appinfo.json

{
    "title": "News",
    "type": "web",
    "main": "index.html",
    "id": "com.palm.app.news11-1",
    "version": "1.0.0",
    "vendor": "Palm",
    "noWindow" : "true",
    "icon": "icon.png",
    "theme": "light"
}

news/framework_config.json

{
    "logLevel": "0",
    "timingEnabled": "true"
}

news/index.html

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
  <title>News</title>
  <script src="/usr/palm/frameworks/mojo/mojo.js" type="text/javascript"
      x-mojo-version="1"></script>
  <link href="stylesheets/News.css" media="screen" rel="stylesheet"
      type="text/css"/>
</head>

<body>
</body>

</html>

news/resources/es_us/appinfo.json

{
    "title": "Noticias",
    "type": "web",
    "main": "../../index.html",
    "id": "com.palm.app.news",
    "version": "1.0.0",
    "vendor": "Palm",
    "noWindow" : "true",
    "icon": "../../icon.png",
    "theme": "light"
}

news/resources/es_us/strings.json

{
   "#{status}" : "#{status}",
   "0##{title} : No New Items|1##{title} : 1 New Item|1>##{title} :
     #{count} New Items" :    "0##{title} : No hay elementos nuevos|1##{title} :
     1 elemento nuevo|1>##{title} : #{count} elementos nuevos",
   "1 Day" : "1 día",
   "1 Hour" : "1 hora",
   "15 Minutes" : "15 minutos",
   "4 Hours" : "4 horas",
   "5 Minutes" : "5 minutos",
   "About News..." : "Acerca de noticias...",
   "Add Feed DB save error : #{message}; can't save feed list." :
     "Error de base de datos al intentar agregar nueva fuente web : #{message};
     no se puede guardar la lista de fuentes web.",
   "Add News Feed Source" : "Añadir fuente web de noticias",
   "Add..." : "Añadir...",
   "Adding a Feed" : "Añadiendo una fuente web",
   "All Read" : "Todas leídas",
   "All Unread" : "Todas las no leídas",
   "Can't open feed database: " :
     "No se puede abrir la base de datos de fuentes web: ",
   "Cancel" : "Cancelar",
   "Cancel search" : "Cancelar búsqueda",
   "Check out this News story..." : "Leer esta noticia...",
   "Check this out: " : "Mira esto: ",
   "Copyright 2009, Palm Inc." : "Copyright 2009, Palm Inc.",
   "Database save error: " : "Error al guardar en la base de datos: ",
   "Edit a Feed" : "Editar una fuente web",
   "Edit Feed" : "Editar fuente web",
   "Edit News Feed" : "Editar una fuente web de noticias",
   "Feature Feed" : "Fuente web destacada",
   "Featured Feed" : "Fuente web destacada",
   "Feature Rotation" : "Rotación de fuente web destacada",
   "Feed Request Success:" : "Solicitud de fuente web lograda:",
   "Feed Updates" : "Actualización de fuentes web",
   "Help..." : "Ayuda...",
   "Interval" : "Intervalo",
   "Invalid Feed - not a supported feed type" :
     "Fuente web no válida: no es un tipo de fuente web admitido",
   "Latest News" : "Últimas noticias",
   "Manual Updates" : "Actualizaciones manuales",
   "Mark Read or Unread" : "Marcar leída o no leída",
   "New Card" : "Tarjeta nueva",
   "New features" : "Nuevas características",
   "New Items" : "Elementos nuevos",
   "News Help" : "Ayuda para noticias",
   "News Preferences" : "Preferencias para noticias",
   "newsfeed.status" :
     "Estado #{status} devuelto desde solicitud de fuente web de noticias",
   "OK" : "OK",
   "Optional" : "Opcional",
   "Preferences..." : "Preferencias...",
   "Reload" : "Cargar nuevamente",
   "Rotate Every" : "Girar cada",
   "Rotation (in seconds)" : "Rotación (en segundos)",
   "RSS or ATOM feed URL" : "Fuente web RSS o ATOM URL",
   "Search for:  #{filter}" : "Buscar:  #{filter}",
   "Show Notification" :"Mostrar aviso",
   "SMS/IM" : "SMS/IM",
   "Status #{status} returned from newsfeed request." :
     "La solicitud de fuente web de noticias indicó el estado #{status}.",
   "Stop" : "Detener",
   "Title" : "Título",
   "Title (Optional)" : "Título (Opcional)",
   "Update All Feeds" : "Actualizar todas las fuentes web",
   "Wake Device" : "Activar dispositivo",
   "Will need to reload on next use." :
     "Se tendrá que cargar de nuevo la próxima vez que se use."

}

news/resources/es_us/views/feedList/addFeed-dialog.html

<div id="palm-dialog-content" class="palm-dialog-content">
    <div id="add-feed-title" class="palm-dialog-title">
        Añadir fuente web
    </div>
    <div class="palm-dialog-separator"></div>
    <div class="textfield-group" x-mojo-focus-highlight="true">
        <div class="title">
           <div x-mojo-element="TextField" id="newFeedURL"></div>
        </div>
    </div>
    <div class="textfield-group" x-mojo-focus-highlight="true">
        <div class="title">
           <div x-mojo-element="TextField" id="newFeedName"></div>
        </div>
    </div>

    <div class="palm-dialog-buttons">
        <div x-mojo-element="Button" id="okButton">
        <div x-mojo-element="Button" id="cancelButton">
    </div>
</div>

news/resources/es_us/views/feedList/feedList-scene.html

<div id="feedListScene">

    <!--    Search Field                                          -->
    <div id="searchFieldContainer">
        <div x-mojo-element="FilterList" id="startSearchField"></div>
    </div>

    <div id="feedListMain">

        <!--    Rotating Feature Story                            -->
        <div id="feedList_view_header" class="palm-header left">
            Últimas noticias
            <div id="featureDrawer" class="featureFeed-close"></div>
        </div>
        <div class="palm-header-spacer"></div>
        <div x-mojo-element="Drawer" id="featureFeedDrawer">
            <div x-mojo-element="Scroller" id="featureScroller" >
                <div id="featureStoryDiv" class="featureScroller">
                    <div id="splashScreen" class="splashScreen">
                        <div class="splashImage"></div>
                        <div class="splashText">
                        Noticias v0.8<span>#{version}</span>
                            <div class="splashBody">Copyright 2009, Palm®</div>
                        </div>
                    </div>
                    <div id="featureStoryTitle" class="palm-body-title">
                    </div>
                   <div id="featureStory" class="palm-body-text">
                </div>
              </div>
           </div>
        </div>

        <!--    Feed List                                         -->
        <div class="palm-list">
            <div x-mojo-element="List" id="feedListWgt"></div>
        </div>
    </div>
</div>

news/resources/es_us/views/preferences/preferences-scene.html

<div class="palm-page-header">
    <div class="palm-page-header-wrapper">
        <div class="icon news-mini-icon"></div>
        <div class="title">Preferencias para noticias</div>
    </div>
</div>

<div class="palm-group">
    <div class="palm-group-title"><span>Fuente web destacada</span></div>
        <div class="palm-list">
            <div x-mojo-element="IntegerPicker" id="featureFeedDelay"></div>
        </div>
    </div>
</div>

<div class="palm-group">
    <div class="palm-group-title"><span>Actualización de fuentes web</span></div>
           <div class="palm-list">
            <div class="palm-row first">
                <div class="palm-row-wrapper">
                    <div x-mojo-element="ListSelector" id="feedCheckIntervalList">
                    </div>
                </div>
            </div>
            <div class="palm-row">
                <div class="palm-row-wrapper">
                     <div x-mojo-element="ToggleButton" id="notificationToggle">
                     </div>
                     <div class="title left">Mostrar aviso</div>
                </div>
            </div>
            <div class="palm-row last">
                <div class="palm-row-wrapper">
                     <div x-mojo-element="ToggleButton" id="bgUpdateToggle"></div>
                    <div class="title left">Activar dispositivo</div>
                </div>
            </div>
        </div>
    </div>
</div>

news/sources.json

[
  {
    "source": "app/assistants/app-assistant.js"
  },
  {
    "source": "app/assistants/stage-assistant.js"
  },
  {
    "source": "app/assistants/dashboard-assistant.js",
    "scenes": "dashboard"
  },
  {
    "source": "app/assistants/feedList-assistant.js",
    "scenes": "feedList"
  },
  {
    "source": "app/assistants/preferences-assistant.js",
    "scenes": "preferences"
  },
  {
    "source": "app/assistants/storyList-assistant.js",
    "scenes": "storyList"
  },
  {
    "source": "app/assistants/storyView-assistant.js",
    "scenes": "storyView"
  },
  {
    "source" : "app/models/cookies.js"
  },
  {
    "source" : "app/models/feeds.js"
  }
]

news/stylesheets/News.css

/*    News CSS

    Copyright 2009 Palm, Inc.  All rights reserved.

    App overrides of palm scene and widget styles.
*/

/* Contrains storyView content to width of scene */
img    {
    max-width:280px;
}


/* Header Styles  */
.icon.news-mini-icon {
    background: url(../images/header-icon-news.png) no-repeat;
    margin-top: 13px;
    margin-left: 17px;
}


/* FeedList Header styles for feature drawer and selection */
.featureFeed-close {
    float:right;
    margin: 8px −12px 0px 0px;
    height:35px;
    width: 35px;
    background: url(../images/details-open-arrow.png) no-repeat;
}

.featureFeed-open {
    float:right;
    margin: 8px −12px 0px 0px;
    height:35px;
    width: 35px;
    background: url(../images/details-closed-arrow.png) no-repeat;
}.palm-drawer-container {
    border-width: 20px 1px 20px 1px;
    -webkit-border-image:
      url(../images/palm-drawer-background-1.png) 20 1 20 1 repeat repeat;
    -webkit-box-sizing: border-box;
    overflow: visible;
}


/* Feature Feed styles */
.featureScroller {
    height: 100px;
    width: 280px;
    margin-left: 20px;
}


/* feedList styles */
.palm-row-wrapper.textfield-group {
    margin-top: 5px;
}

.feedlist-title {
    line-height: 2.0em;
}

.feedlist-url {
    font-size: 14px;
    color: gray;
    margin-top: −20px;
    margin-bottom: −20px;
    line-height: 16px;
}

.feedlist-info {
    background: url(../images/info-icon.png) center center no-repeat;
}

.feedlist-icon-container {
    height: 54px;
    margin-top: 5px;
}

.feedlist-icon {
    background: url(../images/list-icon-rssfeed.png) center no-repeat;
}

.feedlist-newitem   {
    line-height: 20px;
    height: 26px;
    min-width: 26px;
    -webkit-border-image:
      url(../images/feedlist-newitem.png) 4 10 4 10 stretch stretch;
    -webkit-box-sizing: border-box;
    border-width: 4px 10px 4px 10px;
}

.unReadCount {
    color: white;
}


/* Story List styles */
.news-subtitle {
        padding: 0px 14px 0px 14px;
        font-size: 14px;
        margin-top: −10px;
        line-height: 16px;
}

.palm-row-wrapper > .unReadStyle    {
    font-weight: bold;
}
.storyList-filter .filter-field-container {
    top: 48px;
    left: 0px;
    position: fixed;
    width: 100%;
    height: 48px;
    border-width: 26px 23px 20px 23px;
    -webkit-border-image:
      url(../images/filter-search-light-bg.png) 26 23 20 23 repeat repeat;
    -webkit-box-sizing: border-box;
    z-index: 11002;
}

/* Splash Screen image  */
.update-image    {
    background: url(../images/news-icon.png) center center no-repeat;
    float: left;
    height: 58px;
    width: 58px;
    margin-left: −3px;
}

/* dashboard styles */

.dashboard-icon-news {
    background: url(../images/dashboard-icon-news.png);
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.93.145