diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bb091f2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+build
+.lock-waf*
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3624447
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# Hacker News
+
+**Introducing Hacker News for Pebble! Read the front page, new, and best stories on Hacker News now right on your wrist!**
+
+![Home Menu](http://f.cl.ly/items/30431D041R1P1M1z1L05/hackernews1.png)
+![Story List](http://f.cl.ly/items/1a2b0A1U1c2d411h1O1V/hackernews2.png)
+![Story View](http://f.cl.ly/items/1z2Q3x0M0c3E1946023s/hackernews3.png)
+
+It uses HNify API to get the stories and Clipped API to summarize them.
+
+A comments view and settings (change text size) is in the works.
+
+Long click select in the story list view refreshes the list (until i figure out a better way to use it).
+
+Contributions are much welcome!
+
+###### Thanks to @matthewtole for multi-timer! (Good MenuLayer example)
diff --git a/appinfo.json b/appinfo.json
new file mode 100644
index 0000000..6f3a2db
--- /dev/null
+++ b/appinfo.json
@@ -0,0 +1,28 @@
+{
+ "uuid": "bbf7c879-a2c8-43df-8cbc-b258276698bf",
+ "shortName": "Hacker News",
+ "longName": "Hacker News",
+ "companyName": "Neal",
+ "versionCode": 1,
+ "versionLabel": "1.0.0",
+ "watchapp": {
+ "watchface": false
+ },
+ "appKeys": {
+ "endpoint": 0,
+ "index": 1,
+ "title": 2,
+ "subtitle": 3,
+ "summary": 4
+ },
+ "resources": {
+ "media": [
+ {
+ "menuIcon": true,
+ "type": "png",
+ "name": "IMAGE_MENU_ICON",
+ "file": "images/icon.png"
+ }
+ ]
+ }
+}
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..3f72388
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,4 @@
+pebble build || { exit $?; }
+if [ "$1" = "install" ]; then
+ pebble install --logs
+fi
diff --git a/resources/images/icon.png b/resources/images/icon.png
new file mode 100644
index 0000000..c11de7d
Binary files /dev/null and b/resources/images/icon.png differ
diff --git a/src/appmessage.c b/src/appmessage.c
new file mode 100644
index 0000000..2c356d4
--- /dev/null
+++ b/src/appmessage.c
@@ -0,0 +1,41 @@
+#include
+#include "appmessage.h"
+#include "common.h"
+#include "windows/storylist.h"
+
+static void in_received_handler(DictionaryIterator *iter, void *context);
+static void in_dropped_handler(AppMessageResult reason, void *context);
+static void out_sent_handler(DictionaryIterator *sent, void *context);
+static void out_failed_handler(DictionaryIterator *failed, AppMessageResult reason, void *context);
+
+void appmessage_init(void) {
+ app_message_open(128 /* inbound_size */, 32 /* outbound_size */);
+ app_message_register_inbox_received(in_received_handler);
+ app_message_register_inbox_dropped(in_dropped_handler);
+ app_message_register_outbox_sent(out_sent_handler);
+ app_message_register_outbox_failed(out_failed_handler);
+}
+
+static void in_received_handler(DictionaryIterator *iter, void *context) {
+ Tuple *endpoint_tuple = dict_find(iter, HN_KEY_ENDPOINT);
+
+ if (endpoint_tuple) {
+ if (storylist_is_on_top() && storylist_current_endpoint() == endpoint_tuple->value->int16) {
+ storylist_in_received_handler(iter);
+ } else {
+ app_message_outbox_send();
+ }
+ }
+}
+
+static void in_dropped_handler(AppMessageResult reason, void *context) {
+ APP_LOG(APP_LOG_LEVEL_DEBUG, "Incoming AppMessage from Pebble dropped, %d", reason);
+}
+
+static void out_sent_handler(DictionaryIterator *sent, void *context) {
+ // outgoing message was delivered
+}
+
+static void out_failed_handler(DictionaryIterator *failed, AppMessageResult reason, void *context) {
+ APP_LOG(APP_LOG_LEVEL_DEBUG, "Failed to send AppMessage to Pebble");
+}
diff --git a/src/appmessage.h b/src/appmessage.h
new file mode 100644
index 0000000..d702aed
--- /dev/null
+++ b/src/appmessage.h
@@ -0,0 +1,3 @@
+#pragma once
+
+void appmessage_init(void);
diff --git a/src/common.h b/src/common.h
new file mode 100644
index 0000000..d7686c1
--- /dev/null
+++ b/src/common.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#define ENDPOINT_FRONTPAGE 0
+#define ENDPOINT_NEWPOSTS 1
+#define ENDPOINT_BESTPOSTS 2
+
+typedef struct {
+ int index;
+ char title[24];
+ char subtitle[30];
+} HNStory;
+
+enum {
+ HN_KEY_ENDPOINT = 0x0,
+ HN_KEY_INDEX = 0x1,
+ HN_KEY_TITLE = 0x2,
+ HN_KEY_SUBTITLE = 0x3,
+ HN_KEY_SUMMARY = 0x4,
+};
diff --git a/src/js/pebble-js-app.js b/src/js/pebble-js-app.js
new file mode 100644
index 0000000..6ac551b
--- /dev/null
+++ b/src/js/pebble-js-app.js
@@ -0,0 +1,147 @@
+var maxAppMessageBuffer = 100;
+var maxAppMessageTries = 3;
+var appMessageRetryTimeout = 3000;
+var appMessageTimeout = 100;
+var httpTimeout = 12000;
+var appMessageQueue = [];
+var stories = {};
+
+var ENDPOINTS = {
+ 'FRONTPAGE': 0,
+ 'NEWPOSTS': 1,
+ 'BESTPOSTS': 2
+};
+
+var API_URLS = {
+ [ENDPOINTS.FRONTPAGE]: 'http://hnify.herokuapp.com/get/top',
+ [ENDPOINTS.NEWPOSTS]: 'http://hnify.herokuapp.com/get/newest',
+ [ENDPOINTS.BESTPOSTS]: 'http://hnify.herokuapp.com/get/best',
+ 'clipped': 'http://clipped.me/algorithm/clippedapi.php?url='
+};
+
+function sendAppMessage() {
+ if (appMessageQueue.length > 0) {
+ currentAppMessage = appMessageQueue[0];
+ currentAppMessage.numTries = currentAppMessage.numTries || 0;
+ currentAppMessage.transactionId = currentAppMessage.transactionId || -1;
+ if (currentAppMessage.numTries < maxAppMessageTries) {
+ console.log('Sending AppMessage to Pebble: ' + JSON.stringify(currentAppMessage.message));
+ Pebble.sendAppMessage(
+ currentAppMessage.message,
+ function(e) {
+ appMessageQueue.shift();
+ setTimeout(function() {
+ sendAppMessage();
+ }, appMessageTimeout);
+ }, function(e) {
+ console.log('Failed sending AppMessage for transactionId:' + e.data.transactionId + '. Error: ' + e.data.error.message);
+ appMessageQueue[0].transactionId = e.data.transactionId;
+ appMessageQueue[0].numTries++;
+ setTimeout(function() {
+ sendAppMessage();
+ }, appMessageRetryTimeout);
+ }
+ );
+ } else {
+ console.log('Failed sending AppMessage for transactionId:' + currentAppMessage.transactionId + '. Bailing. ' + JSON.stringify(currentAppMessage.message));
+ }
+ }
+}
+
+function hackernews(endpoint) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', API_URLS[endpoint], true);
+ xhr.timeout = httpTimeout;
+ xhr.onload = function(e) {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ if (xhr.responseText) {
+ res = JSON.parse(xhr.responseText);
+ stories = res.stories;
+ stories.forEach(function (element, index, array) {
+ title = element.title.substring(0,23);
+ subtitle = element.points + ' points • ' + element.num_comments + ' comments';
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'index': index, 'title': title, 'subtitle': subtitle}});
+ });
+ } else {
+ console.log('Invalid response received! ' + JSON.stringify(xhr));
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'title': 'Invalid response!'}});
+ }
+ } else {
+ console.log('Request returned error code ' + xhr.status.toString());
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'title': 'HTTP/1.1 ' + xhr.statusText}});
+ }
+ }
+ sendAppMessage();
+ }
+ xhr.ontimeout = function() {
+ console.log('HTTP request timed out');
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'title': 'Request timed out!'}});
+ sendAppMessage();
+ };
+ xhr.onerror = function() {
+ console.log('HTTP request return error');
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'title': 'Failed to connect!'}});
+ sendAppMessage();
+ };
+ xhr.send(null);
+}
+
+function clipped(url, endpoint) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', API_URLS.clipped + url, true);
+ xhr.timeout = httpTimeout;
+ xhr.onload = function(e) {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ if (xhr.responseText) {
+ try {
+ res = JSON.parse(xhr.responseText);
+ title = res.title || '';
+ summary = res.summary.join(' ') || '';
+ for (var i = 0; i <= Math.floor(summary.length/maxAppMessageBuffer); i++) {
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'summary': summary.substring(i * maxAppMessageBuffer, i * maxAppMessageBuffer + maxAppMessageBuffer)}});
+ }
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'summary': true, 'title': title.substring(0,60)}});
+ } catch(e) {
+ console.log('Caught error: ' + JSON.stringify(e));
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'summary':true}});
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'summary':true, 'title': 'Unable to summarize! :('}});
+ }
+ } else {
+ console.log('Invalid response received! ' + JSON.stringify(xhr));
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'summary':true, 'title': 'Invalid response!'}});
+ }
+ } else {
+ console.log('Request returned error code ' + xhr.status.toString());
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'summary':true, 'title': 'HTTP/' + xhr.statusText}});
+ }
+ }
+ sendAppMessage();
+ }
+ xhr.ontimeout = function() {
+ console.log('HTTP request timed out');
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'summary':true, 'title': 'Request timed out!'}});
+ sendAppMessage();
+ };
+ xhr.onerror = function() {
+ console.log('HTTP request return error');
+ appMessageQueue.push({'message': {'endpoint': endpoint, 'summary':true, 'title': 'Failed to connect!'}});
+ sendAppMessage();
+ };
+ xhr.send(null);
+}
+
+Pebble.addEventListener('ready', function(e) {});
+
+Pebble.addEventListener('appmessage', function(e) {
+ console.log('AppMessage received from Pebble: ' + JSON.stringify(e.payload));
+ if (e.payload.summary) {
+ clipped(stories[e.payload.index].link, e.payload.endpoint);
+ } else if (typeof(e.payload.endpoint) != 'undefined') {
+ hackernews(e.payload.endpoint);
+ } else {
+ appMessageQueue = [];
+ }
+});
+
diff --git a/src/libs/pebble-assist.h b/src/libs/pebble-assist.h
new file mode 100644
index 0000000..dd395bf
--- /dev/null
+++ b/src/libs/pebble-assist.h
@@ -0,0 +1,55 @@
+/***
+ * Pebble Assist
+ * Copyright (C) 2013 Matthew Tole
+ ***/
+
+#pragma once
+
+#ifndef MENU_CELL_BASIC_CELL_HEIGHT
+#define MENU_CELL_BASIC_CELL_HEIGHT 44
+#endif
+
+#ifndef PEBBLE_HEIGHT
+#define PEBBLE_HEIGHT 168
+#endif
+
+#ifndef PEBBLE_WIDTH
+#define PEBBLE_WIDTH 144
+#endif
+
+#ifndef STATUS_HEIGHT
+#define STATUS_HEIGHT 16
+#endif
+
+#define layer_add_to_window(layer, window) layer_add_child(window_get_root_layer(window), layer)
+#define text_layer_add_to_window(layer, window) layer_add_child(window_get_root_layer(window), text_layer_get_layer(layer))
+#define bitmap_layer_add_to_window(layer, window) layer_add_child(window_get_root_layer(window), bitmap_layer_get_layer(layer))
+#define inverter_layer_add_to_window(layer, window) layer_add_child(window_get_root_layer(window), inverter_layer_get_layer(layer))
+#define menu_layer_add_to_window(layer, window) layer_add_child(window_get_root_layer(window), menu_layer_get_layer(layer))
+#define simple_menu_layer_add_to_window(layer, window) layer_add_child(window_get_root_layer(window), simple_menu_layer_get_layer(layer))
+#define text_layer_set_system_font(layer, font) text_layer_set_font(layer, fonts_get_system_font(font))
+#define text_layer_set_colours(layer, foreground, background) text_layer_set_text_color(layer, foreground); text_layer_set_background_color(layer, background)
+#define text_layer_set_colors(layer, foreground, background) text_layer_set_text_color(layer, foreground); text_layer_set_background_color(layer, background)
+#define layer_create_fullscreen(window) layer_create(layer_get_bounds(window_get_root_layer(window)));
+#define text_layer_create_fullscreen(window) text_layer_create(layer_get_bounds(window_get_root_layer(window)));
+#define menu_layer_create_fullscreen(window) menu_layer_create(layer_get_bounds(window_get_root_layer(window)));
+#define action_bar_layer_create_and_add(action_bar, window) action_bar = action_bar_layer_create(); action_bar_layer_add_to_window(action_bar, window)
+
+#define window_destroy_safe(window) if (window != NULL) { window_destroy(window); }
+#define number_window_destroy_safe(window) if (window != NULL ) { number_window_destroy(window); }
+#define text_layer_destroy_safe(layer) if (layer != NULL) { text_layer_destroy(layer); }
+#define bitmap_layer_destroy_safe(layer) if (layer != NULL) { bitmap_layer_destroy(layer); }
+#define layer_destroy_safe(layer) if (layer != NULL) { layer_destroy(layer); }
+#define menu_layer_destroy_safe(layer) if (layer != NULL) { menu_layer_destroy(layer); }
+#define simple_menu_layer_destroy_safe(layer) if (layer != NULL) { simple_menu_layer_destroy(layer); }
+#define action_bar_layer_destroy_safe(layer) if (layer != NULL) { action_bar_layer_destroy(layer); }
+#define scroll_layer_destroy_safe(layer) if (layer != NULL) { scroll_layer_destroy(layer); }
+#define app_timer_cancel_safe(timer) if (timer != NULL) { app_timer_cancel(timer); timer = NULL; }
+
+#define persist_read_int_safe(key, value) if (persist_exists(key)) { return persist_read_int(key); } else { return value; }
+
+#define fonts_load_resource_font(resource) fonts_load_custom_font(resource_get_handle(resource))
+
+#define action_bar_layer_clear_icons(action_bar) action_bar_layer_clear_icon(action_bar, BUTTON_ID_SELECT); action_bar_layer_clear_icon(action_bar, BUTTON_ID_DOWN); action_bar_layer_clear_icon(action_bar, BUTTON_ID_UP)
+
+#define menu_layer_reload_data_and_mark_dirty(layer) menu_layer_reload_data(layer); layer_mark_dirty(menu_layer_get_layer(layer));
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..35d523e
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,125 @@
+#include
+#include "common.h"
+#include "appmessage.h"
+#include "libs/pebble-assist.h"
+#include "windows/storylist.h"
+
+#define MENU_NUM_SECTIONS 1
+
+#define MENU_SECTION_HOME 0
+
+#define MENU_SECTION_ROWS_HOME 4
+
+#define MENU_ROW_HOME_FRONTPAGE 0
+#define MENU_ROW_HOME_NEWPOSTS 1
+#define MENU_ROW_HOME_BESTPOSTS 2
+#define MENU_ROW_HOME_SETTINGS 3
+
+static Window *window;
+
+static MenuLayer *menu_layer;
+
+static uint16_t menu_get_num_sections_callback(struct MenuLayer *menu_layer, void *callback_context) {
+ return MENU_NUM_SECTIONS;
+}
+
+static uint16_t menu_get_num_rows_callback(struct MenuLayer *menu_layer, uint16_t section_index, void *callback_context) {
+ switch (section_index) {
+ case MENU_SECTION_HOME:
+ return MENU_SECTION_ROWS_HOME;
+ break;
+ }
+ return 0;
+}
+
+static int16_t menu_get_header_height_callback(struct MenuLayer *menu_layer, uint16_t section_index, void *callback_context) {
+ return MENU_CELL_BASIC_HEADER_HEIGHT;
+}
+
+static int16_t menu_get_cell_height_callback(struct MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context) {
+ return 36;
+}
+
+static void menu_draw_header_callback(GContext *ctx, const Layer *cell_layer, uint16_t section_index, void *callback_context) {
+ menu_cell_basic_header_draw(ctx, cell_layer, "Hacker News");
+}
+
+static void menu_draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index, void *callback_context) {
+ char label[12] = "";
+ switch (cell_index->section) {
+ case MENU_SECTION_HOME:
+ switch (cell_index->row) {
+ case MENU_ROW_HOME_FRONTPAGE:
+ strcpy(label, "Front Page");
+ break;
+ case MENU_ROW_HOME_NEWPOSTS:
+ strcpy(label, "New Posts");
+ break;
+ case MENU_ROW_HOME_BESTPOSTS:
+ strcpy(label, "Best Posts");
+ break;
+ case MENU_ROW_HOME_SETTINGS:
+ strcpy(label, "Settings");
+ break;
+ }
+ break;
+ }
+ graphics_context_set_text_color(ctx, GColorBlack);
+ graphics_draw_text(ctx, label, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD), (GRect) { .origin = { 4, 0 }, .size = { PEBBLE_WIDTH - 8, 24 } }, GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL);
+}
+
+static void menu_select_callback(struct MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context) {
+ switch (cell_index->section) {
+ case MENU_SECTION_HOME:
+ switch (cell_index->row) {
+ case MENU_ROW_HOME_FRONTPAGE:
+ storylist_show(ENDPOINT_FRONTPAGE);
+ break;
+ case MENU_ROW_HOME_NEWPOSTS:
+ storylist_show(ENDPOINT_NEWPOSTS);
+ break;
+ case MENU_ROW_HOME_BESTPOSTS:
+ storylist_show(ENDPOINT_BESTPOSTS);
+ break;
+ case MENU_ROW_HOME_SETTINGS:
+ break;
+ }
+ break;
+ }
+}
+
+static void init(void) {
+ appmessage_init();
+
+ window = window_create();
+
+ menu_layer = menu_layer_create_fullscreen(window);
+ menu_layer_set_callbacks(menu_layer, NULL, (MenuLayerCallbacks) {
+ .get_num_sections = menu_get_num_sections_callback,
+ .get_num_rows = menu_get_num_rows_callback,
+ .get_header_height = menu_get_header_height_callback,
+ .get_cell_height = menu_get_cell_height_callback,
+ .draw_header = menu_draw_header_callback,
+ .draw_row = menu_draw_row_callback,
+ .select_click = menu_select_callback,
+ });
+ menu_layer_set_click_config_onto_window(menu_layer, window);
+ menu_layer_add_to_window(menu_layer, window);
+
+ window_stack_push(window, true /* animated */);
+
+ storylist_init();
+}
+
+static void deinit(void) {
+ storylist_destroy();
+ layer_remove_from_parent(menu_layer_get_layer(menu_layer));
+ menu_layer_destroy_safe(menu_layer);
+ window_destroy_safe(window);
+}
+
+int main(void) {
+ init();
+ app_event_loop();
+ deinit();
+}
diff --git a/src/settings.c b/src/settings.c
new file mode 100644
index 0000000..e69de29
diff --git a/src/settings.h b/src/settings.h
new file mode 100644
index 0000000..e69de29
diff --git a/src/windows/commentsview.c b/src/windows/commentsview.c
new file mode 100644
index 0000000..e69de29
diff --git a/src/windows/commentsview.h b/src/windows/commentsview.h
new file mode 100644
index 0000000..e69de29
diff --git a/src/windows/settings.c b/src/windows/settings.c
new file mode 100644
index 0000000..e69de29
diff --git a/src/windows/settings.h b/src/windows/settings.h
new file mode 100644
index 0000000..e69de29
diff --git a/src/windows/storylist.c b/src/windows/storylist.c
new file mode 100644
index 0000000..2405c00
--- /dev/null
+++ b/src/windows/storylist.c
@@ -0,0 +1,195 @@
+#include
+#include "storylist.h"
+#include "../libs/pebble-assist.h"
+#include "../common.h"
+#include "storyview.h"
+
+#define MAX_STORIES 30
+
+static HNStory stories[MAX_STORIES];
+
+static uint16_t endpoint;
+static int num_stories;
+static char summary[2048];
+static char error[24];
+
+static void refresh_list();
+static void request_data();
+static uint16_t menu_get_num_sections_callback(struct MenuLayer *menu_layer, void *callback_context);
+static uint16_t menu_get_num_rows_callback(struct MenuLayer *menu_layer, uint16_t section_index, void *callback_context);
+static int16_t menu_get_header_height_callback(struct MenuLayer *menu_layer, uint16_t section_index, void *callback_context);
+static int16_t menu_get_cell_height_callback(struct MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context);
+static void menu_draw_header_callback(GContext *ctx, const Layer *cell_layer, uint16_t section_index, void *callback_context);
+static void menu_draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index, void *callback_context);
+static void menu_select_callback(struct MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context);
+static void menu_select_long_callback(struct MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context);
+
+static Window *window;
+static MenuLayer *menu_layer;
+
+void storylist_init(void) {
+ window = window_create();
+
+ menu_layer = menu_layer_create_fullscreen(window);
+ menu_layer_set_callbacks(menu_layer, NULL, (MenuLayerCallbacks) {
+ .get_num_sections = menu_get_num_sections_callback,
+ .get_num_rows = menu_get_num_rows_callback,
+ .get_header_height = menu_get_header_height_callback,
+ .get_cell_height = menu_get_cell_height_callback,
+ .draw_header = menu_draw_header_callback,
+ .draw_row = menu_draw_row_callback,
+ .select_click = menu_select_callback,
+ .select_long_click = menu_select_long_callback,
+ });
+ menu_layer_set_click_config_onto_window(menu_layer, window);
+ menu_layer_add_to_window(menu_layer, window);
+}
+
+void storylist_show(int end_point) {
+ endpoint = end_point;
+ refresh_list();
+ window_stack_push(window, true);
+}
+
+void storylist_destroy(void) {
+ storyview_destroy();
+ layer_remove_from_parent(menu_layer_get_layer(menu_layer));
+ menu_layer_destroy_safe(menu_layer);
+ window_destroy_safe(window);
+}
+
+void storylist_in_received_handler(DictionaryIterator *iter) {
+ Tuple *index_tuple = dict_find(iter, HN_KEY_INDEX);
+ Tuple *title_tuple = dict_find(iter, HN_KEY_TITLE);
+ Tuple *subtitle_tuple = dict_find(iter, HN_KEY_SUBTITLE);
+ Tuple *summary_tuple = dict_find(iter, HN_KEY_SUMMARY);
+
+ if (index_tuple && title_tuple && subtitle_tuple) {
+ HNStory story;
+ story.index = index_tuple->value->int16;
+ strncpy(story.title, title_tuple->value->cstring, sizeof(story.title));
+ strncpy(story.subtitle, subtitle_tuple->value->cstring, sizeof(story.subtitle));
+ stories[story.index] = story;
+ num_stories++;
+ menu_layer_reload_data_and_mark_dirty(menu_layer);
+ APP_LOG(APP_LOG_LEVEL_DEBUG, "received story [%d] %s - %s", story.index, story.title, story.subtitle);
+ }
+ else if (title_tuple) {
+ strncpy(error, title_tuple->value->cstring, sizeof(error));
+ menu_layer_reload_data_and_mark_dirty(menu_layer);
+ }
+
+ if (summary_tuple) {
+ if (title_tuple) {
+ storyview_init(title_tuple->value->cstring, summary);
+ storyview_show();
+ } else {
+ strcat(summary, summary_tuple->value->cstring);
+ }
+ }
+}
+
+bool storylist_is_on_top() {
+ return window == window_stack_get_top_window();
+}
+
+uint16_t storylist_current_endpoint() {
+ return endpoint;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - //
+
+static void refresh_list() {
+ memset(stories, 0x0, sizeof(stories));
+ num_stories = 0;
+ error[0] = '\0';
+ menu_layer_set_selected_index(menu_layer, (MenuIndex) { .row = 0, .section = 0 }, MenuRowAlignBottom, false);
+ menu_layer_reload_data_and_mark_dirty(menu_layer);
+ request_data();
+}
+
+static void request_data() {
+ Tuplet endpoint_tuple = TupletInteger(HN_KEY_ENDPOINT, endpoint);
+
+ DictionaryIterator *iter;
+ app_message_outbox_begin(&iter);
+
+ if (iter == NULL) {
+ return;
+ }
+
+ dict_write_tuplet(iter, &endpoint_tuple);
+ dict_write_end(iter);
+
+ app_message_outbox_send();
+}
+
+static uint16_t menu_get_num_sections_callback(struct MenuLayer *menu_layer, void *callback_context) {
+ return 1;
+}
+
+static uint16_t menu_get_num_rows_callback(struct MenuLayer *menu_layer, uint16_t section_index, void *callback_context) {
+ return (num_stories) ? num_stories : 1;
+}
+
+static int16_t menu_get_header_height_callback(struct MenuLayer *menu_layer, uint16_t section_index, void *callback_context) {
+ return MENU_CELL_BASIC_HEADER_HEIGHT;
+}
+
+static int16_t menu_get_cell_height_callback(struct MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context) {
+ return MENU_CELL_BASIC_CELL_HEIGHT;
+}
+
+static void menu_draw_header_callback(GContext *ctx, const Layer *cell_layer, uint16_t section_index, void *callback_context) {
+ switch (endpoint) {
+ case ENDPOINT_FRONTPAGE:
+ menu_cell_basic_header_draw(ctx, cell_layer, "Hacker News - Front Page");
+ break;
+ case ENDPOINT_NEWPOSTS:
+ menu_cell_basic_header_draw(ctx, cell_layer, "Hacker News - New Posts");
+ break;
+ case ENDPOINT_BESTPOSTS:
+ menu_cell_basic_header_draw(ctx, cell_layer, "Hacker News - Best Posts");
+ break;
+ }
+}
+
+static void menu_draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index, void *callback_context) {
+ if (strlen(error) != 0) {
+ menu_cell_basic_draw(ctx, cell_layer, "Error!", error, NULL);
+ } else if (num_stories == 0) {
+ menu_cell_basic_draw(ctx, cell_layer, "Loading...", NULL, NULL);
+ } else {
+ menu_cell_basic_draw(ctx, cell_layer, stories[cell_index->row].title, stories[cell_index->row].subtitle, NULL);
+ }
+}
+
+static void menu_select_callback(struct MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context) {
+ if (num_stories == 0) {
+ return;
+ }
+
+ Tuplet summary_tuple = TupletInteger(HN_KEY_SUMMARY, 1);
+ Tuplet endpoint_tuple = TupletInteger(HN_KEY_ENDPOINT, endpoint);
+ Tuplet index_tuple = TupletInteger(HN_KEY_INDEX, cell_index->row);
+
+ DictionaryIterator *iter;
+ app_message_outbox_begin(&iter);
+
+ if (iter == NULL) {
+ return;
+ }
+
+ dict_write_tuplet(iter, &summary_tuple);
+ dict_write_tuplet(iter, &endpoint_tuple);
+ dict_write_tuplet(iter, &index_tuple);
+ dict_write_end(iter);
+
+ app_message_outbox_send();
+
+ summary[0] = '\0';
+}
+
+static void menu_select_long_callback(struct MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context) {
+ refresh_list();
+}
diff --git a/src/windows/storylist.h b/src/windows/storylist.h
new file mode 100644
index 0000000..9fc0424
--- /dev/null
+++ b/src/windows/storylist.h
@@ -0,0 +1,8 @@
+#pragma once
+
+void storylist_init(void);
+void storylist_show(int end_point);
+void storylist_destroy(void);
+void storylist_in_received_handler(DictionaryIterator *iter);
+bool storylist_is_on_top();
+uint16_t storylist_current_endpoint();
diff --git a/src/windows/storyview.c b/src/windows/storyview.c
new file mode 100644
index 0000000..9b08ebf
--- /dev/null
+++ b/src/windows/storyview.c
@@ -0,0 +1,52 @@
+#include
+#include "storyview.h"
+#include "../libs/pebble-assist.h"
+#include "../common.h"
+
+static Window *window;
+static ScrollLayer *scroll_layer;
+static TextLayer *title_layer;
+static TextLayer *summary_layer;
+
+void storyview_init(char *title, char *summary) {
+ window = window_create();
+
+ Layer *window_layer = window_get_root_layer(window);
+ GRect bounds = layer_get_bounds(window_layer);
+ GRect max_text_bounds = GRect(2, 0, bounds.size.w - 4, 2000);
+
+ scroll_layer = scroll_layer_create(bounds);
+ scroll_layer_set_click_config_onto_window(scroll_layer, window);
+
+ title_layer = text_layer_create(max_text_bounds);
+ text_layer_set_font(title_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
+ text_layer_set_text(title_layer, title);
+
+ GSize title_max_size = text_layer_get_content_size(title_layer);
+ text_layer_set_size(title_layer, GSize(title_max_size.w, title_max_size.h + 14));
+
+ summary_layer = text_layer_create(GRect(2, title_max_size.h + 14, max_text_bounds.size.w, max_text_bounds.size.h - (title_max_size.h + 14)));
+ text_layer_set_font(summary_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
+ text_layer_set_text(summary_layer, summary);
+
+ GSize summary_max_size = text_layer_get_content_size(summary_layer);
+ text_layer_set_size(summary_layer, GSize(summary_max_size.w, summary_max_size.h + 14));
+
+ scroll_layer_set_content_size(scroll_layer, GSize(bounds.size.w, title_max_size.h + 14 + summary_max_size.h + 8));
+
+ scroll_layer_add_child(scroll_layer, text_layer_get_layer(title_layer));
+ scroll_layer_add_child(scroll_layer, text_layer_get_layer(summary_layer));
+
+ layer_add_child(window_layer, scroll_layer_get_layer(scroll_layer));
+}
+
+void storyview_show(void) {
+ window_stack_push(window, true);
+}
+
+void storyview_destroy(void) {
+ text_layer_destroy_safe(title_layer);
+ text_layer_destroy_safe(summary_layer);
+ scroll_layer_destroy_safe(scroll_layer);
+ window_destroy_safe(window);
+}
diff --git a/src/windows/storyview.h b/src/windows/storyview.h
new file mode 100644
index 0000000..fe9bbf8
--- /dev/null
+++ b/src/windows/storyview.h
@@ -0,0 +1,5 @@
+#pragma once
+
+void storyview_init(char *title, char *summary);
+void storyview_show(void);
+void storyview_destroy(void);
diff --git a/wscript b/wscript
new file mode 100644
index 0000000..4880a16
--- /dev/null
+++ b/wscript
@@ -0,0 +1,24 @@
+
+#
+# This file is the default set of rules to compile a Pebble project.
+#
+# Feel free to customize this to your needs.
+#
+
+top = '.'
+out = 'build'
+
+def options(ctx):
+ ctx.load('pebble_sdk')
+
+def configure(ctx):
+ ctx.load('pebble_sdk')
+
+def build(ctx):
+ ctx.load('pebble_sdk')
+
+ ctx.pbl_program(source=ctx.path.ant_glob('src/**/*.c'),
+ target='pebble-app.elf')
+
+ ctx.pbl_bundle(elf='pebble-app.elf',
+ js=ctx.path.ant_glob('src/js/**/*.js'))