├── .gitignore ├── LICENSE ├── README.md ├── appinfo.json ├── resources └── images │ ├── icon.png │ ├── image.png │ ├── text.png │ └── upvote.png ├── server ├── bitmapgen.py └── rebble_server.py ├── src ├── AppMessages.c ├── AppMessages.h ├── CommentWindow.c ├── CommentWindow.h ├── LoadingWindow.c ├── LoadingWindow.h ├── Rebble.c ├── Rebble.h ├── SubredditListWindow.c ├── SubredditListWindow.h ├── SubredditWindow.c ├── SubredditWindow.h ├── ThreadMenuWindow.c ├── ThreadMenuWindow.h ├── ThreadWindow.c ├── ThreadWindow.h ├── js │ ├── .pebble-js-app.js.swp │ └── pebble-js-app.js ├── netimage.c └── netimage.h ├── wscript └── wscript.backup /.gitignore: -------------------------------------------------------------------------------- 1 | *.lock-waf_linux2_build 2 | build/ 3 | crash_line.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Spacetech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rebble 2 | ====== 3 | 4 | Reddit on the Pebble Smartwatch. With Rebble, you can easily view Reddit on the go; You can read threads and view linked images. You can sign in and browse your favorite subreddits and even upvote and save your favorite posts. Heck, you can even downvote those other guys. No companion app is required! 5 | 6 | ====== 7 | 8 | Released on the [Pebble App Store](http://pblweb.com/appstore/5300e4029810e0a8940000e8/) 9 | 10 | Vote for us on [ChallengePost](http://pebble.challengepost.com/submissions/21824-rebble)! 11 | 12 | [![ScreenShot](http://img.youtube.com/vi/Umkoy4dOLwc/0.jpg)](http://youtu.be/Umkoy4dOLwc) 13 | 14 | -------------------------------------------------------------------------------- /appinfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "d7583c96-0b0c-41ee-86a4-facdca791151", 3 | "shortName": "Rebble", 4 | "longName": "Rebble", 5 | "companyName": "NegaTech", 6 | "versionCode": 8, 7 | "versionLabel": "1.4", 8 | "capabilities": [ 9 | "configurable" 10 | ], 11 | "watchapp": { 12 | "watchface": false 13 | }, 14 | "appKeys": { 15 | "subreddit": 0, 16 | "thread": 1, 17 | "subreddit_next": 2, 18 | "id": 3, 19 | "title": 4, 20 | "score": 5, 21 | "type": 6, 22 | "thread_body": 7, 23 | "upvote": 8, 24 | "downvote": 9, 25 | "save": 10, 26 | "user_subreddit": 11, 27 | "load_subredditlist": 13, 28 | "chunk_size": 14, 29 | "ready": 15, 30 | "thread_subreddit": 16, 31 | "load_comments": 17, 32 | "comment": 18, 33 | "NETIMAGE_DATA": 1768777472, 34 | "NETIMAGE_BEGIN": 1768777473, 35 | "NETIMAGE_END": 1768777474, 36 | "NETIMAGE_CHUNK_SIZE": 1768777475, 37 | "NETIMAGE_URL": 1768777476 38 | }, 39 | "resources": { 40 | "media": [ 41 | { 42 | "type": "png", 43 | "menuIcon": true, 44 | "name": "IMAGE_ICON", 45 | "file": "images/icon.png" 46 | }, 47 | { 48 | "type": "png", 49 | "name": "IMAGE_UPVOTE", 50 | "file": "images/upvote.png" 51 | }, 52 | { 53 | "type": "png", 54 | "name": "IMAGE_TEXT", 55 | "file": "images/text.png" 56 | }, 57 | { 58 | "type": "png", 59 | "name": "IMAGE_IMAGE", 60 | "file": "images/image.png" 61 | } 62 | ] 63 | }, 64 | "targetPlatforms": [ 65 | "aplite", 66 | "basalt" 67 | ], 68 | "sdkVersion": "3" 69 | } 70 | -------------------------------------------------------------------------------- /resources/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spacetech/Rebble/cfb3e2597fb5a7db8d58e4469d2cec4144c90c6b/resources/images/icon.png -------------------------------------------------------------------------------- /resources/images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spacetech/Rebble/cfb3e2597fb5a7db8d58e4469d2cec4144c90c6b/resources/images/image.png -------------------------------------------------------------------------------- /resources/images/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spacetech/Rebble/cfb3e2597fb5a7db8d58e4469d2cec4144c90c6b/resources/images/text.png -------------------------------------------------------------------------------- /resources/images/upvote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spacetech/Rebble/cfb3e2597fb5a7db8d58e4469d2cec4144c90c6b/resources/images/upvote.png -------------------------------------------------------------------------------- /server/bitmapgen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import StringIO 4 | import argparse 5 | import os 6 | import struct 7 | import sys 8 | from PIL import Image 9 | 10 | WHITE_COLOR_MAP = { 11 | 'white' : 1, 12 | 'black' : 0, 13 | 'transparent' : 0, 14 | } 15 | 16 | BLACK_COLOR_MAP = { 17 | 'white' : 0, 18 | 'black' : 1, 19 | 'transparent' : 0, 20 | } 21 | 22 | # Bitmap struct (NB: All fields are little-endian) 23 | # (uint16_t) row_size_bytes 24 | # (uint16_t) info_flags 25 | # bit 0 : reserved (must be zero for bitmap files) 26 | # bits 12-15 : file version 27 | # (int16_t) bounds.origin.x 28 | # (int16_t) bounds.origin.y 29 | # (int16_t) bounds.size.w 30 | # (int16_t) bounds.size.h 31 | # (uint32_t) image data (word-aligned, 0-padded rows of bits) 32 | class PebbleBitmap(object): 33 | def __init__(self, path, color_map = WHITE_COLOR_MAP): 34 | self.version = 1 35 | self.path = path 36 | self.name, _ = os.path.splitext(os.path.basename(path)) 37 | self.image = Image.open(path).convert("RGBA") 38 | self.color_map = color_map 39 | 40 | if not self.image: 41 | self.width = 0 42 | self.height = 0 43 | return 44 | 45 | (left, top, right, bottom) = self.image.getbbox() 46 | self.x = left 47 | self.y = top 48 | self.w = right - left 49 | self.h = bottom - top 50 | 51 | def row_size_bytes(self): 52 | """ 53 | Return the length of the bitmap's row in bytes. 54 | 55 | Row lengths are rounded up to the nearest word, padding up to 56 | 3 empty bytes per row. 57 | """ 58 | 59 | row_size_padded_words = (self.w + 31) / 32 60 | return row_size_padded_words * 4 61 | 62 | def info_flags(self): 63 | """Returns the type and version of bitmap.""" 64 | 65 | return self.version << 12 66 | 67 | def pbi_header(self): 68 | return struct.pack('>f, '#pragma once' 183 | for h in header_paths: 184 | print>>f, "#include \"{0}\"".format(h) 185 | f.close() 186 | 187 | def process_cmd_line_args(): 188 | parser = argparse.ArgumentParser(description="Generate pebble-usable files from png images") 189 | subparsers = parser.add_subparsers(help="commands", dest='which') 190 | 191 | pbi_parser = subparsers.add_parser('pbi', help="make a .pbi (pebble binary image) file") 192 | pbi_parser.add_argument('input_png', metavar='INPUT_PNG', help="The png image to process") 193 | pbi_parser.add_argument('output_pbi', metavar='OUTPUT_PBI', help="The pbi output file") 194 | pbi_parser.set_defaults(func=cmd_pbi) 195 | 196 | h_parser = subparsers.add_parser('header', help="make a .h file") 197 | h_parser.add_argument('input_png', metavar='INPUT_PNG', help="The png image to process") 198 | h_parser.set_defaults(func=cmd_header) 199 | 200 | white_pbi_parser = subparsers.add_parser('white_trans_pbi', help="make a .pbi (pebble binary image) file for a white transparency layer") 201 | white_pbi_parser.add_argument('input_png', metavar='INPUT_PNG', help="The png image to process") 202 | white_pbi_parser.add_argument('output_pbi', metavar='OUTPUT_PBI', help="The pbi output file") 203 | white_pbi_parser.set_defaults(func=cmd_white_trans_pbi) 204 | 205 | black_pbi_parser = subparsers.add_parser('black_trans_pbi', help="make a .pbi (pebble binary image) file for a black transparency layer") 206 | black_pbi_parser.add_argument('input_png', metavar='INPUT_PNG', help="The png image to process") 207 | black_pbi_parser.add_argument('output_pbi', metavar='OUTPUT_PBI', help="The pbi output file") 208 | black_pbi_parser.set_defaults(func=cmd_black_trans_pbi) 209 | 210 | args = parser.parse_args() 211 | args.func(args) 212 | 213 | def main(): 214 | if (len(sys.argv) < 2): 215 | # process everything in the bitmaps folder 216 | process_all_bitmaps() 217 | else: 218 | # process an individual file 219 | process_cmd_line_args() 220 | 221 | if __name__ == "__main__": 222 | main() 223 | -------------------------------------------------------------------------------- /server/rebble_server.py: -------------------------------------------------------------------------------- 1 | import SimpleHTTPServer, SocketServer 2 | import urlparse 3 | import BaseHTTPServer 4 | import socket 5 | from urllib import urlretrieve 6 | from PIL import Image 7 | from bitmapgen import * 8 | import thread 9 | 10 | mixin = SocketServer.ThreadingMixIn 11 | 12 | PORT = 2635 13 | 14 | image = 0 15 | 16 | class RebbleServer(mixin, BaseHTTPServer.HTTPServer): 17 | 18 | def __init__(self): 19 | global PORT 20 | SocketServer.TCPServer.__init__(self, ('', PORT), MyHandler) 21 | 22 | def server_bind(self): 23 | if hasattr(socket, 'SOL_SOCKET') and hasattr(socket, 'SO_REUSEADDR'): 24 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 25 | BaseHTTPServer.HTTPServer.server_bind(self) 26 | 27 | class MyHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 28 | def do_GET(self): 29 | global image 30 | 31 | # Parse query data & params to find out what was passed 32 | parsedParams = urlparse.urlparse(self.path) 33 | queryParsed = urlparse.parse_qs(parsedParams.query) 34 | 35 | #print queryParsed 36 | 37 | if "url" in queryParsed: 38 | url = queryParsed["url"] 39 | 40 | filename = str(image) + ".jpg" 41 | 42 | try: 43 | url = url[0] 44 | 45 | #print url 46 | 47 | urlretrieve(url, filename) 48 | 49 | img = Image.open(filename) 50 | 51 | #Scale 52 | dims = (144, 168) 53 | #print 'resizing from ', img.size, ' to dims ', dims 54 | 55 | img.thumbnail(dims, Image.ANTIALIAS) 56 | #img = img.resize(dims, Image.ANTIALIAS) 57 | #print 'new size: ', img.size 58 | 59 | img = img.convert('1') 60 | 61 | #img = img.convert('P') 62 | #img = img.point(lambda p: p > 127 and 255, '1') 63 | 64 | img.save(str(image) + ".png") 65 | 66 | pb = PebbleBitmap(str(image) + ".png") 67 | #pb.convert_to_pbi(str(image) + ".pbi") 68 | 69 | self.send_response(200) 70 | self.send_header('Content-Type', 'application/text') 71 | self.end_headers() 72 | 73 | self.wfile.write(pb.pbi_header()) 74 | self.wfile.write(pb.image_bits()) 75 | self.wfile.close(); 76 | 77 | except Exception, e: 78 | #print "nooo" 79 | #print e 80 | self.send_response(404) 81 | self.end_headers() 82 | 83 | try: 84 | os.remove(filename) 85 | os.remove(str(image) + ".png") 86 | 87 | except Exception, e: 88 | pass 89 | 90 | image += 1 91 | 92 | else: 93 | self.send_response(404) 94 | self.end_headers() 95 | 96 | httpd = RebbleServer(); 97 | 98 | print "serving at port", PORT 99 | httpd.serve_forever() 100 | -------------------------------------------------------------------------------- /src/AppMessages.c: -------------------------------------------------------------------------------- 1 | /******************************* 2 | App Messsage Handlers 3 | ********************************/ 4 | 5 | #include "Rebble.h" 6 | #include "AppMessages.h" 7 | #include "ThreadWindow.h" 8 | #include "SubredditWindow.h" 9 | #include "SubredditListWindow.h" 10 | #include "CommentWindow.h" 11 | #include "netimage.h" 12 | #include "LoadingWindow.h" 13 | 14 | extern struct ViewThreadData current_thread; 15 | extern struct ThreadData threads[MAX_THREADS]; 16 | 17 | uint32_t inboxSize = 0; 18 | 19 | bool loadedSubredditList = false; 20 | bool refreshSubreddit = false; 21 | 22 | static void in_received_handler(DictionaryIterator *iter, void *context); 23 | static void in_dropped_handler(AppMessageResult reason, void *context); 24 | static void out_failed_handler(DictionaryIterator *failed, AppMessageResult reason, void *context); 25 | static void app_message_send_ready_reply(); 26 | 27 | static void in_received_handler(DictionaryIterator *iter, void *context) 28 | { 29 | Tuple *netimage_begin = dict_find(iter, NETIMAGE_BEGIN); 30 | Tuple *netimage_data = dict_find(iter, NETIMAGE_DATA); 31 | Tuple *netimage_end = dict_find(iter, NETIMAGE_END); 32 | 33 | //DEBUG_MSG("in_received_handler"); 34 | 35 | if(netimage_begin || netimage_data || netimage_end) 36 | { 37 | if(get_netimage_context() != NULL) 38 | { 39 | netimage_receive(iter); 40 | } 41 | else 42 | { 43 | //DEBUG_MSG("null get_netimage_context"); 44 | } 45 | return; 46 | } 47 | 48 | Tuple *thread_id_tuple = dict_find(iter, THREAD_ID); 49 | Tuple *thread_title_tuple = dict_find(iter, THREAD_TITLE); 50 | Tuple *thread_score_tuple = dict_find(iter, THREAD_SCORE); 51 | Tuple *thread_type_tuple = dict_find(iter, THREAD_TYPE); 52 | Tuple *thread_subreddit_tuple = dict_find(iter, THREAD_SUBREDDIT); 53 | 54 | Tuple *thread_body_tuple = dict_find(iter, THREAD_BODY); 55 | 56 | Tuple *thread_comment_tuple = dict_find(iter, THREAD_COMMENT); 57 | 58 | Tuple *user_subreddit_tuple = dict_find(iter, USER_SUBREDDIT); 59 | 60 | Tuple *ready_tuple = dict_find(iter, READY); 61 | 62 | if(ready_tuple) 63 | { 64 | if(ready_tuple->value->uint8 == 2) 65 | { 66 | refreshSubreddit = true; 67 | return; 68 | } 69 | 70 | SetLoggedIn(ready_tuple->value->uint8 == 1); 71 | 72 | app_message_send_ready_reply(); 73 | 74 | return; 75 | } 76 | 77 | if(thread_comment_tuple) 78 | { 79 | DEBUG_MSG("thread_comment_tuple"); 80 | 81 | if(!thread_title_tuple || !thread_score_tuple || !thread_id_tuple || !thread_body_tuple || !thread_type_tuple || !user_subreddit_tuple) 82 | { 83 | // failed to load comment 84 | goto comment_load_failure; 85 | return; 86 | } 87 | 88 | if(current_thread.body != NULL) 89 | { 90 | nt_Free(current_thread.body); 91 | current_thread.body = NULL; 92 | } 93 | 94 | if(current_thread.image != NULL) 95 | { 96 | gbitmap_destroy(current_thread.image); 97 | current_thread.image = NULL; 98 | } 99 | 100 | if(current_thread.author != NULL) 101 | { 102 | nt_Free(current_thread.author); 103 | } 104 | 105 | if(current_thread.score != NULL) 106 | { 107 | nt_Free(current_thread.score); 108 | } 109 | 110 | if(current_thread.comment != NULL) 111 | { 112 | nt_Free(current_thread.comment); 113 | } 114 | 115 | current_thread.author = (char*)nt_Malloc(sizeof(char) * (strlen(thread_title_tuple->value->cstring) + 1)); 116 | if(current_thread.author == NULL) 117 | { 118 | goto comment_load_failure; 119 | } 120 | 121 | current_thread.score = (char*)nt_Malloc(sizeof(char) * (strlen(thread_score_tuple->value->cstring) + 1)); 122 | if(current_thread.score == NULL) 123 | { 124 | nt_Free(current_thread.author); 125 | goto comment_load_failure; 126 | } 127 | 128 | current_thread.comment = (char*)nt_Malloc(sizeof(char) * (strlen(thread_comment_tuple->value->cstring) + 1)); 129 | if(current_thread.comment == NULL) 130 | { 131 | nt_Free(current_thread.author); 132 | nt_Free(current_thread.score); 133 | goto comment_load_failure; 134 | } 135 | 136 | strcpy(current_thread.author, thread_title_tuple->value->cstring); 137 | strcpy(current_thread.score, thread_score_tuple->value->cstring); 138 | strcpy(current_thread.comment, thread_comment_tuple->value->cstring); 139 | 140 | current_thread.depth = thread_type_tuple->value->uint8; 141 | current_thread.index = thread_id_tuple->value->uint8; 142 | current_thread.max = thread_body_tuple->value->uint8; 143 | current_thread.nextDepthPossible = user_subreddit_tuple->value->uint8 == 1 ? true : false; 144 | 145 | comment_load_finished(); 146 | return; 147 | 148 | comment_load_failure: 149 | 150 | if(loading_visible()) 151 | { 152 | loading_disable_dots(); 153 | loading_set_text("Unable to load comments"); 154 | } 155 | 156 | return; 157 | } 158 | 159 | if(user_subreddit_tuple) 160 | { 161 | if(subredditlist_num == -1 || !loading_visible()) 162 | { 163 | DEBUG_MSG("%s", user_subreddit_tuple->value->cstring); 164 | return; 165 | } 166 | 167 | char *subredditlist = user_subreddit_tuple->value->cstring; 168 | int len = strlen(subredditlist); 169 | 170 | int start = 0; 171 | 172 | for(int i=0; i < len; i++) 173 | { 174 | char c = subredditlist[i]; 175 | 176 | if(c == ',') 177 | { 178 | int nameLength = i - start; 179 | if(nameLength == 0) 180 | { 181 | // maybe they put in a double comma by mistake, skip this one 182 | start = i + 1; 183 | continue; 184 | } 185 | 186 | char *name = nt_Malloc(sizeof(char) * (nameLength + 1)); 187 | if(name == NULL) 188 | { 189 | goto done; 190 | } 191 | 192 | strncpy(name, subredditlist + start, i - start); 193 | name[i - start] = '\0'; 194 | 195 | if(user_subreddits == NULL) 196 | { 197 | subredditlist_num = 1; 198 | user_subreddits = (char**)nt_Malloc(sizeof(char*)); 199 | if(user_subreddits == NULL) 200 | { 201 | nt_Free(name); 202 | subredditlist_num--; 203 | goto done; 204 | } 205 | } 206 | else 207 | { 208 | subredditlist_num++; 209 | 210 | char** subredditlist_new = (char**)nt_Malloc(sizeof(char*) * subredditlist_num); 211 | if(subredditlist_new == NULL) 212 | { 213 | nt_Free(name); 214 | subredditlist_num--; 215 | goto done; 216 | } 217 | memcpy(subredditlist_new, user_subreddits, sizeof(char*) * (subredditlist_num - 1)); 218 | 219 | nt_Free(user_subreddits); 220 | 221 | user_subreddits = subredditlist_new; 222 | } 223 | 224 | //DEBUG_MSG("Subreddit: '%s', %d", name, subredditlist_num); 225 | 226 | user_subreddits[subredditlist_num - 1] = name; 227 | 228 | start = i + 1; 229 | } 230 | else if(c == ';') 231 | { 232 | goto done; 233 | } 234 | } 235 | 236 | goto done_skip; 237 | 238 | done: 239 | loading_uninit(); 240 | subredditlist_init(); 241 | 242 | done_skip: 243 | return; 244 | } 245 | 246 | if(thread_type_tuple && thread_type_tuple->value->uint8 == 255) 247 | { 248 | DEBUG_MSG("Done loading thread list"); 249 | subreddit_show_load_more(); 250 | scroll_layer_set_content_size(subreddit_scroll_layer, GSize(144, (thread_loaded + 1) * (THREAD_WINDOW_HEIGHT + THREAD_LAYER_PADDING) + (THREAD_WINDOW_HEIGHT_SELECTED + THREAD_LAYER_PADDING))); 251 | return; 252 | } 253 | 254 | if(thread_title_tuple && thread_score_tuple && thread_type_tuple) 255 | { 256 | DEBUG_MSG("subreddit thread tuple"); 257 | 258 | if(thread_loaded >= MAX_THREADS) 259 | { 260 | //DEBUG_MSG("got too many.."); 261 | return; 262 | } 263 | 264 | if(thread_loaded == 0) 265 | { 266 | subreddit_init(); 267 | } 268 | 269 | struct ThreadData *thread = &threads[thread_loaded]; 270 | 271 | SetThreadTitle(thread, thread_loaded, thread_title_tuple->value->cstring); 272 | SetThreadScore(thread, thread_loaded, thread_score_tuple->value->cstring); 273 | SetThreadSubreddit(thread, thread_loaded, thread_subreddit_tuple ? thread_subreddit_tuple->value->cstring : NULL); 274 | 275 | thread->type = thread_type_tuple->value->uint8; 276 | 277 | layer_set_hidden(thread->layer, false); 278 | 279 | thread_loaded++; 280 | 281 | scroll_layer_set_content_size(subreddit_scroll_layer, GSize(144, thread_loaded * (THREAD_WINDOW_HEIGHT + THREAD_LAYER_PADDING) + THREAD_WINDOW_HEIGHT_SELECTED + THREAD_LAYER_PADDING)); 282 | 283 | if(thread_loaded == 1) 284 | { 285 | subreddit_selection_changed(false); 286 | } 287 | } 288 | 289 | if(thread_id_tuple) 290 | { 291 | if(thread_body_tuple && thread_title_tuple) 292 | { 293 | if(thread_id_tuple->value->uint8 != GetSelectedThreadID()) 294 | { 295 | DEBUG_MSG("loading old thread"); 296 | return; 297 | } 298 | 299 | // thread body 300 | if(current_thread.body != NULL) 301 | { 302 | nt_Free(current_thread.body); 303 | } 304 | 305 | current_thread.body = (char*)nt_Malloc(sizeof(char) * (strlen(thread_body_tuple->value->cstring) + 1)); 306 | if(current_thread.body == NULL) 307 | { 308 | body_fail: if(loading_visible()) 309 | { 310 | loading_disable_dots(); 311 | loading_set_text("Unable to load thread"); 312 | } 313 | return; 314 | } 315 | 316 | strcpy(current_thread.body, thread_body_tuple->value->cstring); 317 | 318 | // thread author 319 | if(current_thread.thread_author != NULL) 320 | { 321 | nt_Free(current_thread.thread_author); 322 | } 323 | 324 | current_thread.thread_author = (char*)nt_Malloc(sizeof(char) * (strlen(thread_title_tuple->value->cstring) + 1)); 325 | if(current_thread.thread_author == NULL) 326 | { 327 | nt_Free(current_thread.body); 328 | current_thread.body = NULL; 329 | goto body_fail; 330 | } 331 | 332 | strcpy(current_thread.thread_author, thread_title_tuple->value->cstring); 333 | 334 | //DEBUG_MSG("filled body, %d", strlen(current_thread.body)); 335 | //DEBUG_MSG("Thread load...?"); 336 | 337 | thread_load_finished(); 338 | 339 | if(thread_body_layer == NULL) 340 | { 341 | return; 342 | } 343 | 344 | text_layer_set_text(thread_body_layer, current_thread.body); 345 | 346 | GSize size = text_layer_get_content_size(thread_body_layer); 347 | size.h += 5; 348 | text_layer_set_size(thread_body_layer, size); 349 | 350 | size.h = window_frame.size.h > size.h ? window_frame.size.h : size.h + 5; 351 | 352 | scroll_layer_set_content_size(thread_scroll_layer, GSize(window_frame.size.w, 22 + size.h + 10)); 353 | 354 | thread_update_comments_position(); 355 | } 356 | else 357 | { 358 | // unable to load subreddit 359 | DEBUG_MSG("Unable to load subreddit"); 360 | 361 | subreddit_init(); 362 | } 363 | } 364 | } 365 | 366 | #ifdef DEBUG_MODE 367 | char* app_message_result_to_string(AppMessageResult reason) 368 | { 369 | switch (reason) 370 | { 371 | case APP_MSG_OK: 372 | return "APP_MSG_OK"; 373 | case APP_MSG_SEND_TIMEOUT: 374 | return "APP_MSG_SEND_TIMEOUT"; 375 | case APP_MSG_SEND_REJECTED: 376 | return "APP_MSG_SEND_REJECTED"; 377 | case APP_MSG_NOT_CONNECTED: 378 | return "APP_MSG_NOT_CONNECTED"; 379 | case APP_MSG_APP_NOT_RUNNING: 380 | return "APP_MSG_APP_NOT_RUNNING"; 381 | case APP_MSG_INVALID_ARGS: 382 | return "APP_MSG_INVALID_ARGS"; 383 | case APP_MSG_BUSY: 384 | return "APP_MSG_BUSY"; 385 | case APP_MSG_BUFFER_OVERFLOW: 386 | return "APP_MSG_BUFFER_OVERFLOW"; 387 | case APP_MSG_ALREADY_RELEASED: 388 | return "APP_MSG_ALREADY_RELEASED"; 389 | case APP_MSG_CALLBACK_ALREADY_REGISTERED: 390 | return "APP_MSG_CALLBACK_ALREADY_REGISTERED"; 391 | case APP_MSG_CALLBACK_NOT_REGISTERED: 392 | return "APP_MSG_CALLBACK_NOT_REGISTERED"; 393 | case APP_MSG_OUT_OF_MEMORY: 394 | return "APP_MSG_OUT_OF_MEMORY"; 395 | case APP_MSG_CLOSED: 396 | return "APP_MSG_CLOSED"; 397 | case APP_MSG_INTERNAL_ERROR: 398 | return "APP_MSG_INTERNAL_ERROR"; 399 | default: 400 | return "UNKNOWN ERROR"; 401 | } 402 | } 403 | #endif 404 | 405 | static void in_dropped_handler(AppMessageResult reason, void *context) 406 | { 407 | DEBUG_MSG("App Message Dropped! %d, %s", (int)reason, app_message_result_to_string(reason)); 408 | /* 409 | if(loading_visible()) 410 | { 411 | loading_set_text("An error occured\nPlease try again"); 412 | } 413 | */ 414 | } 415 | 416 | static void out_failed_handler(DictionaryIterator *failed, AppMessageResult reason, void *context) 417 | { 418 | DEBUG_MSG("App Message Failed to Send! %d, %s", (int)reason, app_message_result_to_string(reason)); 419 | 420 | Tuple* tuple = dict_read_first(failed); 421 | 422 | while(tuple != NULL) 423 | { 424 | DEBUG_MSG("[%ld]", tuple->key); 425 | 426 | tuple = dict_read_next(failed); 427 | } 428 | 429 | if(reason == APP_MSG_NOT_CONNECTED) 430 | { 431 | loading_disconnected(); 432 | } 433 | /*else 434 | { 435 | if(loading_visible()) 436 | { 437 | loading_set_text("An error occured\nPlease try again"); 438 | } 439 | }*/ 440 | } 441 | 442 | void app_message_init() 443 | { 444 | app_message_register_inbox_received(in_received_handler); 445 | app_message_register_inbox_dropped(in_dropped_handler); 446 | app_message_register_outbox_failed(out_failed_handler); 447 | 448 | int max = app_message_inbox_size_maximum(); 449 | 450 | inboxSize = max > 1024 ? 1024 : max; 451 | 452 | app_message_open(inboxSize, 256); 453 | } 454 | 455 | uint32_t app_message_index_size() 456 | { 457 | return inboxSize; 458 | } 459 | 460 | static void app_message_send_ready_reply() 461 | { 462 | DictionaryIterator *iter; 463 | app_message_outbox_begin(&iter); 464 | 465 | if(iter == NULL) 466 | { 467 | return; 468 | } 469 | 470 | uint32_t chunk_size = app_message_index_size() - 8; 471 | 472 | dict_write_int(iter, CHUNK_SIZE, &chunk_size, sizeof(uint32_t), false); 473 | 474 | if(refreshSubreddit) 475 | { 476 | refreshSubreddit = false; 477 | 478 | subreddit_load_setup(); 479 | 480 | Tuplet tuple = TupletCString(VIEW_SUBREDDIT, "0"); 481 | 482 | dict_write_tuplet(iter, &tuple); 483 | } 484 | 485 | app_message_outbox_send(); 486 | } 487 | -------------------------------------------------------------------------------- /src/AppMessages.h: -------------------------------------------------------------------------------- 1 | #ifndef APP_MESSAGES_H 2 | #define APP_MESSAGES_H 3 | 4 | #include 5 | 6 | extern char* *user_subreddits; 7 | extern int subredditlist_num; 8 | extern int thread_loaded; 9 | extern ScrollLayer *subreddit_scroll_layer; 10 | extern TextLayer *loading_text_layer; 11 | extern ScrollLayer *thread_scroll_layer; 12 | extern TextLayer *thread_body_layer; 13 | 14 | void app_message_init(); 15 | uint32_t app_message_index_size(); 16 | 17 | #ifdef DEBUG_MODE 18 | char* app_message_result_to_string(AppMessageResult reason); 19 | #endif 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/CommentWindow.c: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Comment Window 3 | ********************************/ 4 | 5 | #include "Rebble.h" 6 | #include "CommentWindow.h" 7 | #include "ThreadWindow.h" 8 | #include "LoadingWindow.h" 9 | 10 | extern GBitmap *bitmap_upvote; 11 | 12 | Window *window_comment; 13 | 14 | ScrollLayer *comment_scroll_layer; 15 | Layer *comment_header_layer; 16 | TextLayer *comment_body_layer; 17 | 18 | GRect comment_author_rect; 19 | GRect comment_author_fill_rect; 20 | GRect comment_upvote_rect; 21 | GRect comment_score_rect; 22 | 23 | static void comment_header_layer_update_proc(Layer *layer, GContext *ctx); 24 | static void comment_click_config(void *context); 25 | static void comment_button_back(ClickRecognizerRef recognizer, void *context); 26 | static void comment_button_up(ClickRecognizerRef recognizer, void *context); 27 | static void comment_button_select(ClickRecognizerRef recognizer, void *context); 28 | static void comment_button_down(ClickRecognizerRef recognizer, void *context); 29 | static void comment_auto_resize_body(); 30 | 31 | void comment_load(int dir) 32 | { 33 | Tuplet tuple = TupletInteger(LOAD_COMMENTS, dir); 34 | 35 | DictionaryIterator *iter; 36 | app_message_outbox_begin(&iter); 37 | 38 | if (iter == NULL) 39 | { 40 | return; 41 | } 42 | 43 | dict_write_tuplet(iter, &tuple); 44 | 45 | app_message_outbox_send(); 46 | 47 | loading_init(); 48 | 49 | loading_set_text("Loading Comments"); 50 | } 51 | 52 | void comment_load_finished() 53 | { 54 | if(loading_visible()) 55 | { 56 | loading_uninit(); 57 | comment_init(); 58 | } 59 | } 60 | 61 | void comment_init() 62 | { 63 | if(window_stack_contains_window(window_comment)) 64 | { 65 | // we are changing comments 66 | // don't do anything.. 67 | layer_mark_dirty(comment_header_layer); 68 | text_layer_set_text(comment_body_layer, current_thread.comment); 69 | 70 | comment_auto_resize_body(); 71 | 72 | scroll_layer_set_content_offset(comment_scroll_layer, GPoint(0, 0), false); 73 | } 74 | else 75 | { 76 | window_stack_pop(false); 77 | window_stack_push(window_comment, true); 78 | } 79 | } 80 | 81 | void comment_window_load(Window *window) 82 | { 83 | comment_author_rect = GRect(1, -3, window_frame.size.w - 51, 22); 84 | comment_author_fill_rect = GRect(1, 1, window_frame.size.w - 51, 18); 85 | comment_upvote_rect = GRect(window_frame.size.w - 50, 2, 12, 14); 86 | comment_score_rect = GRect(window_frame.size.w - 35, -3, 35, 22); 87 | 88 | comment_scroll_layer = scroll_layer_create(window_frame); 89 | 90 | scroll_layer_set_shadow_hidden(comment_scroll_layer, true); 91 | scroll_layer_set_click_config_onto_window(comment_scroll_layer, window); 92 | scroll_layer_set_content_size(comment_scroll_layer, GSize(window_frame.size.w, 0)); 93 | scroll_layer_set_content_offset(comment_scroll_layer, GPoint(0, 0), false); 94 | 95 | layer_add_child(window_get_root_layer(window), scroll_layer_get_layer(comment_scroll_layer)); 96 | 97 | ScrollLayerCallbacks scrollOverride = 98 | { 99 | .click_config_provider = &comment_click_config, 100 | .content_offset_changed_handler = NULL 101 | }; 102 | scroll_layer_set_callbacks(comment_scroll_layer, scrollOverride); 103 | 104 | comment_header_layer = layer_create(GRect(0, 0, window_frame.size.w, THREAD_WINDOW_HEIGHT)); 105 | layer_set_update_proc(comment_header_layer, comment_header_layer_update_proc); 106 | scroll_layer_add_child(comment_scroll_layer, comment_header_layer); 107 | 108 | comment_body_layer = text_layer_create(GRect(0, THREAD_WINDOW_HEIGHT, window_frame.size.w, 10000)); 109 | text_layer_set_font(comment_body_layer, GetFont()); 110 | text_layer_set_text(comment_body_layer, current_thread.comment); 111 | 112 | scroll_layer_add_child(comment_scroll_layer, text_layer_get_layer(comment_body_layer)); 113 | 114 | comment_auto_resize_body(); 115 | } 116 | 117 | void comment_window_unload(Window *window) 118 | { 119 | if(current_thread.author != NULL) 120 | { 121 | nt_Free(current_thread.author); 122 | current_thread.author = NULL; 123 | } 124 | 125 | if(current_thread.score != NULL) 126 | { 127 | nt_Free(current_thread.score); 128 | current_thread.score = NULL; 129 | } 130 | 131 | if(current_thread.comment != NULL) 132 | { 133 | nt_Free(current_thread.comment); 134 | current_thread.comment = NULL; 135 | } 136 | 137 | layer_destroy(comment_header_layer); 138 | text_layer_destroy(comment_body_layer); 139 | 140 | scroll_layer_destroy(comment_scroll_layer); 141 | } 142 | 143 | static void comment_click_config(void *context) 144 | { 145 | window_single_click_subscribe(BUTTON_ID_BACK , (ClickHandler) comment_button_back); 146 | window_long_click_subscribe(BUTTON_ID_UP, 0, comment_button_up, NULL); 147 | window_single_click_subscribe(BUTTON_ID_SELECT, (ClickHandler) comment_button_select); 148 | window_long_click_subscribe(BUTTON_ID_DOWN, 0, comment_button_down, NULL); 149 | } 150 | 151 | static void comment_button_back(ClickRecognizerRef recognizer, void *context) 152 | { 153 | if(current_thread.depth == 0) 154 | { 155 | // it should look like you're going back 156 | window_stack_pop(true); 157 | loading_disable_animate(); 158 | thread_load(); 159 | } 160 | else 161 | { 162 | current_thread.depth--; 163 | loading_disable_animate(); 164 | loading_animate_pop(); 165 | comment_load(3); 166 | } 167 | } 168 | 169 | static void comment_button_up(ClickRecognizerRef recognizer, void *context) 170 | { 171 | if(current_thread.index <= 0) 172 | { 173 | vibes_double_pulse(); 174 | } 175 | else 176 | { 177 | loading_disable_animate(); 178 | comment_load(1); 179 | } 180 | } 181 | 182 | static void comment_button_select(ClickRecognizerRef recognizer, void *context) 183 | { 184 | !current_thread.nextDepthPossible ? vibes_double_pulse() : comment_load(2); 185 | } 186 | 187 | static void comment_button_down(ClickRecognizerRef recognizer, void *context) 188 | { 189 | if(current_thread.index >= (current_thread.max - 1)) 190 | { 191 | vibes_double_pulse(); 192 | } 193 | else 194 | { 195 | loading_disable_animate(); 196 | comment_load(0); 197 | } 198 | } 199 | 200 | static void comment_header_layer_update_proc(Layer *layer, GContext *ctx) 201 | { 202 | graphics_context_set_text_color(ctx, GColorBlack); 203 | graphics_draw_text(ctx, current_thread.score, GetFont(), comment_score_rect, GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); 204 | 205 | if(current_thread.thread_author != NULL && current_thread.author != NULL && strcmp(current_thread.thread_author, current_thread.author) == 0) 206 | { 207 | graphics_context_set_fill_color(ctx, GColorBlack); 208 | 209 | GSize textSize = graphics_text_layout_get_content_size(current_thread.author, GetFont(), comment_author_rect, GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft); 210 | comment_author_fill_rect.size.w = textSize.w + 1; 211 | graphics_fill_rect(ctx, comment_author_fill_rect, 0, GCornerNone); 212 | 213 | graphics_context_set_text_color(ctx, GColorWhite); 214 | } 215 | 216 | graphics_draw_text(ctx, current_thread.author, GetFont(), comment_author_rect, GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); 217 | 218 | graphics_draw_bitmap_in_rect(ctx, bitmap_upvote, comment_upvote_rect); 219 | 220 | GPoint point = GPoint(0, THREAD_WINDOW_HEIGHT - 1); 221 | 222 | for(int i=0; i < (current_thread.depth + 1); i++) 223 | { 224 | GPoint nextPoint = GPoint(point.x + 20, point.y); 225 | graphics_draw_line(ctx, point, nextPoint); 226 | point = nextPoint; 227 | point.x += 10; 228 | } 229 | } 230 | 231 | static void comment_auto_resize_body() 232 | { 233 | GSize size = text_layer_get_content_size(comment_body_layer); 234 | size.h += 5; 235 | text_layer_set_size(comment_body_layer, size); 236 | scroll_layer_set_content_size(comment_scroll_layer, GSize(window_frame.size.w, THREAD_WINDOW_HEIGHT + size.h + 5)); 237 | } 238 | -------------------------------------------------------------------------------- /src/CommentWindow.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMENT_WINDOW_H 2 | #define COMMENT_WINDOW_H 3 | 4 | #include 5 | 6 | void comment_load(int dir); 7 | void comment_load_finished(); 8 | 9 | void comment_init(); 10 | 11 | void comment_window_load(Window *window); 12 | void comment_window_unload(Window *window); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /src/LoadingWindow.c: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Loading Window 3 | ********************************/ 4 | 5 | #include "Rebble.h" 6 | #include "LoadingWindow.h" 7 | 8 | #define CONNECTION_LOST_TEXT "Lost connection\nto phone\n" 9 | 10 | Window *window_loading; 11 | 12 | GRect window_frame; 13 | 14 | GBitmap *icon; 15 | 16 | Layer *loading_layer; 17 | TextLayer *loading_text_layer; 18 | TextLayer *loading_text_progress_layer; 19 | 20 | char loading_text[3]; 21 | int dots = 0; 22 | bool dotting = true; 23 | bool animate = true; 24 | bool animatePop = false; 25 | 26 | static void loading_layer_update_proc(Layer *layer, GContext *ctx); 27 | static void loading_progress_update(void *data); 28 | 29 | void loading_init() 30 | { 31 | dots = 0; 32 | dotting = true; 33 | 34 | window_stack_push(window_loading, animate); 35 | 36 | animate = true; 37 | 38 | if(IsBluetoothConnected()) 39 | { 40 | text_layer_set_text(loading_text_layer, "Loading"); 41 | } 42 | else 43 | { 44 | loading_disable_dots(); 45 | text_layer_set_text(loading_text_layer, CONNECTION_LOST_TEXT); 46 | } 47 | } 48 | 49 | void loading_uninit() 50 | { 51 | window_stack_remove(window_loading, animatePop); 52 | animatePop = false; 53 | } 54 | 55 | void loading_window_load(Window *window) 56 | { 57 | Layer *window_layer = window_get_root_layer(window); 58 | window_frame = layer_get_frame(window_layer); 59 | 60 | icon = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_ICON); 61 | 62 | loading_layer = layer_create(window_frame); 63 | layer_set_update_proc(loading_layer, loading_layer_update_proc); 64 | 65 | GRect rect = window_frame; 66 | rect.origin.y = window_frame.size.h * 0.55; 67 | rect.size.h = rect.origin.y; 68 | 69 | loading_text_layer = text_layer_create(rect); 70 | text_layer_set_font(loading_text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24)); 71 | text_layer_set_text_alignment(loading_text_layer, GTextAlignmentCenter); 72 | layer_add_child(loading_layer, text_layer_get_layer(loading_text_layer)); 73 | 74 | loading_text_progress_layer = text_layer_create(GRect(window_frame.size.w * 0.6, window_frame.size.h * 0.385, window_frame.size.w * 0.4, 30)); 75 | text_layer_set_text(loading_text_progress_layer, loading_text); 76 | layer_add_child(loading_layer, text_layer_get_layer(loading_text_progress_layer)); 77 | 78 | layer_add_child(window_layer, loading_layer); 79 | 80 | init_timer(app_timer_register(500, loading_progress_update, NULL)); 81 | } 82 | 83 | void loading_window_disappear(Window *window) 84 | { 85 | cancel_timer(); 86 | } 87 | 88 | void loading_window_unload(Window *window) 89 | { 90 | text_layer_destroy(loading_text_layer); 91 | text_layer_destroy(loading_text_progress_layer); 92 | layer_destroy(loading_layer); 93 | gbitmap_destroy(icon); 94 | } 95 | 96 | static void loading_layer_update_proc(Layer *layer, GContext *ctx) 97 | { 98 | graphics_context_set_fill_color(ctx, GColorBlack); 99 | graphics_draw_bitmap_in_rect(ctx, icon, GRect(window_frame.size.w / 2 - 12, window_frame.size.h / 2 - 30, 24, 28)); 100 | } 101 | 102 | static void loading_progress_update(void *data) 103 | { 104 | if(!dotting) 105 | { 106 | return; 107 | } 108 | 109 | for(int i=0; i < dots; i++) 110 | { 111 | loading_text[i] = '.'; 112 | } 113 | 114 | loading_text[dots] = '\0'; 115 | 116 | dots++; 117 | 118 | if(dots > 3) 119 | { 120 | dots = 0; 121 | } 122 | 123 | layer_mark_dirty(text_layer_get_layer(loading_text_progress_layer)); 124 | 125 | init_timer(app_timer_register(500, loading_progress_update, NULL)); 126 | } 127 | 128 | bool loading_visible() 129 | { 130 | return window_stack_contains_window(window_loading); 131 | } 132 | 133 | void loading_set_text(char *loadingText) 134 | { 135 | if(IsBluetoothConnected()) 136 | { 137 | text_layer_set_text(loading_text_layer, loadingText); 138 | } 139 | } 140 | 141 | void loading_disable_dots() 142 | { 143 | dotting = false; 144 | loading_text[0] = '\0'; 145 | } 146 | 147 | void loading_disconnected() 148 | { 149 | loading_disable_dots(); 150 | loading_set_text(CONNECTION_LOST_TEXT); 151 | } 152 | 153 | void loading_disable_animate() 154 | { 155 | animate = false; 156 | } 157 | 158 | void loading_animate_pop() 159 | { 160 | animatePop = true; 161 | } 162 | -------------------------------------------------------------------------------- /src/LoadingWindow.h: -------------------------------------------------------------------------------- 1 | #ifndef LOADING_WINDOW_H 2 | #define LOADING_WINDOW_H 3 | 4 | #include 5 | 6 | Window* loading_create_window(); 7 | 8 | void loading_init(); 9 | void loading_uninit(); 10 | 11 | void loading_window_load(Window *window); 12 | void loading_window_disappear(Window *window); 13 | void loading_window_unload(Window *window); 14 | 15 | bool loading_visible(); 16 | void loading_set_text(char *loadingText); 17 | void loading_disable_dots(); 18 | void loading_disable_animate(); 19 | void loading_animate_pop(); 20 | 21 | void loading_disconnected(); 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /src/Rebble.c: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Rebble 3 | ********************************/ 4 | 5 | #include "Rebble.h" 6 | #include "LoadingWindow.h" 7 | #include "ThreadWindow.h" 8 | #include "SubredditWindow.h" 9 | #include "SubredditListWindow.h" 10 | #include "ThreadMenuWindow.h" 11 | #include "CommentWindow.h" 12 | #include "AppMessages.h" 13 | #include "netimage.h" 14 | 15 | bool loggedIn = false; 16 | 17 | int selectedThread = 0; 18 | AppTimer *timerHandle = NULL; 19 | NetImageContext *netimage_ctx = NULL; 20 | 21 | #ifdef USE_PERSIST_STRINGS 22 | char persist_index_title = -1; 23 | char persist_index_score = -1; 24 | char persist_index_subreddit = -1; 25 | 26 | char persist_string_title[PERSIST_STRING_MAX_LENGTH]; 27 | char persist_string_score[PERSIST_STRING_MAX_LENGTH]; 28 | char persist_string_subreddit[PERSIST_STRING_MAX_LENGTH]; 29 | #endif 30 | 31 | struct ThreadData threads[MAX_THREADS]; 32 | 33 | GFont *font = NULL; 34 | GFont *biggerFont = NULL; 35 | 36 | GFont *GetFont() 37 | { 38 | return font; 39 | } 40 | 41 | GFont *GetBiggerFont() 42 | { 43 | return biggerFont; 44 | } 45 | 46 | bool IsLoggedIn() 47 | { 48 | return loggedIn; 49 | } 50 | 51 | void SetLoggedIn(bool lin) 52 | { 53 | loggedIn = lin; 54 | } 55 | 56 | /******************************* 57 | Reddit Actions 58 | ********************************/ 59 | 60 | void LoadSubreddit(char *name) 61 | { 62 | DEBUG_MSG("LoadSubreddit: %s", name); 63 | 64 | Tuplet tuple = TupletCString(VIEW_SUBREDDIT, name); 65 | 66 | DictionaryIterator *iter; 67 | app_message_outbox_begin(&iter); 68 | 69 | if (iter == NULL) 70 | { 71 | DEBUG_MSG("failed LoadSubreddit"); 72 | return; 73 | } 74 | 75 | dict_write_tuplet(iter, &tuple); 76 | 77 | app_message_outbox_send(); 78 | } 79 | 80 | void LoadThread(int index) 81 | { 82 | DEBUG_MSG("LoadThread"); 83 | 84 | Tuplet tuple = TupletInteger(VIEW_THREAD, index); 85 | 86 | DictionaryIterator *iter; 87 | app_message_outbox_begin(&iter); 88 | 89 | if (iter == NULL) 90 | { 91 | return; 92 | } 93 | 94 | dict_write_tuplet(iter, &tuple); 95 | 96 | app_message_outbox_send(); 97 | } 98 | 99 | void LoadThreadNext() 100 | { 101 | DEBUG_MSG("LoadThreadNext"); 102 | 103 | Tuplet tuple = TupletCString(VIEW_SUBREDDIT_NEXT, " "); 104 | 105 | DictionaryIterator *iter; 106 | app_message_outbox_begin(&iter); 107 | 108 | if (iter == NULL) 109 | { 110 | return; 111 | } 112 | 113 | dict_write_tuplet(iter, &tuple); 114 | 115 | app_message_outbox_send(); 116 | } 117 | 118 | void UpvoteThread(int index) 119 | { 120 | DEBUG_MSG("UpvoteThread"); 121 | 122 | Tuplet tuple = TupletInteger(THREAD_UPVOTE, index); 123 | 124 | DictionaryIterator *iter; 125 | app_message_outbox_begin(&iter); 126 | 127 | if (iter == NULL) 128 | { 129 | return; 130 | } 131 | 132 | dict_write_tuplet(iter, &tuple); 133 | 134 | app_message_outbox_send(); 135 | } 136 | 137 | void DownvoteThread(int index) 138 | { 139 | DEBUG_MSG("DownvoteThread"); 140 | 141 | Tuplet tuple = TupletInteger(THREAD_DOWNVOTE, index); 142 | 143 | DictionaryIterator *iter; 144 | app_message_outbox_begin(&iter); 145 | 146 | if (iter == NULL) 147 | { 148 | return; 149 | } 150 | 151 | dict_write_tuplet(iter, &tuple); 152 | 153 | app_message_outbox_send(); 154 | } 155 | 156 | void SaveThread(int index) 157 | { 158 | DEBUG_MSG("SaveThread"); 159 | 160 | Tuplet tuple = TupletInteger(THREAD_SAVE, index); 161 | 162 | DictionaryIterator *iter; 163 | app_message_outbox_begin(&iter); 164 | 165 | if (iter == NULL) 166 | { 167 | return; 168 | } 169 | 170 | dict_write_tuplet(iter, &tuple); 171 | 172 | app_message_outbox_send(); 173 | } 174 | 175 | /******************************* 176 | Memory Utilities 177 | ********************************/ 178 | 179 | #ifdef DEBUG_MODE 180 | 181 | int count = 0; 182 | 183 | void *nt_Malloc_Raw(size_t size, const char *function, int line) 184 | { 185 | void *pointer = malloc(size); 186 | 187 | if(pointer == NULL) 188 | { 189 | DEBUG_MSG("nt_Malloc: Failed to mallocc %d", size); 190 | DEBUG_MSG("%s: %d", function, line); 191 | } 192 | else 193 | { 194 | //DEBUG_MSG("nt_Malloc: %d, %s: %d", size, function, line); 195 | } 196 | 197 | count++; 198 | 199 | return pointer; 200 | } 201 | 202 | void nt_Free_Raw(void *pointer) 203 | { 204 | count--; 205 | 206 | if(pointer == NULL) 207 | { 208 | count++; 209 | DEBUG_MSG("nt_Free: Tried to freee NULL"); 210 | return; 211 | } 212 | 213 | free(pointer); 214 | pointer = NULL; 215 | } 216 | 217 | void nt_Stats() 218 | { 219 | DEBUG_MSG("nt_Stats: %d", count); 220 | } 221 | 222 | #endif 223 | 224 | /******************************* 225 | Thread Utilities 226 | ********************************/ 227 | 228 | struct ThreadData *GetThread(int index) 229 | { 230 | return &threads[index]; 231 | } 232 | 233 | struct ThreadData *GetSelectedThread() 234 | { 235 | return &threads[selectedThread]; 236 | } 237 | 238 | void SetSelectedThreadID(int index) 239 | { 240 | selectedThread = index; 241 | } 242 | 243 | int GetSelectedThreadID() 244 | { 245 | return selectedThread; 246 | } 247 | 248 | char* GetThreadTitle(int index) 249 | { 250 | #ifdef USE_PERSIST_STRINGS 251 | if(persist_index_title != index) 252 | { 253 | persist_index_title = index; 254 | persist_read_string(PERSIST_OFFSET_THREAD_TITLE + index, persist_string_title, PERSIST_STRING_MAX_LENGTH); 255 | } 256 | return persist_string_title; 257 | #else 258 | return threads[index].title; 259 | #endif 260 | } 261 | 262 | char* GetThreadScore(int index) 263 | { 264 | #ifdef USE_PERSIST_STRINGS 265 | if(persist_index_score != index) 266 | { 267 | persist_index_score = index; 268 | persist_read_string(PERSIST_OFFSET_THREAD_SCORE + index, persist_string_score, PERSIST_STRING_MAX_LENGTH); 269 | } 270 | return persist_string_score; 271 | #else 272 | return threads[index].score; 273 | #endif 274 | } 275 | 276 | char* GetThreadSubreddit(int index) 277 | { 278 | #ifdef USE_PERSIST_STRINGS 279 | if(persist_index_subreddit != index) 280 | { 281 | persist_index_subreddit = index; 282 | if(persist_read_string(PERSIST_OFFSET_THREAD_SUBREDDIT + index, persist_string_subreddit, PERSIST_STRING_MAX_LENGTH) == E_DOES_NOT_EXIST) 283 | { 284 | return NULL; 285 | } 286 | } 287 | return persist_string_subreddit; 288 | #else 289 | return threads[index].subreddit; 290 | #endif 291 | } 292 | 293 | void SetThreadTitle(struct ThreadData* thread, int index, char* str) 294 | { 295 | #ifdef USE_PERSIST_STRINGS 296 | persist_write_string(PERSIST_OFFSET_THREAD_SCORE + index, str); 297 | #else 298 | if(thread->title != NULL) 299 | { 300 | nt_Free(thread->title); 301 | } 302 | thread->title = (char*)nt_Malloc(sizeof(char) * (strlen(str) + 1)); 303 | strcpy(thread->title, str); 304 | #endif 305 | } 306 | 307 | void SetThreadScore(struct ThreadData* thread, int index, char* str) 308 | { 309 | #ifdef USE_PERSIST_STRINGS 310 | persist_write_string(PERSIST_OFFSET_THREAD_SCORE + index, str); 311 | #else 312 | if(thread->score != NULL) 313 | { 314 | nt_Free(thread->score); 315 | } 316 | thread->score = (char*)nt_Malloc(sizeof(char) * (strlen(str) + 1)); 317 | strcpy(thread->score, str); 318 | #endif 319 | } 320 | 321 | void SetThreadSubreddit(struct ThreadData* thread, int index, char* str) 322 | { 323 | #ifdef USE_PERSIST_STRINGS 324 | if(str != NULL) 325 | { 326 | persist_write_string(PERSIST_OFFSET_THREAD_SUBREDDIT + index, str); 327 | } 328 | else 329 | { 330 | persist_delete(PERSIST_OFFSET_THREAD_SUBREDDIT + index); 331 | } 332 | #else 333 | if(thread->subreddit != NULL) 334 | { 335 | nt_Free(thread->subreddit); 336 | } 337 | if(str != NULL) 338 | { 339 | thread->subreddit = (char*)nt_Malloc(sizeof(char) * (strlen(str) + 1)); 340 | strcpy(thread->subreddit, str); 341 | } 342 | #endif 343 | } 344 | 345 | /******************************* 346 | Timer 347 | ********************************/ 348 | 349 | void init_timer(AppTimer *handle) 350 | { 351 | //DEBUG_MSG("init_timer"); 352 | //cancel_timer(); 353 | timerHandle = handle; 354 | } 355 | 356 | void cancel_timer() 357 | { 358 | //DEBUG_MSG("cancel_timer"); 359 | if(timerHandle != NULL) 360 | { 361 | app_timer_cancel(timerHandle); 362 | timerHandle = NULL; 363 | } 364 | } 365 | 366 | /******************************* 367 | Net Image 368 | ********************************/ 369 | 370 | void init_netimage(int index) 371 | { 372 | DEBUG_MSG("init_netimage %d", index); 373 | free_netimage(); 374 | netimage_ctx = netimage_create_context(callback_netimage); 375 | netimage_request(index); 376 | } 377 | 378 | void callback_netimage(GBitmap *image) 379 | { 380 | DEBUG_MSG("callback_netimage"); 381 | thread_display_image(image); 382 | } 383 | 384 | NetImageContext *get_netimage_context() 385 | { 386 | return netimage_ctx; 387 | } 388 | 389 | void free_netimage() 390 | { 391 | if(netimage_ctx != NULL) 392 | { 393 | netimage_destroy_context(netimage_ctx); 394 | netimage_ctx = NULL; 395 | } 396 | } 397 | 398 | /******************************* 399 | Bluetooth Handler 400 | ********************************/ 401 | 402 | bool bluetoothConnected = true; 403 | 404 | bool IsBluetoothConnected() 405 | { 406 | return bluetoothConnected; 407 | } 408 | 409 | void OnBluetoothConnection(bool connected) 410 | { 411 | DEBUG_MSG("OnBluetoothConnection: %s", connected ? "woo" : "nope"); 412 | bluetoothConnected = connected; 413 | loading_disconnected(); 414 | } 415 | 416 | /******************************* 417 | Main 418 | ********************************/ 419 | 420 | inline void windows_create() 421 | { 422 | window_loading = window_create(); 423 | window_set_window_handlers(window_loading, (WindowHandlers) 424 | { 425 | .load = loading_window_load, 426 | .disappear = loading_window_disappear, 427 | .unload = loading_window_unload, 428 | }); 429 | 430 | window_subreddit = window_create(); 431 | window_set_window_handlers(window_subreddit, (WindowHandlers) 432 | { 433 | .load = subreddit_window_load, 434 | .appear = subreddit_window_appear, 435 | .disappear = subreddit_window_disappear, 436 | .unload = subreddit_window_unload, 437 | }); 438 | 439 | window_thread = window_create(); 440 | window_set_window_handlers(window_thread, (WindowHandlers) 441 | { 442 | .load = thread_window_load, 443 | .appear = thread_window_appear, 444 | .disappear = thread_window_disappear, 445 | .unload = thread_window_unload, 446 | }); 447 | 448 | window_threadmenu = window_create(); 449 | window_set_window_handlers(window_threadmenu, (WindowHandlers) 450 | { 451 | .load = threadmenu_window_load, 452 | .unload = threadmenu_window_unload, 453 | }); 454 | 455 | window_subredditlist = window_create(); 456 | window_set_window_handlers(window_subredditlist, (WindowHandlers) 457 | { 458 | .load = subredditlist_window_load, 459 | .unload = subredditlist_window_unload, 460 | }); 461 | 462 | window_comment = window_create(); 463 | window_set_window_handlers(window_comment, (WindowHandlers) 464 | { 465 | .load = comment_window_load, 466 | .unload = comment_window_unload, 467 | }); 468 | } 469 | 470 | inline void windows_destroy() 471 | { 472 | window_destroy(window_loading); 473 | window_destroy(window_subredditlist); 474 | window_destroy(window_thread); 475 | window_destroy(window_threadmenu); 476 | window_destroy(window_subreddit); 477 | window_destroy(window_comment); 478 | } 479 | 480 | int main() 481 | { 482 | current_thread.author = NULL; 483 | current_thread.score = NULL; 484 | current_thread.body = NULL; 485 | current_thread.comment = NULL; 486 | current_thread.image = NULL; 487 | current_thread.thread_author = NULL; 488 | 489 | #ifndef USE_PERSIST_STRINGS 490 | for(int i = 0; i < MAX_THREADS; ++i) 491 | { 492 | struct ThreadData *thread = GetThread(i); 493 | thread->title = NULL; 494 | thread->score = NULL; 495 | thread->subreddit = NULL; 496 | } 497 | #endif 498 | 499 | font = fonts_get_system_font(FONT_KEY_GOTHIC_18); 500 | biggerFont = fonts_get_system_font(FONT_KEY_GOTHIC_24); 501 | 502 | bluetoothConnected = bluetooth_connection_service_peek(); 503 | bluetooth_connection_service_subscribe(OnBluetoothConnection); 504 | 505 | /* 506 | for(int i=0; i < 20; i++) 507 | { 508 | char *test = nt_Malloc(PERSIST_STRING_MAX_LENGTH * sizeof(char)); 509 | strcpy(test, "hey"); 510 | test[4] = '\0'; 511 | 512 | if(persist_exists(i)) 513 | { 514 | test[0] = '\0'; 515 | persist_read_string(i, test, PERSIST_STRING_MAX_LENGTH); 516 | MSG("%d: %s", i, test); 517 | persist_delete(i); 518 | } 519 | //persist_write_string(i, test); 520 | 521 | nt_Free(test); 522 | } 523 | */ 524 | 525 | //app_comm_set_sniff_interval(SNIFF_INTERVAL_REDUCED); 526 | 527 | /////////////////////////////////////// 528 | 529 | windows_create(); 530 | 531 | app_message_init(); 532 | 533 | loading_init(); 534 | 535 | app_event_loop(); 536 | 537 | windows_destroy(); 538 | 539 | /////////////////////////////////////// 540 | 541 | bluetooth_connection_service_unsubscribe(); 542 | 543 | #ifndef USE_PERSIST_STRINGS 544 | for(int i = 0; i < MAX_THREADS; ++i) 545 | { 546 | struct ThreadData *thread = GetThread(i); 547 | 548 | if(thread->title != NULL) 549 | { 550 | nt_Free(thread->title); 551 | } 552 | 553 | if(thread->score != NULL) 554 | { 555 | nt_Free(thread->score); 556 | } 557 | 558 | if(thread->subreddit != NULL) 559 | { 560 | nt_Free(thread->subreddit); 561 | } 562 | } 563 | #endif 564 | 565 | if(current_thread.thread_author != NULL) 566 | { 567 | nt_Free(current_thread.thread_author); 568 | } 569 | 570 | if(user_subreddits != NULL) 571 | { 572 | for(int i = 0; i < subredditlist_num; i++) 573 | { 574 | nt_Free(user_subreddits[i]); 575 | } 576 | 577 | nt_Free(user_subreddits); 578 | } 579 | 580 | free_netimage(); 581 | 582 | #ifdef DEBUG_MODE 583 | nt_Stats(); 584 | #endif 585 | } 586 | -------------------------------------------------------------------------------- /src/Rebble.h: -------------------------------------------------------------------------------- 1 | #ifndef REBBLE_H 2 | #define REBBLE_H 3 | 4 | #include 5 | #include "netimage.h" 6 | 7 | #define MAX_THREADS 15 8 | #define TITLE_SCROLL_SPEED 100 9 | 10 | #define THREAD_LAYER_PADDING 8 11 | #define THREAD_LAYER_PADDING_HALF (THREAD_LAYER_PADDING / 2) 12 | #define THREAD_LAYER_PADDING_TEXT_LEFT 2 13 | 14 | #define THREAD_WINDOW_HEIGHT 22 15 | #define THREAD_WINDOW_HEIGHT_SELECTED (THREAD_WINDOW_HEIGHT * 2) 16 | #define THREAD_WINDOW_PADDING_TEXT_LEFT 2 17 | 18 | #define LOAD_COMMENTS_HEIGHT 32 19 | 20 | //#define USE_PERSIST_STRINGS 21 | 22 | #ifdef USE_PERSIST_STRINGS 23 | #define PERSIST_OFFSET_THREAD_TITLE 4096 24 | #define PERSIST_OFFSET_THREAD_SCORE 5120 25 | #define PERSIST_OFFSET_THREAD_SUBREDDIT 6144 26 | #endif 27 | 28 | //#define DEBUG_MODE 29 | 30 | #ifdef DEBUG_MODE 31 | #define DEBUG_MSG(args...) APP_LOG(APP_LOG_LEVEL_DEBUG, args) 32 | #else 33 | #define DEBUG_MSG(args...) 34 | #endif 35 | 36 | #define MSG(args...) APP_LOG(APP_LOG_LEVEL_DEBUG, args) 37 | 38 | enum 39 | { 40 | STATE_SUBREDDIT = 0x0, 41 | STATE_THREAD = 0x0 42 | }; 43 | 44 | enum 45 | { 46 | VIEW_SUBREDDIT = 0x0, 47 | VIEW_THREAD = 0x1, 48 | 49 | VIEW_SUBREDDIT_NEXT = 0x2, 50 | 51 | THREAD_ID = 0x3, 52 | THREAD_TITLE = 0x4, 53 | THREAD_SCORE = 0x5, 54 | THREAD_TYPE = 0x6, 55 | 56 | THREAD_BODY = 0x7, 57 | 58 | THREAD_UPVOTE = 0x8, 59 | THREAD_DOWNVOTE = 0x9, 60 | THREAD_SAVE = 0xA, 61 | 62 | USER_SUBREDDIT = 0xB, 63 | 64 | LOAD_SUBREDDITLIST = 0xD, 65 | 66 | CHUNK_SIZE = 0xE, 67 | 68 | READY = 0xF, 69 | 70 | THREAD_SUBREDDIT = 0x10, 71 | 72 | LOAD_COMMENTS = 0x11, 73 | THREAD_COMMENT = 0x12, 74 | 75 | LAST_DICT_ITEM = 0x13 76 | }; 77 | 78 | struct ThreadData 79 | { 80 | #ifndef USE_PERSIST_STRINGS 81 | char *title; 82 | char *score; 83 | char *subreddit; 84 | #endif 85 | unsigned char type; 86 | Layer *layer; 87 | }; 88 | 89 | struct ViewThreadData 90 | { 91 | // only body & thread_author is actual thread data 92 | // the other stuff is for comments 93 | char *thread_author; 94 | char *score; 95 | char *body; 96 | char *comment; 97 | char *author; 98 | GBitmap *image; 99 | unsigned char depth; 100 | unsigned char index; 101 | unsigned char max; 102 | bool nextDepthPossible; 103 | }; 104 | 105 | extern Window *window_subreddit; 106 | extern Window *window_subredditlist; 107 | extern Window *window_thread; 108 | extern Window *window_threadmenu; 109 | extern Window *window_loading; 110 | extern Window *window_comment; 111 | 112 | extern int thread_offset; 113 | 114 | extern struct ViewThreadData current_thread; 115 | 116 | GFont* GetFont(); 117 | GFont* GetBiggerFont(); 118 | 119 | bool IsLoggedIn(); 120 | void SetLoggedIn(bool lin); 121 | 122 | void LoadSubreddit(char *name); 123 | void LoadThread(int index); 124 | void LoadThreadNext(); 125 | void UpvoteThread(int index); 126 | void DownvoteThread(int index); 127 | void SaveThread(int index); 128 | 129 | #ifdef DEBUG_MODE 130 | void* nt_Malloc_Raw(size_t size, const char* function, int line); 131 | void nt_Free_Raw(void* pointer); 132 | void nt_Stats(); 133 | #endif 134 | 135 | struct ThreadData* GetThread(int index); 136 | struct ThreadData* GetSelectedThread(); 137 | void SetSelectedThreadID(int index); 138 | int GetSelectedThreadID(); 139 | 140 | char* GetThreadTitle(int index); 141 | char* GetThreadScore(int index); 142 | char* GetThreadSubreddit(int index); 143 | 144 | void SetThreadTitle(struct ThreadData* thread, int index, char* str); 145 | void SetThreadScore(struct ThreadData* thread, int index, char* str); 146 | void SetThreadSubreddit(struct ThreadData* thread, int index, char* str); 147 | 148 | void init_netimage(int index); 149 | void callback_netimage(GBitmap *image); 150 | NetImageContext* get_netimage_context(); 151 | void free_netimage(); 152 | 153 | void init_timer(AppTimer *handle); 154 | void cancel_timer(); 155 | 156 | bool IsBluetoothConnected(); 157 | void OnBluetoothConnection(bool connected); 158 | 159 | #ifdef DEBUG_MODE 160 | #define nt_Malloc(size) nt_Malloc_Raw((size), __FUNCTION__, __LINE__) 161 | #define nt_Free(pointer) nt_Free_Raw((pointer)) 162 | #else 163 | #define nt_Malloc(size) malloc((size)) 164 | #define nt_Free(pointer) free((pointer)) 165 | #endif 166 | 167 | #endif 168 | -------------------------------------------------------------------------------- /src/SubredditListWindow.c: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Subreddit List Window 3 | ********************************/ 4 | 5 | #include "Rebble.h" 6 | #include "SubredditListWindow.h" 7 | #include "SubredditWindow.h" 8 | #include "ThreadWindow.h" 9 | #include "LoadingWindow.h" 10 | 11 | Window *window_subredditlist; 12 | 13 | MenuLayer *subredditlist_menu_layer; 14 | 15 | char* *user_subreddits = NULL; 16 | int subredditlist_num = -1; 17 | 18 | static uint16_t subredditlist_menu_get_num_sections_callback(MenuLayer *menu_layer, void *data); 19 | static uint16_t subredditlist_menu_get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *data); 20 | static void subredditlist_menu_draw_header_callback(GContext* ctx, const Layer *cell_layer, uint16_t section_index, void *data); 21 | static int16_t subredditlist_menu_get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, void *data); 22 | static void subredditlist_menu_draw_row_callback(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data); 23 | static void subredditlist_menu_select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *data); 24 | 25 | void subredditlist_load() 26 | { 27 | Tuplet tuple = TupletCString(LOAD_SUBREDDITLIST, ""); 28 | 29 | DictionaryIterator *iter; 30 | app_message_outbox_begin(&iter); 31 | 32 | if (iter == NULL) 33 | { 34 | return; 35 | } 36 | 37 | dict_write_tuplet(iter, &tuple); 38 | 39 | app_message_outbox_send(); 40 | 41 | subredditlist_num = 0; 42 | 43 | loading_init(); 44 | 45 | loading_set_text("Loading Subreddits"); 46 | } 47 | 48 | void subredditlist_init() 49 | { 50 | window_stack_push(window_subredditlist, true); 51 | } 52 | 53 | void subredditlist_window_load(Window *window) 54 | { 55 | Layer *window_layer = window_get_root_layer(window); 56 | 57 | subredditlist_menu_layer = menu_layer_create(window_frame); 58 | 59 | menu_layer_set_callbacks(subredditlist_menu_layer, NULL, (MenuLayerCallbacks){ 60 | .get_num_sections = subredditlist_menu_get_num_sections_callback, 61 | .get_num_rows = subredditlist_menu_get_num_rows_callback, 62 | .get_header_height = subredditlist_menu_get_header_height_callback, 63 | .draw_header = subredditlist_menu_draw_header_callback, 64 | .draw_row = subredditlist_menu_draw_row_callback, 65 | .select_click = subredditlist_menu_select_callback, 66 | }); 67 | 68 | menu_layer_set_click_config_onto_window(subredditlist_menu_layer, window); 69 | 70 | layer_add_child(window_layer, menu_layer_get_layer(subredditlist_menu_layer)); 71 | } 72 | 73 | void subredditlist_window_unload(Window *window) 74 | { 75 | menu_layer_destroy(subredditlist_menu_layer); 76 | 77 | if(user_subreddits != NULL) 78 | { 79 | for(int i=0; i < subredditlist_num; i++) 80 | { 81 | nt_Free(user_subreddits[i]); 82 | } 83 | 84 | nt_Free(user_subreddits); 85 | user_subreddits = NULL; 86 | } 87 | 88 | subredditlist_num = -1; 89 | } 90 | 91 | static uint16_t subredditlist_menu_get_num_sections_callback(MenuLayer *menu_layer, void *data) 92 | { 93 | return 1; 94 | } 95 | 96 | static uint16_t subredditlist_menu_get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) 97 | { 98 | switch (section_index) 99 | { 100 | case 0: 101 | return subredditlist_num + 1; 102 | 103 | default: 104 | return 0; 105 | } 106 | } 107 | 108 | static void subredditlist_menu_draw_header_callback(GContext* ctx, const Layer *cell_layer, uint16_t section_index, void *data) 109 | { 110 | switch (section_index) 111 | { 112 | case 0: 113 | menu_cell_basic_header_draw(ctx, cell_layer, "Subreddits"); 114 | break; 115 | } 116 | } 117 | 118 | static int16_t subredditlist_menu_get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) 119 | { 120 | return 24; 121 | } 122 | 123 | static void subredditlist_menu_draw_row_callback(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data) 124 | { 125 | menu_cell_title_draw(ctx, cell_layer, cell_index->row == 0 ? "Frontpage" : user_subreddits[cell_index->row - 1]); 126 | } 127 | 128 | static void subredditlist_menu_select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *data) 129 | { 130 | LoadSubreddit(cell_index->row == 0 ? "" : user_subreddits[cell_index->row - 1]); 131 | subreddit_load_setup(); 132 | } 133 | -------------------------------------------------------------------------------- /src/SubredditListWindow.h: -------------------------------------------------------------------------------- 1 | #ifndef SUBREDDITLIST_WINDOW_H 2 | #define SUBREDDITLIST_WINDOW_H 3 | 4 | #include 5 | 6 | void subredditlist_load(); 7 | 8 | void subredditlist_init(); 9 | 10 | void subredditlist_window_load(Window *window); 11 | void subredditlist_window_unload(Window *window); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/SubredditWindow.c: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Subreddit Window 3 | ********************************/ 4 | 5 | #include "Rebble.h" 6 | #include "SubredditWindow.h" 7 | #include "SubredditListWindow.h" 8 | #include "ThreadWindow.h" 9 | #include "LoadingWindow.h" 10 | 11 | Window *window_subreddit; 12 | 13 | ScrollLayer *subreddit_scroll_layer; 14 | 15 | GBitmap *bitmap_upvote; 16 | GBitmap *bitmap_image; 17 | GBitmap *bitmap_text; 18 | 19 | GRect sub_score_rect; 20 | GRect sub_subreddit_rect; 21 | GSize text_size; 22 | 23 | Layer *thread_sub_layer; 24 | Layer *thread_load_more_layer; 25 | Layer *thread_refresh_layer; 26 | 27 | int thread_offset = 0; 28 | bool thread_offset_reset = false; 29 | bool thread_load_more_visible = false; 30 | 31 | int thread_loaded = 0; 32 | 33 | struct ViewThreadData current_thread; 34 | 35 | static void subreddit_click_config_provider(void *context); 36 | static void subreddit_layer_update_proc(Layer *layer, GContext *ctx); 37 | static void subreddit_sub_layer_update_proc(Layer *layer, GContext *ctx); 38 | static void subreddit_scroll_layer_update_proc(Layer *layer, GContext *ctx); 39 | static void subreddit_scroll_timer_callback(void *data); 40 | 41 | void subreddit_init() 42 | { 43 | loading_uninit(); 44 | window_stack_push(window_subreddit, true); 45 | } 46 | 47 | void subreddit_window_load(Window *window) 48 | { 49 | bitmap_upvote = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_UPVOTE); 50 | bitmap_text = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_TEXT); 51 | bitmap_image = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_IMAGE); 52 | 53 | sub_score_rect = GRect(22, 0, 50, THREAD_WINDOW_HEIGHT); 54 | sub_subreddit_rect = GRect(55, 0, 68, THREAD_WINDOW_HEIGHT); 55 | 56 | subreddit_scroll_layer = scroll_layer_create(window_frame); 57 | 58 | scroll_layer_set_shadow_hidden(subreddit_scroll_layer, true); 59 | 60 | window_set_click_config_provider_with_context(window, subreddit_click_config_provider, subreddit_scroll_layer); 61 | 62 | thread_sub_layer = layer_create(GRect(0, THREAD_WINDOW_HEIGHT, window_frame.size.w, THREAD_WINDOW_HEIGHT)); 63 | layer_set_update_proc(thread_sub_layer, subreddit_sub_layer_update_proc); 64 | 65 | thread_refresh_layer = layer_create_with_data(GRect(0, 0, window_frame.size.w, THREAD_WINDOW_HEIGHT_SELECTED), 4); 66 | layer_set_update_proc(thread_refresh_layer, subreddit_layer_update_proc); 67 | layer_set_hidden(thread_refresh_layer, false); 68 | scroll_layer_add_child(subreddit_scroll_layer, thread_refresh_layer); 69 | *(int*)layer_get_data(thread_refresh_layer) = -1; 70 | 71 | for(int i = 0; i < MAX_THREADS; ++i) 72 | { 73 | struct ThreadData *thread = GetThread(i); 74 | 75 | thread->layer = layer_create_with_data(GRect(0, 0, window_frame.size.w, i == 0 ? THREAD_WINDOW_HEIGHT_SELECTED : THREAD_WINDOW_HEIGHT), 4); 76 | layer_set_update_proc(thread->layer, subreddit_layer_update_proc); 77 | layer_set_hidden(thread->layer, true); 78 | scroll_layer_add_child(subreddit_scroll_layer, thread->layer); 79 | *(int*)layer_get_data(thread->layer) = i; 80 | } 81 | 82 | thread_load_more_layer = layer_create_with_data(GRect(0, 0, window_frame.size.w, THREAD_WINDOW_HEIGHT_SELECTED), 4); 83 | layer_set_update_proc(thread_load_more_layer, subreddit_layer_update_proc); 84 | layer_set_hidden(thread_load_more_layer, true); 85 | scroll_layer_add_child(subreddit_scroll_layer, thread_load_more_layer); 86 | *(int*)layer_get_data(thread_load_more_layer) = MAX_THREADS; 87 | 88 | scroll_layer_set_content_size(subreddit_scroll_layer, GSize(window_frame.size.w, 0)); 89 | scroll_layer_set_content_offset(subreddit_scroll_layer, GPoint(0, 0), false); 90 | 91 | layer_set_update_proc(scroll_layer_get_layer(subreddit_scroll_layer), subreddit_scroll_layer_update_proc); 92 | 93 | layer_add_child(window_get_root_layer(window), scroll_layer_get_layer(subreddit_scroll_layer)); 94 | } 95 | 96 | void subreddit_window_appear(Window *window) 97 | { 98 | subreddit_selection_changed(false); 99 | } 100 | 101 | void subreddit_window_disappear(Window *window) 102 | { 103 | cancel_timer(); 104 | } 105 | 106 | void subreddit_window_unload(Window *window) 107 | { 108 | for(int i = 0; i < MAX_THREADS; ++i) 109 | { 110 | layer_destroy(GetThread(i)->layer); 111 | } 112 | 113 | gbitmap_destroy(bitmap_upvote); 114 | gbitmap_destroy(bitmap_text); 115 | gbitmap_destroy(bitmap_image); 116 | 117 | layer_destroy(thread_refresh_layer); 118 | layer_destroy(thread_load_more_layer); 119 | layer_destroy(thread_sub_layer); 120 | 121 | scroll_layer_destroy(subreddit_scroll_layer); 122 | } 123 | 124 | static void subreddit_click_config_provider(void *context) 125 | { 126 | window_single_click_subscribe(BUTTON_ID_UP, (ClickHandler) subreddit_button_up); 127 | window_single_click_subscribe(BUTTON_ID_DOWN, (ClickHandler) subreddit_button_down); 128 | 129 | window_single_repeating_click_subscribe(BUTTON_ID_UP, 100, (ClickHandler) subreddit_button_up); 130 | window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100, (ClickHandler) subreddit_button_down); 131 | 132 | window_single_click_subscribe(BUTTON_ID_SELECT, (ClickHandler) subreddit_button_select); 133 | window_long_click_subscribe(BUTTON_ID_SELECT, 750, (ClickHandler) subreddit_button_select_long, NULL); 134 | } 135 | 136 | static void subreddit_layer_update_proc(Layer *layer, GContext *ctx) 137 | { 138 | void *data = layer_get_data(layer); 139 | int index = *(int*)data; 140 | 141 | struct ThreadData *thread = NULL; 142 | 143 | bool isSelected = index == GetSelectedThreadID(); 144 | 145 | graphics_context_set_text_color(ctx, isSelected ? GColorWhite : GColorBlack); 146 | 147 | draw_text: 148 | if(index == -1 || index == MAX_THREADS || thread != NULL) 149 | { 150 | GRect rect = GRect(0, index == GetSelectedThreadID() ? 10 : -1, window_frame.size.w, THREAD_WINDOW_HEIGHT_SELECTED); 151 | graphics_draw_text(ctx, thread != NULL ? GetThreadTitle(index) : (index == -1 ? "Refresh" : "Load More"), index == GetSelectedThreadID() ? GetBiggerFont() : GetFont(), rect, GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter, NULL); 152 | return; 153 | } 154 | 155 | thread = GetThread(index); 156 | if(thread->type >= 250) 157 | { 158 | goto draw_text; 159 | } 160 | 161 | GRect rect = GRect(THREAD_LAYER_PADDING_TEXT_LEFT, 0, window_frame.size.w, THREAD_WINDOW_HEIGHT); 162 | 163 | if(isSelected) 164 | { 165 | rect.origin.x -= thread_offset; 166 | rect.size.w += thread_offset; 167 | } 168 | 169 | graphics_draw_text(ctx, GetThreadTitle(index), GetFont(), rect, GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); 170 | } 171 | 172 | static void subreddit_sub_layer_update_proc(Layer *layer, GContext *ctx) 173 | { 174 | struct ThreadData *thread = GetSelectedThread(); 175 | if(thread->type >= 250) 176 | { 177 | return; 178 | } 179 | 180 | graphics_context_set_fill_color(ctx, GColorWhite); 181 | graphics_context_set_text_color(ctx, GColorWhite); 182 | 183 | graphics_draw_bitmap_in_rect(ctx, bitmap_upvote, GRect(5, 5, 12, 15)); 184 | 185 | graphics_draw_text(ctx, GetThreadScore(GetSelectedThreadID()), GetFont(), sub_score_rect, GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); 186 | 187 | if(GetThreadSubreddit(GetSelectedThreadID()) != NULL) 188 | { 189 | graphics_draw_text(ctx, GetThreadSubreddit(GetSelectedThreadID()), GetFont(), sub_subreddit_rect, GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); 190 | } 191 | 192 | graphics_draw_bitmap_in_rect(ctx, thread->type == 1 ? bitmap_image : bitmap_text, GRect(window_frame.size.w - (thread->type == 1 ? 20 : 21), 8, thread->type == 1 ? 16 : 15, thread->type == 1 ? 12 : 9)); 193 | } 194 | 195 | static void subreddit_scroll_layer_update_proc(Layer *layer, GContext *ctx) 196 | { 197 | GPoint offset = scroll_layer_get_content_offset(subreddit_scroll_layer); 198 | 199 | int y = offset.y + THREAD_LAYER_PADDING_HALF; 200 | 201 | int max = thread_loaded; 202 | 203 | if(thread_load_more_visible) 204 | { 205 | max++; 206 | } 207 | 208 | graphics_context_set_fill_color(ctx, GColorBlack); 209 | 210 | for(int i=-1; i < max; i++) 211 | { 212 | bool isSelected = GetSelectedThreadID() == i; 213 | if(isSelected) 214 | { 215 | graphics_fill_rect(ctx, GRect(0, y - THREAD_LAYER_PADDING_HALF, window_frame.size.w, THREAD_LAYER_PADDING + THREAD_WINDOW_HEIGHT_SELECTED), 0, GCornerNone); 216 | } 217 | 218 | y += (isSelected ? THREAD_WINDOW_HEIGHT_SELECTED : THREAD_WINDOW_HEIGHT); 219 | y += THREAD_LAYER_PADDING_HALF; 220 | graphics_draw_line(ctx, GPoint(0, y), GPoint(window_frame.size.w, y)); 221 | y += THREAD_LAYER_PADDING_HALF; 222 | } 223 | } 224 | 225 | void subreddit_show_load_more() 226 | { 227 | thread_load_more_visible = true; 228 | layer_set_hidden(thread_load_more_layer, false); 229 | } 230 | 231 | void subreddit_hide_load_more() 232 | { 233 | thread_load_more_visible = false; 234 | layer_set_hidden(thread_load_more_layer, true); 235 | } 236 | 237 | #define UPDATE_LAYER_PROPS(layer, index) \ 238 | height = THREAD_WINDOW_HEIGHT;\ 239 | if(GetSelectedThreadID() == index)\ 240 | {\ 241 | height = THREAD_WINDOW_HEIGHT_SELECTED;\ 242 | }\ 243 | layer_set_frame(layer, GRect(0, y, window_frame.size.w, height));\ 244 | y += height + THREAD_LAYER_PADDING; 245 | 246 | void subreddit_selection_changed(bool before) 247 | { 248 | if(thread_loaded == 0) 249 | { 250 | return; 251 | } 252 | 253 | if(before) 254 | { 255 | if(GetSelectedThreadID() == -1 || GetSelectedThreadID() == MAX_THREADS) 256 | { 257 | return; 258 | } 259 | layer_remove_from_parent(thread_sub_layer); 260 | layer_mark_dirty(GetSelectedThread()->layer); 261 | return; 262 | } 263 | 264 | cancel_timer(); 265 | 266 | int y = THREAD_LAYER_PADDING_HALF; 267 | int height; 268 | 269 | UPDATE_LAYER_PROPS(thread_refresh_layer, -1); 270 | for(int i=0; i < MAX_THREADS; i++) 271 | { 272 | UPDATE_LAYER_PROPS(GetThread(i)->layer, i); 273 | } 274 | UPDATE_LAYER_PROPS(thread_load_more_layer, MAX_THREADS); 275 | 276 | if(GetSelectedThreadID() == -1 || GetSelectedThreadID() == MAX_THREADS) 277 | { 278 | return; 279 | } 280 | 281 | thread_offset = 0; 282 | thread_offset_reset = false; 283 | 284 | layer_add_child(GetSelectedThread()->layer, thread_sub_layer); 285 | 286 | text_size = graphics_text_layout_get_content_size(GetThreadTitle(GetSelectedThreadID()), GetFont(), GRect(0, 0, 1024, THREAD_WINDOW_HEIGHT), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft); 287 | 288 | if(text_size.w > window_frame.size.w) 289 | { 290 | init_timer(app_timer_register(600, subreddit_scroll_timer_callback, NULL)); 291 | } 292 | } 293 | 294 | void subreddit_button_up(ClickRecognizerRef recognizer, void *context) 295 | { 296 | if(GetSelectedThreadID() > -1) 297 | { 298 | subreddit_selection_changed(true); 299 | SetSelectedThreadID(GetSelectedThreadID() - 1); 300 | subreddit_set_content_offset(GetSelectedThreadID()); 301 | subreddit_selection_changed(false); 302 | } 303 | } 304 | 305 | void subreddit_button_select(ClickRecognizerRef recognizer, void *context) 306 | { 307 | if(GetSelectedThreadID() == -1) 308 | { 309 | subreddit_load_setup(); 310 | LoadSubreddit("0"); 311 | } 312 | else if(GetSelectedThreadID() == MAX_THREADS) 313 | { 314 | subreddit_load_setup(); 315 | LoadThreadNext(); 316 | } 317 | else if(thread_loaded > 0 && GetSelectedThread()->type < 250) 318 | { 319 | thread_load(); 320 | } 321 | } 322 | 323 | void subreddit_button_select_long(ClickRecognizerRef recognizer, void *context) 324 | { 325 | subredditlist_load(); 326 | } 327 | 328 | void subreddit_button_down(ClickRecognizerRef recognizer, void *context) 329 | { 330 | if(GetSelectedThreadID() < thread_loaded - 1 || (thread_loaded == MAX_THREADS && GetSelectedThreadID() < MAX_THREADS && thread_load_more_visible)) 331 | { 332 | subreddit_selection_changed(true); 333 | SetSelectedThreadID(GetSelectedThreadID() + 1); 334 | subreddit_set_content_offset(GetSelectedThreadID()); 335 | subreddit_selection_changed(false); 336 | } 337 | } 338 | 339 | void subreddit_set_content_offset(int index) 340 | { 341 | int selectionY = THREAD_WINDOW_HEIGHT + THREAD_LAYER_PADDING + THREAD_LAYER_PADDING_HALF; 342 | selectionY += index * THREAD_WINDOW_HEIGHT + index * THREAD_LAYER_PADDING + THREAD_LAYER_PADDING_HALF; 343 | selectionY += (THREAD_WINDOW_HEIGHT_SELECTED / 2) - (window_frame.size.h / 2); 344 | selectionY = selectionY > 0 ? selectionY : 0; 345 | 346 | GPoint offset = scroll_layer_get_content_offset(subreddit_scroll_layer); 347 | offset.y = -selectionY; 348 | scroll_layer_set_content_offset(subreddit_scroll_layer, offset, true); 349 | } 350 | 351 | static void subreddit_scroll_timer_callback(void *data) 352 | { 353 | struct ThreadData *thread = GetSelectedThread(); 354 | 355 | if(thread_offset_reset) 356 | { 357 | thread_offset_reset = false; 358 | thread_offset = 0; 359 | } 360 | else 361 | { 362 | thread_offset += 4; 363 | } 364 | 365 | if(text_size.w - thread_offset < window_frame.size.w) 366 | { 367 | thread_offset_reset = true; 368 | init_timer(app_timer_register(1000, subreddit_scroll_timer_callback, NULL)); 369 | } 370 | else 371 | { 372 | init_timer(app_timer_register(thread_offset == 0 ? 1000 : TITLE_SCROLL_SPEED, subreddit_scroll_timer_callback, NULL)); 373 | } 374 | 375 | layer_mark_dirty(thread->layer); 376 | } 377 | 378 | void subreddit_load_setup() 379 | { 380 | thread_loaded = 0; 381 | thread_offset_reset = false; 382 | thread_offset = 0; 383 | 384 | SetSelectedThreadID(0); 385 | 386 | scroll_layer_set_content_offset(subreddit_scroll_layer, GPoint(0, 0), false); 387 | scroll_layer_set_content_size(subreddit_scroll_layer, GSize(window_frame.size.w, 0)); 388 | 389 | subreddit_hide_load_more(); 390 | 391 | layer_remove_from_parent(thread_sub_layer); 392 | 393 | for(int i = 0; i < MAX_THREADS; ++i) 394 | { 395 | struct ThreadData *thread = GetThread(i); 396 | 397 | #ifndef USE_PERSIST_STRINGS 398 | if(thread->title != NULL) 399 | { 400 | nt_Free(thread->title); 401 | thread->title = NULL; 402 | } 403 | 404 | if(thread->score != NULL) 405 | { 406 | nt_Free(thread->score); 407 | thread->score = NULL; 408 | } 409 | 410 | if(thread->subreddit != NULL) 411 | { 412 | nt_Free(thread->subreddit); 413 | thread->subreddit = NULL; 414 | } 415 | #endif 416 | 417 | layer_set_hidden(thread->layer, true); 418 | } 419 | 420 | window_stack_pop_all(true); 421 | loading_init(); 422 | } 423 | -------------------------------------------------------------------------------- /src/SubredditWindow.h: -------------------------------------------------------------------------------- 1 | #ifndef SUBREDDIT_WINDOW_H 2 | #define SUBREDDIT_WINDOW_H 3 | 4 | #include 5 | 6 | extern GRect window_frame; 7 | 8 | void subreddit_init(); 9 | 10 | void subreddit_window_load(Window *window); 11 | void subreddit_window_appear(Window *window); 12 | void subreddit_window_disappear(Window *window); 13 | void subreddit_window_unload(Window *window); 14 | 15 | void subreddit_show_load_more(); 16 | void subreddit_hide_load_more(); 17 | 18 | void subreddit_selection_changed(bool before); 19 | 20 | void subreddit_button_up(ClickRecognizerRef recognizer, void *context); 21 | void subreddit_button_select(ClickRecognizerRef recognizer, void *context); 22 | void subreddit_button_select_long(ClickRecognizerRef recognizer, void *context); 23 | void subreddit_button_down(ClickRecognizerRef recognizer, void *context); 24 | 25 | void subreddit_set_content_offset(int index); 26 | 27 | void subreddit_load_setup(); 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /src/ThreadMenuWindow.c: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Thread Menu Window 3 | ********************************/ 4 | 5 | #include "Rebble.h" 6 | #include "SubredditWindow.h" 7 | #include "ThreadMenuWindow.h" 8 | 9 | Window *window_threadmenu; 10 | 11 | MenuLayer *threadmenu_menu_layer; 12 | 13 | static uint16_t threadmenu_menu_get_num_sections_callback(MenuLayer *menu_layer, void *data); 14 | static uint16_t threadmenu_menu_get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *data); 15 | static void threadmenu_menu_draw_header_callback(GContext* ctx, const Layer *cell_layer, uint16_t section_index, void *data); 16 | static int16_t threadmenu_menu_get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, void *data); 17 | static void threadmenu_menu_draw_row_callback(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data); 18 | static void threadmenu_menu_select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *data); 19 | 20 | void threadmenu_init() 21 | { 22 | window_stack_push(window_threadmenu, true); 23 | } 24 | 25 | void threadmenu_window_load(Window *window) 26 | { 27 | Layer *window_layer = window_get_root_layer(window); 28 | 29 | threadmenu_menu_layer = menu_layer_create(window_frame); 30 | 31 | menu_layer_set_callbacks(threadmenu_menu_layer, NULL, (MenuLayerCallbacks){ 32 | .get_num_sections = threadmenu_menu_get_num_sections_callback, 33 | .get_num_rows = threadmenu_menu_get_num_rows_callback, 34 | .get_header_height = threadmenu_menu_get_header_height_callback, 35 | .draw_header = threadmenu_menu_draw_header_callback, 36 | .draw_row = threadmenu_menu_draw_row_callback, 37 | .select_click = threadmenu_menu_select_callback, 38 | }); 39 | 40 | menu_layer_set_click_config_onto_window(threadmenu_menu_layer, window); 41 | 42 | layer_add_child(window_layer, menu_layer_get_layer(threadmenu_menu_layer)); 43 | } 44 | 45 | void threadmenu_window_unload(Window *window) 46 | { 47 | menu_layer_destroy(threadmenu_menu_layer); 48 | } 49 | 50 | static uint16_t threadmenu_menu_get_num_sections_callback(MenuLayer *menu_layer, void *data) 51 | { 52 | return 1; 53 | } 54 | 55 | static uint16_t threadmenu_menu_get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) 56 | { 57 | switch (section_index) 58 | { 59 | case 0: 60 | return 3; 61 | 62 | default: 63 | return 0; 64 | } 65 | } 66 | 67 | static void threadmenu_menu_draw_header_callback(GContext* ctx, const Layer *cell_layer, uint16_t section_index, void *data) 68 | { 69 | switch (section_index) 70 | { 71 | case 0: 72 | menu_cell_basic_header_draw(ctx, cell_layer, "Thread Menu"); 73 | break; 74 | } 75 | } 76 | 77 | static int16_t threadmenu_menu_get_header_height_callback(MenuLayer *menu_layer, uint16_t section_index, void *data) 78 | { 79 | return 24; 80 | } 81 | 82 | static void threadmenu_menu_draw_row_callback(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data) 83 | { 84 | menu_cell_title_draw(ctx, cell_layer, cell_index->row == 0 ? "Upvote" : (cell_index->row == 1 ? "Downvote" : "Save")); 85 | } 86 | 87 | static void threadmenu_menu_select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *data) 88 | { 89 | if(!IsLoggedIn()) 90 | { 91 | vibes_double_pulse(); 92 | return; 93 | } 94 | 95 | int selected = GetSelectedThreadID(); 96 | 97 | switch (cell_index->row) 98 | { 99 | case 0: 100 | UpvoteThread(selected); 101 | break; 102 | 103 | case 1: 104 | DownvoteThread(selected); 105 | break; 106 | 107 | case 2: 108 | SaveThread(selected); 109 | break; 110 | } 111 | 112 | vibes_short_pulse(); 113 | 114 | window_stack_pop(true); 115 | } 116 | -------------------------------------------------------------------------------- /src/ThreadMenuWindow.h: -------------------------------------------------------------------------------- 1 | #ifndef THREAD_MENU_WINDOW_H 2 | #define THREAD_MENU_WINDOW_H 3 | 4 | #include 5 | 6 | extern int thread_loaded; 7 | extern ScrollLayer *subreddit_scroll_layer; 8 | 9 | void threadmenu_load(); 10 | 11 | void threadmenu_init(); 12 | 13 | void threadmenu_window_load(Window *window); 14 | void threadmenu_window_unload(Window *window); 15 | 16 | #endif -------------------------------------------------------------------------------- /src/ThreadWindow.c: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Thread Window 3 | ********************************/ 4 | 5 | #include 6 | #include "Rebble.h" 7 | #include "ThreadWindow.h" 8 | #include "SubredditWindow.h" 9 | #include "LoadingWindow.h" 10 | #include "ThreadMenuWindow.h" 11 | #include "CommentWindow.h" 12 | 13 | Window *window_thread; 14 | 15 | GSize scroll_layer_size; 16 | 17 | ScrollLayer *thread_scroll_layer; 18 | Layer *thread_title_layer; 19 | TextLayer *thread_body_layer = NULL; 20 | TextLayer *thread_view_comments_layer; 21 | 22 | bool thread_view_comments_selected; 23 | 24 | BitmapLayer *thread_bitmap_layer; 25 | 26 | static void thread_click_config(void *context); 27 | static void thread_offset_changed_handler(ScrollLayer *scroll_layer, void *context); 28 | static void thread_button_up(ClickRecognizerRef recognizer, void *context); 29 | static void thread_button_select(ClickRecognizerRef recognizer, void *context); 30 | static void thread_button_down(ClickRecognizerRef recognizer, void *context); 31 | static void thread_title_layer_update_proc(Layer *layer, GContext *ctx); 32 | static void thread_scroll_timer_callback(void *data); 33 | 34 | void thread_load() 35 | { 36 | loading_init(); 37 | 38 | if(GetSelectedThread()->type == 1) 39 | { 40 | loading_set_text("Loading Image"); 41 | 42 | init_netimage(GetSelectedThreadID()); 43 | } 44 | else 45 | { 46 | loading_set_text("Loading Thread"); 47 | 48 | LoadThread(GetSelectedThreadID()); 49 | } 50 | } 51 | 52 | void thread_load_finished() 53 | { 54 | if(loading_visible()) 55 | { 56 | loading_uninit(); 57 | thread_init(); 58 | } 59 | } 60 | 61 | void thread_init() 62 | { 63 | window_stack_push(window_thread, true); 64 | } 65 | 66 | void thread_window_load(Window *window) 67 | { 68 | struct ThreadData *thread = GetSelectedThread(); 69 | 70 | thread_scroll_layer = scroll_layer_create(window_frame); 71 | 72 | scroll_layer_set_shadow_hidden(thread_scroll_layer, true); 73 | scroll_layer_set_click_config_onto_window(thread_scroll_layer, window); 74 | scroll_layer_set_content_size(thread_scroll_layer, GSize(window_frame.size.w, 0)); 75 | scroll_layer_set_content_offset(thread_scroll_layer, GPoint(0, 0), false); 76 | 77 | ScrollLayerCallbacks scrollOverride = 78 | { 79 | .click_config_provider = &thread_click_config, 80 | .content_offset_changed_handler = &thread_offset_changed_handler 81 | }; 82 | scroll_layer_set_callbacks(thread_scroll_layer, scrollOverride); 83 | 84 | thread_title_layer = layer_create(GRect(0, 0, window_frame.size.w, 22)); 85 | layer_set_update_proc(thread_title_layer, thread_title_layer_update_proc); 86 | scroll_layer_add_child(thread_scroll_layer, thread_title_layer); 87 | 88 | layer_add_child(window_get_root_layer(window), scroll_layer_get_layer(thread_scroll_layer)); 89 | 90 | thread_view_comments_layer = text_layer_create(GRect(0, 0, window_frame.size.w, LOAD_COMMENTS_HEIGHT)); 91 | text_layer_set_text(thread_view_comments_layer, "View Comments"); 92 | text_layer_set_font(thread_view_comments_layer, GetBiggerFont()); 93 | text_layer_set_text_alignment(thread_view_comments_layer, GTextAlignmentCenter); 94 | scroll_layer_add_child(thread_scroll_layer, text_layer_get_layer(thread_view_comments_layer)); 95 | 96 | thread_view_comments_selected = false; 97 | 98 | if(thread->type == 1) 99 | { 100 | // we are an image 101 | thread_body_layer = NULL; 102 | 103 | thread_bitmap_layer = bitmap_layer_create(GRect(0, 22, window_frame.size.w, window_frame.size.h)); 104 | scroll_layer_add_child(thread_scroll_layer, bitmap_layer_get_layer(thread_bitmap_layer)); 105 | 106 | scroll_layer_set_content_size(thread_scroll_layer, GSize(window_frame.size.w, 22 + window_frame.size.h + 10)); 107 | 108 | thread_update_comments_position(); 109 | } 110 | else 111 | { 112 | //current_thread.image = NULL; 113 | thread_bitmap_layer = NULL; 114 | 115 | thread_body_layer = text_layer_create(GRect(0, 22, window_frame.size.w, 10000)); 116 | text_layer_set_font(thread_body_layer, GetFont()); 117 | scroll_layer_add_child(thread_scroll_layer, text_layer_get_layer(thread_body_layer)); 118 | } 119 | } 120 | 121 | void thread_window_appear(Window *window) 122 | { 123 | thread_offset = 0; 124 | thread_offset_reset = false; 125 | 126 | text_size = graphics_text_layout_get_content_size(GetThreadTitle(GetSelectedThreadID()), GetFont(), GRect(0, 0, 1024, THREAD_WINDOW_HEIGHT), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft); 127 | 128 | if(text_size.w > window_frame.size.w) 129 | { 130 | init_timer(app_timer_register(600, thread_scroll_timer_callback, NULL)); 131 | } 132 | } 133 | 134 | void thread_window_disappear(Window *window) 135 | { 136 | cancel_timer(); 137 | } 138 | 139 | void thread_window_unload(Window *window) 140 | { 141 | DEBUG_MSG("thread_window_unload"); 142 | 143 | free_netimage(); 144 | 145 | if(current_thread.body != NULL) 146 | { 147 | nt_Free(current_thread.body); 148 | current_thread.body = NULL; 149 | } 150 | 151 | if (current_thread.image != NULL) 152 | { 153 | gbitmap_destroy(current_thread.image); 154 | current_thread.image = NULL; 155 | } 156 | 157 | layer_destroy(thread_title_layer); 158 | 159 | if(thread_body_layer != NULL) 160 | { 161 | text_layer_destroy(thread_body_layer); 162 | thread_body_layer = NULL; 163 | } 164 | 165 | if(thread_bitmap_layer != NULL) 166 | { 167 | bitmap_layer_destroy(thread_bitmap_layer); 168 | thread_bitmap_layer = NULL; 169 | } 170 | 171 | text_layer_destroy(thread_view_comments_layer); 172 | scroll_layer_destroy(thread_scroll_layer); 173 | } 174 | 175 | void thread_display_image(GBitmap *image) 176 | { 177 | if(image == NULL) 178 | { 179 | loading_disable_dots(); 180 | loading_set_text("Unable to load image"); 181 | return; 182 | } 183 | 184 | thread_load_finished(); 185 | 186 | if (current_thread.image) 187 | { 188 | gbitmap_destroy(current_thread.image); 189 | DEBUG_MSG("gbitmap_destroy 1"); 190 | } 191 | 192 | current_thread.image = image; 193 | 194 | if(thread_bitmap_layer == NULL) 195 | { 196 | return; 197 | } 198 | 199 | DEBUG_MSG("thread_display_image!"); 200 | 201 | bitmap_layer_set_bitmap(thread_bitmap_layer, image); 202 | } 203 | 204 | static void thread_offset_changed_handler(ScrollLayer *scroll_layer, void *context) 205 | { 206 | GPoint offset = scroll_layer_get_content_offset(scroll_layer); 207 | 208 | bool selected = (scroll_layer_size.h + offset.y - window_frame.size.h) <= THREAD_WINDOW_HEIGHT; 209 | if(thread_view_comments_selected != selected) 210 | { 211 | thread_view_comments_selected = selected; 212 | text_layer_set_text_color(thread_view_comments_layer, thread_view_comments_selected ? GColorWhite : GColorBlack); 213 | text_layer_set_background_color(thread_view_comments_layer, thread_view_comments_selected ? GColorBlack : GColorWhite); 214 | } 215 | } 216 | 217 | static void thread_click_config(void *context) 218 | { 219 | window_long_click_subscribe(BUTTON_ID_UP, 0, thread_button_up, NULL); 220 | window_single_click_subscribe(BUTTON_ID_SELECT, (ClickHandler) thread_button_select); 221 | window_long_click_subscribe(BUTTON_ID_DOWN, 0, thread_button_down, NULL); 222 | } 223 | 224 | static void thread_button_up(ClickRecognizerRef recognizer, void *context) 225 | { 226 | int current = GetSelectedThreadID(); 227 | subreddit_button_up(recognizer, context); 228 | if(current != GetSelectedThreadID()) 229 | { 230 | window_stack_pop(false); 231 | thread_load(); 232 | } 233 | } 234 | 235 | static void thread_button_select(ClickRecognizerRef recognizer, void *context) 236 | { 237 | if(thread_view_comments_selected) 238 | { 239 | comment_load(-1); 240 | } 241 | else 242 | { 243 | threadmenu_init(); 244 | } 245 | } 246 | 247 | static void thread_button_down(ClickRecognizerRef recognizer, void *context) 248 | { 249 | int current = GetSelectedThreadID(); 250 | subreddit_button_down(recognizer, context); 251 | if(current != GetSelectedThreadID()) 252 | { 253 | window_stack_pop(false); 254 | thread_load(); 255 | } 256 | } 257 | 258 | static void thread_title_layer_update_proc(Layer *layer, GContext *ctx) 259 | { 260 | graphics_context_set_text_color(ctx, GColorBlack); 261 | graphics_draw_text(ctx, GetThreadTitle(GetSelectedThreadID()), GetFont(), GRect(-thread_offset, 0, window_frame.size.w + thread_offset, THREAD_WINDOW_HEIGHT), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); 262 | } 263 | 264 | static void thread_scroll_timer_callback(void *data) 265 | { 266 | if(thread_offset_reset) 267 | { 268 | thread_offset_reset = false; 269 | thread_offset = -THREAD_WINDOW_PADDING_TEXT_LEFT; 270 | } 271 | else 272 | { 273 | thread_offset += 4; 274 | } 275 | 276 | if(text_size.w - thread_offset - THREAD_WINDOW_PADDING_TEXT_LEFT < window_frame.size.w) 277 | { 278 | thread_offset_reset = true; 279 | init_timer(app_timer_register(1000, thread_scroll_timer_callback, NULL)); 280 | } 281 | else 282 | { 283 | init_timer(app_timer_register(thread_offset == -THREAD_WINDOW_PADDING_TEXT_LEFT ? 1000 : TITLE_SCROLL_SPEED, thread_scroll_timer_callback, NULL)); 284 | } 285 | 286 | layer_mark_dirty(thread_title_layer); 287 | } 288 | 289 | void thread_update_comments_position() 290 | { 291 | scroll_layer_size = scroll_layer_get_content_size(thread_scroll_layer); 292 | 293 | Layer *layer = text_layer_get_layer(thread_view_comments_layer); 294 | 295 | GRect rect = layer_get_frame(layer); 296 | rect.origin.y = scroll_layer_size.h; 297 | layer_set_frame(layer, rect); 298 | 299 | scroll_layer_size.h += LOAD_COMMENTS_HEIGHT; 300 | 301 | scroll_layer_set_content_size(thread_scroll_layer, scroll_layer_size); 302 | } 303 | -------------------------------------------------------------------------------- /src/ThreadWindow.h: -------------------------------------------------------------------------------- 1 | #ifndef THREAD_WINDOW_H 2 | #define THREAD_WINDOW_H 3 | 4 | #include 5 | 6 | extern GRect window_frame; 7 | extern bool thread_offset_reset; 8 | extern GSize text_size; 9 | 10 | void thread_load(); 11 | void thread_load_finished(); 12 | 13 | void thread_init(); 14 | 15 | void thread_window_load(Window *window); 16 | void thread_window_appear(Window *window); 17 | void thread_window_disappear(Window *window); 18 | void thread_window_unload(Window *window); 19 | 20 | void thread_display_image(GBitmap *image); 21 | 22 | void thread_update_comments_position(); 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /src/js/.pebble-js-app.js.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spacetech/Rebble/cfb3e2597fb5a7db8d58e4469d2cec4144c90c6b/src/js/.pebble-js-app.js.swp -------------------------------------------------------------------------------- /src/js/pebble-js-app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Rebble Javascript 3 | */ 4 | 5 | var username; 6 | var password; 7 | var password; 8 | var subreddits_enabled; 9 | var subreddits; 10 | var last_subreddit; 11 | 12 | var redditUrl = "https://www.reddit.com"; 13 | var default_subreddits = "all,AskReddit,aww,bestof,books,earthporn,explainlikeimfive,funny,games,IAmA,movies,music,news,pics,science,technology,television,todayilearned,tifu"; 14 | 15 | var modhash = ""; 16 | 17 | var nt_AppMessageQueue = []; 18 | var nt_AppMessageQueueSize = []; 19 | var nt_AppMessageQueueRunning = []; 20 | 21 | var transferInProgress = false; 22 | var transferInProgressURL = ""; 23 | 24 | var threads = 0; 25 | var loadedThreads = {}; 26 | 27 | var threadCommentsDepth = null; 28 | 29 | var chunkSize = 0; 30 | 31 | var SUBREDDIT_QUEUE = "Subreddit"; 32 | var SUBREDDITLIST_QUEUE = "SubredditList"; 33 | var THREAD_QUEUE = "Thread"; 34 | var NET_IMAGE_QUEUE = "NetImage"; 35 | var COMMENT_QUEUE = "Comment"; 36 | var OTHER_QUEUE = "Other"; 37 | 38 | /************************************* 39 | App Message Queue System 40 | *************************************/ 41 | 42 | function nt_InitAppMessageQueue(name) 43 | { 44 | nt_AppMessageQueue[name] = []; 45 | nt_AppMessageQueueSize[name] = 0; 46 | nt_AppMessageQueueRunning[name] = false; 47 | } 48 | 49 | function nt_NextAppMessageQueue(name) 50 | { 51 | //console.log("nt_NextAppMessageQueue[" + name + "]: " + nt_AppMessageQueueSize[name]); 52 | 53 | if(nt_AppMessageQueueSize[name] === 0) 54 | { 55 | nt_AppMessageQueueRunning[name] = false; 56 | //console.log("nt_NextAppMessageQueue Done"); 57 | return; 58 | } 59 | 60 | nt_AppMessageQueueSize[name]--; 61 | 62 | var messageObject = nt_AppMessageQueue[name].shift(); 63 | 64 | var send = function() { 65 | Pebble.sendAppMessage(messageObject, function() { nt_NextAppMessageQueue(name) }, send); 66 | }; 67 | 68 | send(); 69 | } 70 | 71 | function nt_BeginAppMessageQueue(name) 72 | { 73 | if(nt_AppMessageQueueRunning[name]) 74 | { 75 | return; 76 | } 77 | 78 | if(nt_AppMessageQueueSize[name] === 0) 79 | { 80 | return; 81 | } 82 | 83 | nt_AppMessageQueueRunning[name] = true; 84 | 85 | nt_NextAppMessageQueue(name); 86 | } 87 | 88 | function sendAppMessageEx(name, messageObject) 89 | { 90 | //console.log("sendAppMessageEx: " + nt_AppMessageQueueSize); 91 | 92 | nt_AppMessageQueueSize[name]++; 93 | nt_AppMessageQueue[name].push(messageObject); 94 | 95 | nt_BeginAppMessageQueue(name); 96 | } 97 | 98 | nt_InitAppMessageQueue(SUBREDDIT_QUEUE); 99 | nt_InitAppMessageQueue(SUBREDDITLIST_QUEUE); 100 | nt_InitAppMessageQueue(THREAD_QUEUE); 101 | nt_InitAppMessageQueue(NET_IMAGE_QUEUE); 102 | nt_InitAppMessageQueue(OTHER_QUEUE); 103 | 104 | /********************************************************************************/ 105 | 106 | function GetThreadID(index) 107 | { 108 | return loadedThreads[index].id; 109 | } 110 | 111 | function GetThreadName(index) 112 | { 113 | return loadedThreads[index].name; 114 | } 115 | 116 | function GetThreadURL(index) 117 | { 118 | return loadedThreads[index].url; 119 | } 120 | 121 | function GetThreadSubreddit(index) 122 | { 123 | return loadedThreads[index].subreddit; 124 | } 125 | 126 | /********************************************************************************/ 127 | 128 | function RedditAPI(url, postdata, success, failure, method) 129 | { 130 | //console.log("RedditAPI: Loading... " + url); 131 | 132 | var req = new XMLHttpRequest(); 133 | 134 | var method = method || "POST"; 135 | 136 | req.open(method, redditUrl + "/" + url, true); 137 | req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 138 | req.setRequestHeader("User-Agent", "Pebble Rebble App 1.3"); 139 | req.setRequestHeader("X-Modhash", modhash) 140 | 141 | req.onload = function(e) 142 | { 143 | if (req.readyState === 4 && req.status === 200) 144 | { 145 | // console.log("RedditAPI: Loaded " + url); 146 | success(req.responseText); 147 | return; 148 | } 149 | 150 | // console.log("RedditAPI: Failed to load " + url + ", " + req.status); 151 | 152 | failure(req.responseText); 153 | }; 154 | 155 | req.send(postdata); 156 | } 157 | 158 | /********************************************************************************/ 159 | 160 | function IsLoggedIn() 161 | { 162 | return modhash.length !== 0; 163 | } 164 | 165 | function SetLoggedIn(mh, refresh) 166 | { 167 | //console.log("SetLoggedIn! " + mh + ", " + refresh); 168 | 169 | modhash = mh; 170 | 171 | // order matters 172 | if(refresh) 173 | { 174 | sendAppMessageEx(OTHER_QUEUE, {"ready": 2}); 175 | } 176 | 177 | sendAppMessageEx(OTHER_QUEUE, {"ready": 1}); 178 | } 179 | 180 | function RemoveLoggedIn(refresh) 181 | { 182 | //console.log("RemoveLoggedIn: " + refresh); 183 | 184 | modhash = ""; 185 | 186 | // order matters 187 | if(refresh) 188 | { 189 | sendAppMessageEx(OTHER_QUEUE, {"ready": 2}); 190 | } 191 | 192 | sendAppMessageEx(OTHER_QUEUE, {"ready": 0}); 193 | } 194 | 195 | function CheckLogin() 196 | { 197 | //console.log("CheckLogin"); 198 | 199 | // check if we are already logged in 200 | RedditAPI("api/me.json", "", 201 | function(responseText) 202 | { 203 | var response = JSON.parse(responseText); 204 | if("data" in response) 205 | { 206 | SetLoggedIn(response["data"]["modhash"], false); 207 | } 208 | else 209 | { 210 | // we aren't logged in 211 | Login(); 212 | } 213 | }, 214 | function(responseText) 215 | { 216 | // we aren't logged in 217 | Login(); 218 | }, "GET" 219 | ); 220 | } 221 | 222 | function Login() 223 | { 224 | //console.log("Login"); 225 | 226 | RedditAPI("api/login", "user=" + encodeURIComponent(username) + "&passwd=" + encodeURIComponent(password) + "&api_type=json", 227 | function(responseText) 228 | { 229 | var response = JSON.parse(responseText); 230 | if("data" in response["json"] && "modhash" in response["json"]["data"]) 231 | { 232 | //console.log(response["json"]["data"]); 233 | SetLoggedIn(response["json"]["data"]["modhash"], true); 234 | } 235 | else 236 | { 237 | //console.log(responseText); 238 | Pebble.showSimpleNotificationOnPebble("Rebble", "Failed to login to reddit. Please check your username and password in the settings dialog."); 239 | } 240 | }, 241 | function(responseText) 242 | { 243 | //console.log("Login failed"); 244 | //console.log(responseText); 245 | Pebble.showSimpleNotificationOnPebble("Rebble", "Failed to login to reddit. Please check your username and password in the settings dialog."); 246 | } 247 | ); 248 | } 249 | 250 | function Logout(onLogout) 251 | { 252 | //console.log("Logout"); 253 | 254 | if(!IsLoggedIn()) 255 | { 256 | if(onLogout) 257 | { 258 | onLogout(); 259 | } 260 | return; 261 | } 262 | 263 | var ret = function(responseText) { 264 | if(onLogout) 265 | { 266 | RemoveLoggedIn(false); 267 | onLogout(); 268 | } 269 | else 270 | { 271 | RemoveLoggedIn(true); 272 | } 273 | }; 274 | 275 | RedditAPI("logout", "top=off&uh=" + modhash, ret, ret); 276 | } 277 | 278 | function Thread_Vote(id, dir) 279 | { 280 | //console.log("Thread_Vote"); 281 | 282 | if(!IsLoggedIn()) 283 | { 284 | return; 285 | } 286 | 287 | RedditAPI("api/vote", "dir=" + encodeURIComponent(dir) + "&id=" + encodeURIComponent(GetThreadName(id)) + "&uh=" + modhash + "&api_type=json", 288 | function(responseText) 289 | { 290 | //console.log(responseText); 291 | }, 292 | function(responseText) 293 | { 294 | 295 | } 296 | ); 297 | } 298 | 299 | function Thread_Save(id) 300 | { 301 | //console.log("Thread_Save"); 302 | 303 | if(!IsLoggedIn()) 304 | { 305 | return; 306 | } 307 | 308 | RedditAPI("api/save", "uh=" + modhash + "&id=" + encodeURIComponent(GetThreadName(id)) + "&api_type=json", 309 | function(responseText) 310 | { 311 | //console.log(responseText); 312 | }, 313 | function(responseText) 314 | { 315 | 316 | } 317 | ); 318 | } 319 | 320 | function SubredditList_Send(res) 321 | { 322 | var sorted = res.sort(function(a, b) 323 | { 324 | return a.toLowerCase().localeCompare(b.toLowerCase()); 325 | }); 326 | 327 | var huge_message = ""; 328 | 329 | for (var i = 0; i < sorted.length; ++i) 330 | { 331 | var url = sorted[i]; 332 | 333 | if((huge_message.length + url.length + 32) > chunkSize) 334 | { 335 | sendAppMessageEx(SUBREDDITLIST_QUEUE, { "user_subreddit": huge_message }); 336 | huge_message = ""; 337 | } 338 | 339 | huge_message += url + ","; 340 | } 341 | 342 | huge_message += ";"; 343 | 344 | sendAppMessageEx(SUBREDDITLIST_QUEUE, { "user_subreddit": huge_message }); 345 | } 346 | 347 | function SubredditList_Load() 348 | { 349 | //console.log("SubredditList_Load"); 350 | 351 | nt_InitAppMessageQueue(SUBREDDITLIST_QUEUE); 352 | 353 | if(subreddits_enabled === true) 354 | { 355 | //console.log("Sending custom list"); 356 | 357 | var res = subreddits.split(","); 358 | 359 | for (var i = 0; i < res.length; ++i) 360 | { 361 | res[i] = res[i].replace(/;/g, '').trim(); 362 | } 363 | 364 | SubredditList_Send(res); 365 | 366 | return; 367 | } 368 | 369 | if(!IsLoggedIn()) 370 | { 371 | //console.log("Sending default list"); 372 | SubredditList_Send(default_subreddits.split(",")); 373 | return; 374 | } 375 | 376 | RedditAPI("subreddits/mine/subscriber.json", "", 377 | function(responseText) 378 | { 379 | var response = JSON.parse(responseText); 380 | 381 | var children = response["data"]["children"]; 382 | 383 | var res = []; 384 | 385 | for (var i = 0; i < children.length; ++i) 386 | { 387 | var child = children[i]["data"]; 388 | 389 | var url = child["url"].substr(3, child["url"].length - 4); 390 | 391 | res.push(url); 392 | } 393 | 394 | SubredditList_Send(res); 395 | }, 396 | function(responseText) 397 | { 398 | SubredditList_Load(); 399 | }, 400 | "GET" 401 | ); 402 | } 403 | 404 | function Thread_Load(subreddit, id, index) 405 | { 406 | //console.log("Thread_Load: " + id); 407 | 408 | nt_InitAppMessageQueue(THREAD_QUEUE); 409 | 410 | var url; 411 | if(subreddit === "") 412 | { 413 | url = "comments/" + id + ".json"; 414 | } 415 | else 416 | { 417 | url = "r/" + subreddit + "/comments/" + id + ".json"; 418 | } 419 | 420 | threadCommentsDepth = null; 421 | 422 | RedditAPI(url, null, 423 | function(responseText) 424 | { 425 | var response = JSON.parse(responseText); 426 | 427 | var original_post = response[0].data 428 | if (original_post) 429 | { 430 | var children = original_post.children; 431 | 432 | var child = children[0].data; 433 | var is_self = child["is_self"]; 434 | 435 | if(is_self) 436 | { 437 | var selftext = child["selftext"]; 438 | 439 | //console.log("Text Length: " + selftext.length); 440 | 441 | var trimmed_body = selftext.replace(/[^A-Za-z 0-9 \.,\?""!@#\$%\^&\*\(\)-_=\+;:<>\/\\\|\}\{\[\]`~]*/g, '').substr(0, chunkSize - 128).trim(); 442 | 443 | if(trimmed_body.length === 0) 444 | { 445 | trimmed_body = "Empty self post"; 446 | } 447 | 448 | sendAppMessageEx(THREAD_QUEUE, { 449 | "id": index, 450 | "title": child["author"], 451 | "thread_body" : trimmed_body 452 | }); 453 | } 454 | 455 | //console.log("setting comments"); 456 | 457 | threadCommentsDepth = [{ 458 | "index": 0, 459 | "data": response[1].data 460 | }]; 461 | } 462 | else 463 | { 464 | //console.log("Thread_Load no data body"); 465 | } 466 | }, 467 | function(responseText) 468 | { 469 | //console.log("Thread_Load Failed!"); 470 | //console.log(responseText); 471 | }, 472 | "GET" 473 | ); 474 | } 475 | 476 | function Comments_Load(dir) 477 | { 478 | //console.log("Comments_Load: " + dir); 479 | 480 | nt_InitAppMessageQueue(COMMENT_QUEUE); 481 | 482 | if(threadCommentsDepth === null) 483 | { 484 | // we are probably viewing an image. additional loading must be done. 485 | LoadImageComments(dir); 486 | return; 487 | } 488 | 489 | var depth = threadCommentsDepth.length - 1; 490 | 491 | comments = threadCommentsDepth[depth]; 492 | 493 | //console.log("Current Depth: " + depth + ", # of comments: " + comments.data.children.length); 494 | 495 | if(dir == 0) 496 | { 497 | // next comment for the current depth 498 | comments.index++; 499 | } 500 | else if(dir == 1) 501 | { 502 | // previous comment for the current depth 503 | comments.index--; 504 | } 505 | else if(dir == 2) 506 | { 507 | // go deeper 508 | 509 | if(comments.data.children[comments.index].data.replies === "") 510 | { 511 | return; 512 | } 513 | 514 | threadCommentsDepth.push({ 515 | "index": 0, 516 | "data": comments.data.children[comments.index].data.replies.data 517 | }); 518 | 519 | Comments_Load(-1); 520 | 521 | return; 522 | } 523 | else if(dir == 3) 524 | { 525 | // go towards the surface! 526 | 527 | threadCommentsDepth.pop(); 528 | 529 | Comments_Load(-1); 530 | 531 | return; 532 | } 533 | 534 | var max_comments = 0; 535 | for(var i=0; i < comments.data.children.length; i++) 536 | { 537 | if(comments.data.children[i].kind === "t1") 538 | { 539 | max_comments++; 540 | } 541 | } 542 | 543 | if(comments.index < 0 || comments.index >= max_comments) 544 | { 545 | //console.log("No comments"); 546 | sendAppMessageEx(COMMENT_QUEUE, {"comment": "No more comments here"}); 547 | } 548 | else 549 | { 550 | var commentData = comments.data.children[comments.index].data; 551 | 552 | var replies = commentData.replies; 553 | var author = commentData.author; 554 | var body = commentData.body; 555 | 556 | var score = "Hidden"; 557 | if(!commentData.score_hidden) 558 | { 559 | score = commentData.ups - commentData.downs; 560 | score = score.toString(); 561 | } 562 | 563 | // count next depths comments 564 | var count = 0; 565 | if(replies !== "") 566 | { 567 | for(var i=0; i < replies.data.children.length; i++) 568 | { 569 | if(replies.data.children[i].kind === "t1") 570 | { 571 | count++; 572 | } 573 | } 574 | } 575 | 576 | // it looks weird because I'm reusing keys. 577 | sendAppMessageEx(COMMENT_QUEUE, { 578 | "title": author, 579 | "score": score, 580 | "comment": body.substr(0, chunkSize - 128), 581 | 582 | // current depth 583 | "type": depth, 584 | 585 | // current index 586 | "id": comments.index, 587 | 588 | // max comments for current depth 589 | "thread_body": max_comments, 590 | 591 | // can we go deeper 592 | "user_subreddit": count > 0 ? 1 : 0 593 | }); 594 | 595 | //console.log("SENT " + score + " " + body.length); 596 | //console.log("blah " + (count > 0 ? "next depth possible" : "end of the line")); 597 | //console.log(body); 598 | } 599 | } 600 | 601 | function Subreddit_Load(subreddit, after) 602 | { 603 | //console.log("Subreddit_Load: " + subreddit); 604 | //console.log("Heyyy"); 605 | 606 | nt_InitAppMessageQueue(SUBREDDIT_QUEUE); 607 | 608 | var url; 609 | var frontpage = false; 610 | 611 | if(subreddit === "") 612 | { 613 | frontpage = true; 614 | url = "hot.json?limit=100"; 615 | } 616 | else 617 | { 618 | url = "r/" + subreddit + "/hot.json?limit=100"; 619 | } 620 | 621 | if(after !== undefined) 622 | { 623 | url += "&after=" + encodeURIComponent(after); 624 | } 625 | 626 | RedditAPI(url, null, 627 | function(responseText) 628 | { 629 | threads = 0; 630 | loadedThreads = {}; 631 | 632 | var response = false; 633 | try 634 | { 635 | response = JSON.parse(responseText); 636 | } 637 | catch(e) 638 | { 639 | 640 | } 641 | 642 | if (response !== false && response.data) 643 | { 644 | var children = response.data.children; 645 | 646 | for (var i = 0; i < children.length; ++i) 647 | { 648 | var thread = children[i].data; 649 | 650 | var image = ""; 651 | var url = thread.url; 652 | if(url.slice(-4) === ".jpg" || url.slice(-5) === ".jpeg" || url.slice(-4) === ".gif" || url.slice(-4) === ".png" || url.slice(-4) === ".bmp") 653 | { 654 | //console.log("found image: " + url); 655 | image = url; 656 | } 657 | 658 | if(thread.is_self === true || image !== "") 659 | { 660 | success = true; 661 | loadedThreads[threads] = { 662 | "id": thread.id, 663 | "url": thread.url, 664 | "name": thread.name 665 | }; 666 | 667 | var trimmed_title = thread.title.replace(/[^A-Za-z 0-9 \.,\?""!@#\$%\^&\*\(\)-_=\+;:<>\/\\\|\}\{\[\]`~]*/g, ''); 668 | 669 | var messageObject = { 670 | "title": trimmed_title, 671 | "score": thread.score.toString(), 672 | "type": thread.is_self ? 0 : 1 673 | } 674 | 675 | if(frontpage) 676 | { 677 | messageObject["thread_subreddit"] = thread.subreddit; 678 | loadedThreads[threads].subreddit = thread.subreddit; 679 | } 680 | else 681 | { 682 | loadedThreads[threads].subreddit = ""; 683 | } 684 | 685 | //console.log("loadedThreads " + threads + ", " + thread.title); 686 | 687 | sendAppMessageEx(SUBREDDIT_QUEUE, messageObject); 688 | 689 | threads++; 690 | 691 | if(threads >= 20) 692 | { 693 | //console.log("Stopped at 20"); 694 | break; 695 | } 696 | } 697 | } 698 | 699 | if(threads === 0) 700 | { 701 | sendAppMessageEx(SUBREDDIT_QUEUE, { 702 | "title": "No threads found", 703 | "score": "", 704 | "type": 254 705 | }); 706 | } 707 | else 708 | { 709 | // tells the app it's done loading the current thread list 710 | sendAppMessageEx(SUBREDDIT_QUEUE, { 711 | "type": 255 712 | }); 713 | } 714 | } 715 | else 716 | { 717 | sendAppMessageEx(SUBREDDIT_QUEUE, { 718 | "title": "Invalid Subreddit", 719 | "score": "", 720 | "type": 254 721 | }); 722 | } 723 | }, 724 | function(responseText) 725 | { 726 | //Subreddit_Load(subreddit); 727 | sendAppMessageEx(SUBREDDIT_QUEUE, { 728 | "title": "Error Loading Subreddit", 729 | "score": "", 730 | "type": 254 731 | }); 732 | }, 733 | "GET" 734 | ); 735 | } 736 | 737 | /********************************************************************************/ 738 | 739 | Pebble.addEventListener("ready", function(e) 740 | { 741 | console.log("ready " + e.ready); 742 | 743 | username = localStorage.getItem("username"); 744 | password = localStorage.getItem("password"); 745 | subreddits_enabled = localStorage.getItem("subreddits_enabled"); 746 | subreddits = localStorage.getItem("subreddits"); 747 | last_subreddit = localStorage.getItem("last_subreddit"); 748 | 749 | if(last_subreddit === undefined || last_subreddit === null || last_subreddit === false || last_subreddit === 0 || last_subreddit === "0") 750 | { 751 | last_subreddit = ""; 752 | } 753 | 754 | if(username === undefined || username === null) 755 | { 756 | username = ""; 757 | } 758 | 759 | if(password === undefined || password === null) 760 | { 761 | password = ""; 762 | } 763 | 764 | if(subreddits_enabled === undefined || subreddits_enabled === null) 765 | { 766 | subreddits_enabled = false; 767 | } 768 | 769 | if(subreddits_enabled === 'true') 770 | { 771 | subreddits_enabled = true; 772 | } 773 | 774 | if(subreddits_enabled === 'false') 775 | { 776 | subreddits_enabled = false; 777 | } 778 | 779 | if(subreddits === undefined || subreddits === null) 780 | { 781 | subreddits = ""; 782 | } 783 | 784 | sendAppMessageEx(OTHER_QUEUE, {"ready": 0}); 785 | 786 | if(username && password && username.length > 0 && password.length > 0) 787 | { 788 | CheckLogin(); 789 | } 790 | 791 | Subreddit_Load(last_subreddit); 792 | }); 793 | 794 | Pebble.addEventListener("appmessage", function(e) 795 | { 796 | //console.log(JSON.stringify(e.payload)); 797 | try 798 | { 799 | if("chunk_size" in e.payload) 800 | { 801 | chunkSize = e.payload['chunk_size']; 802 | //console.log("Got chunkSize"); 803 | } 804 | 805 | if ("NETIMAGE_URL" in e.payload) 806 | { 807 | threadCommentsIndex = e.payload['NETIMAGE_URL']; 808 | 809 | var url = encodeURIComponent(GetThreadURL(e.payload['NETIMAGE_URL'])); 810 | 811 | transferInProgress = true; 812 | transferInProgressURL = url; 813 | 814 | //SendImage("http://core.binghamton.edu:2635/?url=" + url, chunkSize); 815 | //SendImage("http://garywilber.com:2635/?url=" + url, chunkSize - 8); 816 | SendImage("https://rebble.azurewebsites.net/?url=" + url, chunkSize - 8); 817 | } 818 | else if ("subreddit" in e.payload) 819 | { 820 | var check = e.payload.subreddit.trim(); 821 | if(check === "0") 822 | { 823 | Subreddit_Load(last_subreddit); 824 | } 825 | else 826 | { 827 | last_subreddit = check; 828 | localStorage.setItem("last_subreddit", last_subreddit); 829 | Subreddit_Load(last_subreddit); 830 | } 831 | } 832 | else if ("subreddit_next" in e.payload) 833 | { 834 | Subreddit_Load(last_subreddit, GetThreadName(threads - 1)); 835 | } 836 | else if ("thread" in e.payload) 837 | { 838 | Thread_Load(last_subreddit, GetThreadID(e.payload.thread), e.payload.thread); 839 | } 840 | else if ("upvote" in e.payload) 841 | { 842 | Thread_Vote(e.payload.upvote, 1); 843 | } 844 | else if ("downvote" in e.payload) 845 | { 846 | Thread_Vote(e.payload.downvote, -1); 847 | } 848 | else if("save" in e.payload) 849 | { 850 | Thread_Save(e.payload.save); 851 | } 852 | else if("load_subredditlist" in e.payload) 853 | { 854 | SubredditList_Load(); 855 | } 856 | else if("load_comments" in e.payload) 857 | { 858 | Comments_Load(e.payload.load_comments); 859 | } 860 | else 861 | { 862 | //console.log("Bad Message"); 863 | //console.log(JSON.stringify(e)); 864 | } 865 | } 866 | catch(ex) 867 | { 868 | console.log("Error Detected"); 869 | console.log(ex); 870 | } 871 | 872 | }); 873 | 874 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent 875 | function fixedEncodeURIComponent(str) 876 | { 877 | return encodeURIComponent(str).replace(/[!'()]/g, escape).replace(/\*/g, "%2A"); 878 | } 879 | 880 | Pebble.addEventListener("showConfiguration", function(e) 881 | { 882 | //console.log("showConfiguration"); 883 | 884 | var url = "https://spacetech.github.io/Rebble/index.html?"; 885 | 886 | if(username) 887 | { 888 | url += fixedEncodeURIComponent("username") + "=" + fixedEncodeURIComponent(username) + "&"; 889 | } 890 | 891 | if(password) 892 | { 893 | url += fixedEncodeURIComponent("password") + "=" + fixedEncodeURIComponent(password) + "&"; 894 | } 895 | 896 | if(subreddits_enabled) 897 | { 898 | url += fixedEncodeURIComponent("subreddits_enabled") + "=" + fixedEncodeURIComponent(subreddits_enabled) + "&"; 899 | } 900 | 901 | if(subreddits) 902 | { 903 | url += fixedEncodeURIComponent("subreddits") + "=" + fixedEncodeURIComponent(subreddits) + "&"; 904 | } 905 | 906 | url += "dev"; 907 | 908 | Pebble.openURL(url); 909 | }); 910 | 911 | Pebble.addEventListener("webviewclosed", function(e) 912 | { 913 | //console.log("webviewclosed"); 914 | 915 | if(e.response === "" || e.response === "CANCELLED") 916 | { 917 | return; 918 | } 919 | 920 | //console.log(e.response); 921 | 922 | var options = JSON.parse(decodeURIComponent(e.response)); 923 | 924 | if(options["username"] === undefined) 925 | { 926 | return; 927 | } 928 | 929 | subreddits_enabled = options["subreddits_enabled"]; 930 | if(subreddits_enabled === 'true') 931 | { 932 | subreddits_enabled = true; 933 | } 934 | if(subreddits_enabled === 'false') 935 | { 936 | subreddits_enabled = false; 937 | } 938 | 939 | subreddits = options["subreddits"]; 940 | 941 | if(username !== options["username"] || password !== options["password"]) 942 | { 943 | username = options["username"]; 944 | password = options["password"]; 945 | 946 | if(username.length === 0 || password.length === 0) 947 | { 948 | Logout(null); 949 | } 950 | else 951 | { 952 | Logout(Login); 953 | } 954 | } 955 | 956 | localStorage.setItem("username", username); 957 | localStorage.setItem("password", password); 958 | localStorage.setItem("subreddits_enabled", subreddits_enabled); 959 | localStorage.setItem("subreddits", subreddits); 960 | }); 961 | 962 | /********************************************************************************/ 963 | 964 | function LoadImageComments(dir) 965 | { 966 | threadCommentsDepth = null; 967 | 968 | var url; 969 | if(GetThreadSubreddit(threadCommentsIndex) === "") 970 | { 971 | url = "comments/" + GetThreadID(threadCommentsIndex) + ".json"; 972 | } 973 | else 974 | { 975 | url = "r/" + GetThreadSubreddit(threadCommentsIndex) + "/comments/" + GetThreadID(threadCommentsIndex) + ".json"; 976 | } 977 | 978 | RedditAPI(url, null, 979 | function(responseText) 980 | { 981 | var response = JSON.parse(responseText); 982 | 983 | var original_post = response[0].data 984 | if (original_post) 985 | { 986 | threadCommentsDepth = [{ 987 | "index": 0, 988 | "data": response[1].data 989 | }]; 990 | 991 | if(dir !== null) 992 | { 993 | Comments_Load(dir); 994 | } 995 | } 996 | }, 997 | function(responseText) 998 | { 999 | 1000 | }, 1001 | "GET" 1002 | ); 1003 | } 1004 | 1005 | function SendImage(url, chunkSize) 1006 | { 1007 | //console.log("SendImage: " + chunkSize); 1008 | 1009 | if(chunkSize === 0) 1010 | { 1011 | return; 1012 | } 1013 | 1014 | LoadImageComments(null); 1015 | 1016 | nt_InitAppMessageQueue(NET_IMAGE_QUEUE); 1017 | 1018 | var req = new XMLHttpRequest(); 1019 | req.open("GET", url, true); 1020 | req.responseType = "arraybuffer"; 1021 | req.onload = function(e) 1022 | { 1023 | var buf = req.response; 1024 | if(req.status == 200 && buf) 1025 | { 1026 | var byteArray = new Uint8Array(buf); 1027 | 1028 | var bytes = []; 1029 | for(var i=0; i < byteArray.byteLength; i++) 1030 | { 1031 | bytes.push(byteArray[i]); 1032 | } 1033 | 1034 | //console.log("Queuing image with " + byteArray.length + " bytes."); 1035 | 1036 | sendAppMessageEx(NET_IMAGE_QUEUE, {"NETIMAGE_BEGIN": bytes.length}); 1037 | 1038 | for(var i=0; i < bytes.length; i += chunkSize) 1039 | { 1040 | sendAppMessageEx(NET_IMAGE_QUEUE, {"NETIMAGE_DATA": bytes.slice(i, i + chunkSize)}); 1041 | } 1042 | 1043 | sendAppMessageEx(NET_IMAGE_QUEUE, {"NETIMAGE_END": "done"}); 1044 | 1045 | //console.log("Queued image"); 1046 | } 1047 | else 1048 | { 1049 | //console.log("Request status is " + req.status); 1050 | sendAppMessageEx(NET_IMAGE_QUEUE, {"NETIMAGE_BEGIN": 0}); 1051 | sendAppMessageEx(NET_IMAGE_QUEUE, {"NETIMAGE_END": "done"}); 1052 | } 1053 | } 1054 | req.onerror = function(e) 1055 | { 1056 | //SendImage(url, chunkSize); 1057 | sendAppMessageEx(NET_IMAGE_QUEUE, {"NETIMAGE_BEGIN": 0}); 1058 | sendAppMessageEx(NET_IMAGE_QUEUE, {"NETIMAGE_END": "done"}); 1059 | } 1060 | 1061 | req.send(null); 1062 | } 1063 | -------------------------------------------------------------------------------- /src/netimage.c: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/pebble-hacks/pebble-faces 3 | */ 4 | 5 | #include "netimage.h" 6 | #include "Rebble.h" 7 | #include "AppMessages.h" 8 | 9 | NetImageContext *netimage_create_context(NetImageCallback callback) 10 | { 11 | NetImageContext *ctx = nt_Malloc(sizeof(NetImageContext)); 12 | 13 | ctx->length = 0; 14 | ctx->index = 0; 15 | ctx->data = NULL; 16 | ctx->callback = callback; 17 | 18 | return ctx; 19 | } 20 | 21 | void netimage_destroy_context(NetImageContext *ctx) 22 | { 23 | if (ctx->data) 24 | { 25 | nt_Free(ctx->data); 26 | } 27 | nt_Free(ctx); 28 | } 29 | 30 | void netimage_request(int index) 31 | { 32 | DictionaryIterator *outbox; 33 | 34 | app_message_outbox_begin(&outbox); 35 | 36 | dict_write_uint8(outbox, NETIMAGE_URL, (uint8_t)index); 37 | 38 | DEBUG_MSG("NETIMAGE_URL %d", index); 39 | 40 | app_message_outbox_send(); 41 | } 42 | 43 | void netimage_receive(DictionaryIterator *iter) 44 | { 45 | NetImageContext *ctx = get_netimage_context(); 46 | 47 | Tuple *tuple = dict_read_first(iter); 48 | if (!tuple) 49 | { 50 | DEBUG_MSG("Got a message with no first key! Size of message: %li", (uint32_t)iter->end - (uint32_t)iter->dictionary); 51 | return; 52 | } 53 | 54 | switch (tuple->key) 55 | { 56 | case NETIMAGE_DATA: 57 | if (ctx->index + tuple->length <= ctx->length) 58 | { 59 | memcpy(ctx->data + ctx->index, tuple->value->data, tuple->length); 60 | ctx->index += tuple->length; 61 | } 62 | else 63 | { 64 | DEBUG_MSG("Not overriding rx buffer. Bufsize=%li BufIndex=%li DataLen=%i", 65 | ctx->length, ctx->index, tuple->length); 66 | } 67 | break; 68 | case NETIMAGE_BEGIN: 69 | DEBUG_MSG("Start transmission. Size=%lu", tuple->value->uint32); 70 | if (ctx->data != NULL) 71 | { 72 | nt_Free(ctx->data); 73 | } 74 | if(tuple->value->uint32 == 0) 75 | { 76 | ctx->data = NULL; 77 | break; 78 | } 79 | ctx->data = nt_Malloc(tuple->value->uint32); 80 | if (ctx->data != NULL) 81 | { 82 | ctx->length = tuple->value->uint32; 83 | ctx->index = 0; 84 | } 85 | else 86 | { 87 | DEBUG_MSG("Unable to allocate memory to receive image."); 88 | ctx->length = 0; 89 | ctx->index = 0; 90 | } 91 | break; 92 | case NETIMAGE_END: 93 | if (ctx->data && ctx->length > 0 && ctx->index > 0) 94 | { 95 | GBitmap *bitmap = gbitmap_create_with_data(ctx->data); 96 | nt_Free(ctx->data); 97 | if (bitmap) 98 | { 99 | ctx->callback(bitmap); 100 | } 101 | else 102 | { 103 | ctx->callback(NULL); 104 | DEBUG_MSG("Unable to create GBitmap. Is this a valid PBI?"); 105 | } 106 | ctx->data = NULL; 107 | ctx->index = ctx->length = 0; 108 | } 109 | else 110 | { 111 | ctx->callback(NULL); 112 | DEBUG_MSG("Got End message but we have no image..."); 113 | } 114 | break; 115 | default: 116 | DEBUG_MSG("Unknown key in dict: %lu", tuple->key); 117 | break; 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /src/netimage.h: -------------------------------------------------------------------------------- 1 | #ifndef NET_IMAGE_H 2 | #define NET_IMAGE_H 3 | 4 | #include 5 | 6 | /* The key used to transmit image data. Contains byte array. */ 7 | #define NETIMAGE_DATA 0x696d6700 /* "img" */ 8 | /* The key used to start a new image transmission. Contains uint32 size */ 9 | #define NETIMAGE_BEGIN NETIMAGE_DATA + 1 10 | /* The key used to finalize an image transmission. Data not defined. */ 11 | #define NETIMAGE_END NETIMAGE_DATA + 2 12 | 13 | /* The key used to tell the JS how big chunks should be */ 14 | #define NETIMAGE_CHUNK_SIZE NETIMAGE_DATA + 3 15 | /* The key used to request a PBI */ 16 | #define NETIMAGE_URL NETIMAGE_DATA + 4 17 | 18 | typedef void (*NetImageCallback)(GBitmap *image); 19 | 20 | typedef struct 21 | { 22 | /* size of the data buffer allocated */ 23 | uint32_t length; 24 | /* buffer of data that will contain the actual image */ 25 | uint8_t *data; 26 | /* Next byte to write */ 27 | uint32_t index; 28 | /* Callback to call when we are done loading the image */ 29 | NetImageCallback callback; 30 | } NetImageContext; 31 | 32 | NetImageContext *netimage_create_context(NetImageCallback callback); 33 | 34 | void netimage_destroy_context(NetImageContext *ctx); 35 | 36 | void netimage_request(int index); 37 | 38 | void netimage_receive(DictionaryIterator *iter); 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /wscript: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # This file is the default set of rules to compile a Pebble project. 4 | # 5 | # Feel free to customize this to your needs. 6 | # 7 | 8 | import os.path 9 | 10 | top = '.' 11 | out = 'build' 12 | 13 | def options(ctx): 14 | ctx.load('pebble_sdk') 15 | 16 | def configure(ctx): 17 | ctx.load('pebble_sdk') 18 | 19 | def build(ctx): 20 | ctx.load('pebble_sdk') 21 | 22 | build_worker = os.path.exists('worker_src') 23 | binaries = [] 24 | 25 | for p in ctx.env.TARGET_PLATFORMS: 26 | ctx.set_env(ctx.all_envs[p]) 27 | ctx.set_group(ctx.env.PLATFORM_NAME) 28 | app_elf='{}/pebble-app.elf'.format(ctx.env.BUILD_DIR) 29 | ctx.pbl_program(source=ctx.path.ant_glob('src/**/*.c'), 30 | target=app_elf) 31 | 32 | if build_worker: 33 | worker_elf='{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR) 34 | binaries.append({'platform': p, 'app_elf': app_elf, 'worker_elf': worker_elf}) 35 | ctx.pbl_worker(source=ctx.path.ant_glob('worker_src/**/*.c'), 36 | target=worker_elf) 37 | else: 38 | binaries.append({'platform': p, 'app_elf': app_elf}) 39 | 40 | ctx.set_group('bundle') 41 | ctx.pbl_bundle(binaries=binaries, js=ctx.path.ant_glob('src/js/**/*.js')) 42 | -------------------------------------------------------------------------------- /wscript.backup: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # This file is the default set of rules to compile a Pebble project. 4 | # 5 | # Feel free to customize this to your needs. 6 | # 7 | 8 | top = '.' 9 | out = 'build' 10 | 11 | def options(ctx): 12 | ctx.load('pebble_sdk') 13 | 14 | def configure(ctx): 15 | ctx.load('pebble_sdk') 16 | 17 | def build(ctx): 18 | ctx.load('pebble_sdk') 19 | 20 | ctx.pbl_program(source=ctx.path.ant_glob('src/**/*.c'), target='pebble-app.elf') 21 | 22 | ctx.pbl_bundle(elf='pebble-app.elf', js=ctx.path.ant_glob('src/js/**/*.js')) 23 | --------------------------------------------------------------------------------