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'))