├── README.md ├── core ├── images │ ├── mask.png │ ├── effect_bw.png │ ├── spinner_28.gif │ ├── buttons-hover.png │ ├── dark_panel_bg.png │ ├── effect_autumn.png │ ├── effect_beauty.png │ ├── effect_lomo.png │ ├── effect_normal.png │ ├── effect_purple.png │ ├── effect_sketch.png │ ├── effect_soften.png │ ├── grid_cell@2x.png │ ├── mono_icons@2x.png │ ├── service │ │ ├── gplus.png │ │ ├── weibo.png │ │ ├── twitter.png │ │ ├── facebook.png │ │ └── statusnet.png │ ├── side_shadow.png │ ├── effect_enhance.png │ ├── effect_lighten.png │ ├── effect_vintage.png │ ├── control_icons@2x.png │ ├── guide │ │ ├── add_slot_bg.png │ │ └── add_slot_bg@2x.png │ └── default_profile_image.jpg ├── icons │ ├── 22x22 │ │ └── apps │ │ │ └── hotot.png │ ├── 24x24 │ │ └── apps │ │ │ └── hotot.png │ └── 128x128 │ │ └── apps │ │ ├── hotot.ico │ │ └── hotot.png ├── partials │ ├── new_slot_building_page.html │ ├── new_slot_auth_page.html │ ├── new_slot_oauth_page.html │ ├── settings_advanced_page.html │ ├── settings_general_page.html │ └── settings_behavior_page.html ├── scripts │ ├── abs.column.coffee │ ├── serv.notify.coffee │ ├── util.tabs_frame.coffee │ ├── dialog.alert.coffee │ ├── proto.base.coffee │ ├── serv.app.coffee │ ├── serv.log.coffee │ ├── serv.columns_state.coffee │ ├── dialog.log.coffee │ ├── sandbox.coffee │ ├── serv.conn.coffee │ ├── lib.oauth2.coffee │ ├── hotot_app.coffee │ ├── ctrl.win.coffee │ ├── background.coffee │ ├── util.hotkey.coffee │ ├── ctrl.column_area.coffee │ ├── fake_source.coffee │ ├── interface.coffee │ ├── serv.cache.coffee │ ├── com.account_selector.coffee │ ├── angular.sanitize.js │ ├── serv.daemon.coffee │ ├── dialog.settings.coffee │ ├── lib.oauth1.coffee │ ├── ctrl.nav.coffee │ ├── dialog.profile.coffee │ ├── serv.settings.coffee │ ├── serv.relation.coffee │ ├── dialog.preview.coffee │ ├── dialog.new_slot.coffee │ ├── lib.sha1.js │ ├── util.column.coffee │ ├── dialog.message.coffee │ └── lib.base64.js ├── styles │ ├── dialog.alert.less │ ├── dialog.log.less │ ├── dialog.profile.less │ ├── new_slot.less │ ├── component.less │ ├── settings.less │ ├── dialog.preview.less │ ├── dialog.message.less │ ├── dialog.columns.less │ ├── dialog.people.less │ └── main.less ├── sandbox.html ├── manifest.json ├── dialogs │ ├── log.html │ ├── alert.html │ ├── preview.html │ ├── new_slot.html │ ├── profile.html │ ├── columns.html │ ├── settings.html │ ├── message.html │ └── compose.html ├── pin.html ├── login_b.html └── login_a.html └── Cakefile /README.md: -------------------------------------------------------------------------------- 1 | hotot3 2 | ====== 3 | -------------------------------------------------------------------------------- /core/images/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/mask.png -------------------------------------------------------------------------------- /core/images/effect_bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_bw.png -------------------------------------------------------------------------------- /core/images/spinner_28.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/spinner_28.gif -------------------------------------------------------------------------------- /core/images/buttons-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/buttons-hover.png -------------------------------------------------------------------------------- /core/images/dark_panel_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/dark_panel_bg.png -------------------------------------------------------------------------------- /core/images/effect_autumn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_autumn.png -------------------------------------------------------------------------------- /core/images/effect_beauty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_beauty.png -------------------------------------------------------------------------------- /core/images/effect_lomo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_lomo.png -------------------------------------------------------------------------------- /core/images/effect_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_normal.png -------------------------------------------------------------------------------- /core/images/effect_purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_purple.png -------------------------------------------------------------------------------- /core/images/effect_sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_sketch.png -------------------------------------------------------------------------------- /core/images/effect_soften.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_soften.png -------------------------------------------------------------------------------- /core/images/grid_cell@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/grid_cell@2x.png -------------------------------------------------------------------------------- /core/images/mono_icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/mono_icons@2x.png -------------------------------------------------------------------------------- /core/images/service/gplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/service/gplus.png -------------------------------------------------------------------------------- /core/images/service/weibo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/service/weibo.png -------------------------------------------------------------------------------- /core/images/side_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/side_shadow.png -------------------------------------------------------------------------------- /core/icons/22x22/apps/hotot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/icons/22x22/apps/hotot.png -------------------------------------------------------------------------------- /core/icons/24x24/apps/hotot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/icons/24x24/apps/hotot.png -------------------------------------------------------------------------------- /core/images/effect_enhance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_enhance.png -------------------------------------------------------------------------------- /core/images/effect_lighten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_lighten.png -------------------------------------------------------------------------------- /core/images/effect_vintage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/effect_vintage.png -------------------------------------------------------------------------------- /core/images/service/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/service/twitter.png -------------------------------------------------------------------------------- /core/icons/128x128/apps/hotot.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/icons/128x128/apps/hotot.ico -------------------------------------------------------------------------------- /core/icons/128x128/apps/hotot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/icons/128x128/apps/hotot.png -------------------------------------------------------------------------------- /core/images/control_icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/control_icons@2x.png -------------------------------------------------------------------------------- /core/images/guide/add_slot_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/guide/add_slot_bg.png -------------------------------------------------------------------------------- /core/images/service/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/service/facebook.png -------------------------------------------------------------------------------- /core/images/service/statusnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/service/statusnet.png -------------------------------------------------------------------------------- /core/images/guide/add_slot_bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/guide/add_slot_bg@2x.png -------------------------------------------------------------------------------- /core/images/default_profile_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyricat/hotot3/HEAD/core/images/default_profile_image.jpg -------------------------------------------------------------------------------- /core/partials/new_slot_building_page.html: -------------------------------------------------------------------------------- 1 |
2 | Coming soon. 3 |
-------------------------------------------------------------------------------- /core/scripts/abs.column.coffee: -------------------------------------------------------------------------------- 1 | 2 | class AbsColumn 3 | 4 | @loadFeaturePic = (item) -> 5 | Hotot.fetchImage(item.feature_pic_url, (data) -> 6 | $scope.$apply(() -> 7 | item.feature_pic_data = window.webkitURL.createObjectURL(data) 8 | ) 9 | ) 10 | return 11 | 12 | root = exports ? this 13 | this.AbsColumn = AbsColumn 14 | -------------------------------------------------------------------------------- /core/scripts/serv.notify.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('NotifyService', ['$rootScope', 'Logger', ($rootScope, Logger) -> 4 | NotifyService = {} 5 | NotifyService.notifications = [] 6 | NotifyService.notify = (title, summary, timeout=3000, type="native") -> 7 | if type == 'native' 8 | opts = {title: title, summary: summary, timeout: timeout} 9 | hotot.notifications.create("", opts) 10 | 11 | return NotifyService 12 | ]) 13 | -------------------------------------------------------------------------------- /core/scripts/util.tabs_frame.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | root.app.controller('TabsFrameCtrl', ['$scope', ($scope) -> 3 | $scope.select = (idx) -> 4 | for v, i in $scope.tabSelected 5 | $scope.tabSelected[i] = '' 6 | $scope.tabSelected[idx] = 'selected' 7 | $scope.initTabs = (num) -> 8 | $scope.tabSelected = [] 9 | for i in [0..num] 10 | $scope.tabSelected[i] = '' 11 | $scope.tabSelected[0] = 'selected' 12 | ]) 13 | 14 | -------------------------------------------------------------------------------- /core/scripts/dialog.alert.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module('HototAlertDialog', []) 2 | app.controller('AlertCtrl', ['$scope', ($scope) -> 3 | buttons = [] 4 | hotot.bus.onMessage.addListener((request, sender, senderResponse) -> 5 | if not request.cmd 6 | return 7 | if request.cmd == 'reset_alert' 8 | reset(request.content.logs) 9 | ) 10 | 11 | reset = (logs) -> 12 | $scope.$apply(() -> 13 | ) 14 | 15 | $scope.onButtonClick = (btn) -> 16 | return 17 | 18 | $scope.getButtons = -> 19 | return [] 20 | 21 | return 22 | ]) 23 | -------------------------------------------------------------------------------- /core/styles/dialog.alert.less: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | #container { 7 | height: 100%; 8 | width: 100%; 9 | background: white; 10 | overflow: auto; 11 | .icon { 12 | 13 | } 14 | .title { 15 | -webkit-touch-callout: text; 16 | -webkit-user-select: text; 17 | user-select: text; 18 | } 19 | .text { 20 | -webkit-touch-callout: text; 21 | -webkit-user-select: text; 22 | user-select: text; 23 | } 24 | .buttons { 25 | height: 32px; 26 | .button { 27 | max-width: 180px; 28 | } 29 | .ok { 30 | 31 | } 32 | .cancel { 33 | 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/scripts/proto.base.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('Proto', ['$http', 'Logger', ($http, Logger) -> 4 | Proto = {} 5 | Proto.successHandler = (data, status, headers, config) -> 6 | pos = config.url.indexOf('?') 7 | if pos != -1 8 | url = config.url.substring(0, pos) 9 | else 10 | url = config.url 11 | Logger.info("#{status} #{config.method} #{url}") 12 | 13 | Proto.defaultErrorHandler = (data, status, headers, config) -> 14 | pos = config.url.indexOf('?') 15 | if pos != -1 16 | url = config.url.substring(0, pos) 17 | else 18 | url = config.url 19 | Logger.error("#{status} #{config.method} #{url}") 20 | return Proto 21 | ]) -------------------------------------------------------------------------------- /core/styles/dialog.log.less: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | #container { 7 | height: 100%; 8 | width: 100%; 9 | background: white; 10 | overflow: auto; 11 | -webkit-touch-callout: text; 12 | -webkit-user-select: text; 13 | user-select: text; 14 | .logs { 15 | font-size: 11px; 16 | list-style: none; 17 | padding: 5px; 18 | .item { 19 | -webkit-touch-callout: text; 20 | -webkit-user-select: text; 21 | user-select: text; 22 | color: #333; 23 | padding: 1px 0; 24 | } 25 | .warn { 26 | color: orange; 27 | } 28 | .error { 29 | color: red; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /core/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Hotot", 4 | "description": "Master your social networks.", 5 | "version": "3.0.18.6", 6 | "minimum_chrome_version": "27", 7 | "app": { 8 | "background": { 9 | "scripts": ["scripts/background.js"] 10 | } 11 | }, 12 | "icons": { 13 | "16": "icons/22x22/apps/hotot.png", 14 | "24": "icons/24x24/apps/hotot.png", 15 | "128": "icons/128x128/apps/hotot.png" 16 | }, 17 | "sandbox": { 18 | "pages": ["sandbox.html"] 19 | }, 20 | "permissions": [ 21 | "https://*/*", 22 | "http://*/*" , 23 | "storage", 24 | "webview", 25 | "contextMenus", 26 | "unlimitedStorage", 27 | "notifications", 28 | "clipboardRead", 29 | "clipboardWrite", 30 | "fileSystem", 31 | "fileSystem.write" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /core/dialogs/log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Logger 13 | 14 | 15 |
16 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /core/partials/new_slot_auth_page.html: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 |

5 |

6 |

7 |

{{error_box.text}}

8 |

9 | 10 |

11 |
12 | -------------------------------------------------------------------------------- /core/dialogs/alert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Alert 12 | 13 | 14 |
15 |

{{props.title}}

16 |
{{props.text}}
17 |
18 | {{btn.label}} 19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /core/scripts/serv.app.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('AppService', ['$rootScope', ($rootScope) -> 4 | AppService = {} 5 | AppService.cmd = '' 6 | AppService.winWidth = 0 7 | AppService.winHeight = 0 8 | AppService.mainWidth = 0 9 | AppService.mainHeight = 0 10 | AppService.maxColNum = 1 11 | AppService.keywords = '' 12 | 13 | AppService.broadcast = (cmd, values) -> 14 | this.cmd = cmd 15 | for k, v of values 16 | if this.hasOwnProperty(k) 17 | this[k] = v 18 | switch cmd 19 | when 'resize' 20 | $rootScope.$broadcast('AppResize') 21 | when 'trigger_resize' 22 | $rootScope.$broadcast('AppTriggerResize') 23 | when 'search' 24 | $rootScope.$broadcast('AppSearch') 25 | when 'leave_search_mode' 26 | $rootScope.$broadcast('leaveSearchMode') 27 | 28 | return AppService 29 | ]) 30 | -------------------------------------------------------------------------------- /core/scripts/serv.log.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('Logger', ['$rootScope', ($rootScope) -> 4 | Logger = {} 5 | Logger.capacity = 1000 6 | Logger.logs = [] 7 | 8 | Logger.add = (type, message) -> 9 | this.logs.push({type: type, message: "[#{type}] #{(new Date()).toLocaleTimeString()} #{message}"}) 10 | hotot.bus.sendMessage( 11 | {'role': 'logger', 'cmd': "log", 'content': {type: type, message: message}} 12 | ) 13 | Logger.trim() 14 | return 15 | 16 | Logger.info = (message) -> 17 | this.add('info', message) 18 | return 19 | 20 | Logger.warn = (message) -> 21 | this.add('warn', message) 22 | return 23 | 24 | Logger.error = (message) -> 25 | this.add('error', message) 26 | return 27 | 28 | Logger.trim = () -> 29 | if this.logs.length > this.capacity 30 | this.logs.splice(0, this.capacity/2) 31 | return 32 | 33 | return Logger 34 | ]) 35 | -------------------------------------------------------------------------------- /core/scripts/serv.columns_state.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('ColumnsState', ['$rootScope', ($rootScope) -> 4 | ColumnsState = {} 5 | ColumnsState.state = {} 6 | ColumnsState.bind = (key, posMode) -> 7 | if not this.state.hasOwnProperty(key) 8 | this.state[key] = 9 | position_mode: posMode 10 | since_id: '' 11 | max_id: '' 12 | previous_cursor: '' 13 | next_cursor: '' 14 | page: '' 15 | return this.state[key] 16 | 17 | ColumnsState.unbind = (key) -> 18 | if this.state.hasOwnProperty(key) 19 | delete this.state[key] 20 | return 21 | 22 | ColumnsState.get = (key) -> 23 | if this.state.hasOwnProperty(key) 24 | return this.state[key] 25 | return null 26 | 27 | ColumnsState.save = (key) -> 28 | return 29 | 30 | ColumnsState.save = (key) -> 31 | return 32 | 33 | return ColumnsState 34 | ]) 35 | -------------------------------------------------------------------------------- /core/scripts/dialog.log.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module('HototLogDialog', []) 2 | app.controller('LogCtrl', ['$scope', ($scope) -> 3 | $scope.log = [] 4 | capacity = 1000 5 | hotot.bus.onMessage.addListener((request, sender, senderResponse) -> 6 | if not request.cmd 7 | return 8 | if request.cmd == 'log' 9 | append(request.content.type, request.content.message) 10 | if request.cmd == 'reset_log' 11 | reset(request.content.logs) 12 | ) 13 | 14 | reset = (logs) -> 15 | $scope.$apply(() -> 16 | $scope.logs = logs 17 | ) 18 | 19 | append = (type, message) -> 20 | $scope.$apply(() -> 21 | $scope.logs.push({ 22 | type: type, 23 | message: "[#{type}] #{(new Date()).toLocaleTimeString()} #{message}" 24 | }) 25 | ) 26 | trim() 27 | 28 | trim = -> 29 | if $scope.log.length > capacity 30 | $scope.log.splice(0, capacity/2) 31 | return 32 | ]) 33 | -------------------------------------------------------------------------------- /core/partials/new_slot_oauth_page.html: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 |

5 |

6 |

7 |

{{error_box.text}}

8 |

9 | 10 |

11 |

12 |
13 | -------------------------------------------------------------------------------- /core/partials/settings_advanced_page.html: -------------------------------------------------------------------------------- 1 |
2 |

System

3 | 12 |

13 | Restore Defaults 14 |

15 |
-------------------------------------------------------------------------------- /core/scripts/sandbox.coffee: -------------------------------------------------------------------------------- 1 | class Controller 2 | constructor: (@sandboxSelector) -> 3 | @ready = false 4 | @msgTable = {} 5 | @sandbox = $(@sandboxSelector).get(0) 6 | 7 | register: () -> 8 | window.addEventListener("message", 9 | (ev) => 10 | # console.log('control', ev) 11 | id = ev.data.id 12 | result = ev.data.result 13 | state = ev.data.state 14 | if @msgTable.hasOwnProperty(id) 15 | if state == 0 16 | @msgTable[id](result) 17 | else 18 | console.log("error ##{state}: #{result}") 19 | delete @msgTable[id] 20 | ) 21 | @checkSandbox() 22 | 23 | checkSandbox: => 24 | if not @ready 25 | @mandate('ok?', null, (result)=> 26 | @ready = true 27 | ) 28 | setTimeout(@checkSandbox, 100) 29 | 30 | s4: () -> 31 | (((1+Math.random())*0x10000)|0).toString(16).substring(1) 32 | 33 | mandate: (cmd, context, callback) -> 34 | id = @s4() 35 | @msgTable[id] = callback 36 | @postMessage(id, cmd, context) 37 | 38 | postMessage: (id, cmd, context) -> 39 | msg = {id: id, cmd: cmd, context: context} 40 | # console.log('post message to sandbox', msg) 41 | if @sandbox.contentWindow 42 | @sandbox.contentWindow.postMessage(msg, "*") 43 | 44 | root = exports ? self 45 | root.SandBoxCtrl = Controller 46 | 47 | -------------------------------------------------------------------------------- /core/scripts/serv.conn.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('ConnManager', ['HototSlot', 'HototDaemon', 'AppService', 4 | (HototSlot, HototDaemon, AppService) -> 5 | ConnManager = {} 6 | ConnManager.protocols = {} 7 | ConnManager.clients = {} 8 | 9 | ConnManager.init = ()-> 10 | HototSlot.all((slots) -> 11 | for slot in slots 12 | conn = ConnManager.createConn(slot.serv, slot.name) 13 | conn.config(slot) 14 | HototDaemon.run() 15 | ) 16 | 17 | ConnManager.createConn = (serv, name) -> 18 | if ConnManager.protocols.hasOwnProperty(serv) 19 | key = serv + '/' + name 20 | client = new ConnManager.protocols[serv]() 21 | ConnManager.clients[key] = client 22 | # @BUG the streaming should be slot specified. 23 | if client.supportStream 24 | HototDaemon.registerStream(serv, name, client.buildWatcher()) 25 | return client 26 | 27 | ConnManager.getConn = (serv, slotName) -> 28 | key = serv + '/' + slotName 29 | if ConnManager.clients.hasOwnProperty(key) 30 | return ConnManager.clients[key] 31 | return null 32 | 33 | ConnManager.getProto = (serv) -> 34 | if ConnManager.protocols.hasOwnProperty(serv) 35 | return ConnManager.protocols[serv] 36 | return null 37 | 38 | ConnManager.addProto = (serv, protocol) -> 39 | ConnManager.protocols[serv] = protocol 40 | 41 | return ConnManager 42 | ]) 43 | -------------------------------------------------------------------------------- /core/scripts/lib.oauth2.coffee: -------------------------------------------------------------------------------- 1 | 2 | # A JavaScript implementation for Sina Weibo's OAuth(OAuth 2.0) 3 | # Version 0.1 Copyright Shellex Wai<5h3ll3x@gmail.com> 2009 - 2013. 4 | # Distributed under the MIT License 5 | # See http://oauth.net/ for details. 6 | root = exports ? this 7 | 8 | root.app.factory('OAuth2', ['$http', ($http) -> 9 | buildFn = () -> 10 | lib = {} 11 | lib.oauth_base = 'https://api.weibo.com/oauth2/' 12 | lib.access_token_url = 'access_token' 13 | lib.user_auth_url = 'authorize' 14 | lib.key = '' 15 | lib.secret = '' 16 | lib.access_token = null 17 | lib.redirect_uri = 'https://api.weibo.com/oauth2/default.html' 18 | 19 | lib.get_auth_url = () -> 20 | return "#{lib.oauth_base}#{lib.user_auth_url}?client_id=#{lib.key}&redirect_uri=#{lib.redirect_uri}&display=wap&forcelogin=true" 21 | 22 | lib.get_access_token = (pin, on_success, on_error) -> 23 | params = 24 | client_id: lib.key 25 | client_secret: lib.secret 26 | grant_type: 'authorization_code' 27 | redirect_uri: lib.redirect_uri 28 | code: pin 29 | $http({method: 'POST', url: lib.oauth_base + lib.access_token_url, data: params}). 30 | success((data, status, headers, config) -> 31 | lib.access_token = data 32 | if on_success then on_success(data) 33 | ). 34 | error((data, status, headers, config) -> 35 | if on_error then on_error(data) 36 | ) 37 | return 38 | return lib 39 | return buildFn 40 | ]) 41 | -------------------------------------------------------------------------------- /core/scripts/hotot_app.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | root.app = angular.module('Hotot', ['ngSanitize'], ['$httpProvider', ($httpProvider) -> 3 | # Use x-www-form-urlencoded Content-Type 4 | $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8' 5 | 6 | # Override $http service's default transformRequest 7 | $httpProvider.defaults.transformRequest = [ 8 | (data) -> 9 | param = (obj) -> 10 | query = [] 11 | for name, value of obj 12 | if value != undefined and value != null 13 | query.push(Hotot.quote(name) + '=' + Hotot.quote(value)) 14 | return query.join('&') 15 | if data and data.constructor == Object # don't use angular.isObject 16 | ret = param(data) 17 | else 18 | ret = data 19 | if ret == undefined 20 | ret = '' 21 | return ret 22 | ] 23 | ]) 24 | bindDriective(root.app, ['KEY', 'MISC', 'MOUSE', 'ANI', 'SCROLL', 'DND']) 25 | root.app.run(['HototSlot', 'HototColumn', 'HototBus', 'ConnManager', 'ProtoTwitter', 'ProtoWeibo', 'MessageService', 'SettingsService', 'RelationService', 'SliderService', 26 | (HototSlot, HototColumn, HototBus, ConnManager, ProtoTwitter, ProtoWeibo, MessageService, SettingsService, RelationService, SliderService) -> 27 | HototBus.init() 28 | HototSlot.init() 29 | HototColumn.init() 30 | ConnManager.addProto('twitter', ProtoTwitter) 31 | ConnManager.addProto('weibo', ProtoWeibo) 32 | ConnManager.init() 33 | MessageService.init() 34 | SettingsService.init() 35 | RelationService.init() 36 | SliderService.init() 37 | ]) 38 | -------------------------------------------------------------------------------- /core/scripts/ctrl.win.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | root.app.controller('WindowCtrl', ['$scope', 'SettingsService', 'Logger', 3 | ($scope, SettingsService, Logger) -> 4 | holding = false 5 | win = null 6 | offsetY = 0 7 | offsetX = 0 8 | $scope.hasFocus = true 9 | 10 | $scope.titleBarCls = '' 11 | 12 | root.onfocus = -> 13 | $scope.$apply(()-> 14 | $scope.hasFocus = true 15 | ) 16 | 17 | root.onblur = -> 18 | $scope.$apply(()-> 19 | $scope.hasFocus = false 20 | ) 21 | 22 | $scope.initWinCtrl = () -> 23 | setTimeout(()-> 24 | x = SettingsService.getForbiddens('theme') 25 | win = hotot.window.current() 26 | switch SettingsService.getForbiddens('theme') 27 | when 'auto' 28 | $scope.titleBarCls = if Hotot.detectOS() == 'osx' then 'osx' else 'other' 29 | when 'osx', 'other' 30 | $scope.titleBarCls = SettingsService.getForbiddens('theme') 31 | else 32 | $scope.titleBarCls = if Hotot.detectOS() == 'osx' then 'osx' else 'other' 33 | , 1000 34 | ) 35 | return 36 | 37 | $scope.getTitleBarCls = () -> 38 | return $scope.titleBarCls + " " + if not $scope.hasFocus then 'lose_focus' else '' 39 | 40 | $scope.minimizeWindow = -> 41 | win.minimize() 42 | return 43 | 44 | $scope.normalizeWindow = -> 45 | if not win.isMaximized() 46 | win.maximize() 47 | else 48 | win.restore() 49 | return 50 | 51 | $scope.closeWindow = -> 52 | win.close() 53 | return 54 | 55 | $scope.fullscreenWindow = -> 56 | win.fullscreen() 57 | 58 | ]) 59 | -------------------------------------------------------------------------------- /core/scripts/background.coffee: -------------------------------------------------------------------------------- 1 | 2 | # application entry 3 | start = -> 4 | chrome.storage.local.get("WINDOW_BOUNDS", (result) -> 5 | bounds = result['WINDOW_BOUNDS'] 6 | if bounds and bounds.length != 0 7 | if bounds.width < 342 8 | bounds.width = 342 9 | if bounds.width > window.screen.availWidth 10 | bounds.width = window.screen.availWidth 11 | if bounds.height < 500 12 | bounds.height = 500 13 | if bounds.height > window.screen.availHeight 14 | bounds.height = window.screen.availHeight 15 | if bounds.top < window.screen.availTop 16 | bounds.top = window.screen.availTop 17 | if bounds.top > window.screen.availHeight - bounds.height 18 | bounds.top = window.screen.availHeight - bounds.height 19 | if bounds.left < window.screen.availLeft 20 | bounds.left = window.screen.availLeft 21 | if bounds.left > window.screen.availWidth - bounds.width 22 | bounds.left = window.screen.availWidth - bounds.width 23 | else 24 | bounds = { 25 | width: 342 26 | height: 500 27 | left: parseInt(window.screen.availWidth - 342)/2 28 | top: parseInt(window.screen.availHeight - 500)/2 29 | } 30 | chrome.app.window.create('index.html', { 31 | 'width': bounds.width, 32 | 'height': bounds.height, 33 | 'minWidth': 300+42, 34 | 'minHeight': 500, 35 | 'hidden': true, 36 | 'frame': 'chrome' 37 | }, (win) -> 38 | win.moveTo(bounds.left, bounds.top) 39 | ) 40 | ) 41 | 42 | chrome.app.runtime.onLaunched.addListener(start) 43 | -------------------------------------------------------------------------------- /core/dialogs/preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Preview 14 | 15 | 16 |
17 |
18 | 19 |
loading…
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /core/styles/dialog.profile.less: -------------------------------------------------------------------------------- 1 | html,body { 2 | height: 100%; 3 | width: 100%; 4 | background: rgb(237,237,237); 5 | } 6 | 7 | #container { 8 | height: 100%; 9 | width: 100%; 10 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #e6e6e8)); 11 | overflow: auto; 12 | display: -webkit-flex; 13 | -webkit-flex-direction: column; 14 | } 15 | .top { 16 | -webkit-flex: 0 0 28px; 17 | padding: 2px 6px; 18 | -webkit-app-region: drag; 19 | box-shadow: 0 0 10px rgba(0,0,0,0.1); 20 | right: 0; 21 | left: 0; 22 | z-index: 100; 23 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, rgba(255,255,255,0.9))); 24 | } 25 | .body { 26 | -webkit-flex: 1 0 auto; 27 | -webkit-order: 1; 28 | padding: 6px; 29 | font-size: 14px; 30 | .mochi_list_item { 31 | .label { 32 | font-weight: normal; 33 | color: #333; 34 | } 35 | .mochi_entry, .mochi_textarea { 36 | width: 240px; 37 | } 38 | } 39 | .avatar_wrapper { 40 | height: 48px; 41 | .avatar { 42 | height: 48px; 43 | } 44 | .label { 45 | line-height: 48px; 46 | } 47 | .popup { 48 | float: right; 49 | font-size: 12px; 50 | padding: 4px 8px; 51 | height: 40px; 52 | text-align: center; 53 | background: #f2f2f2; 54 | line-height: 38px; 55 | width: 176px; 56 | .mochi_button { 57 | font-size: 10px; 58 | line-height: 18px; 59 | height: 20px; 60 | padding: 0 6px; 61 | margin: 0 4px; 62 | } 63 | } 64 | } 65 | .textarea_wrapper { 66 | height: 60px; 67 | .mochi_textarea { 68 | height: 60px; 69 | } 70 | } 71 | .save { 72 | width: 100%; 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /core/scripts/util.hotkey.coffee: -------------------------------------------------------------------------------- 1 | SHIFT = 16 2 | CTRL = 17 3 | OPTION = 18 4 | CMD = 91 5 | MAC_CTRL = 17 6 | UP = 38 7 | DOWN = 40 8 | class HotkeyUtils 9 | @SHIFT = SHIFT 10 | @CTRL = CTRL # dont use ctrl directly 11 | @OPTION = OPTION 12 | @CMD = CMD 13 | @MAC_CTRL = MAC_CTRL 14 | @UP = UP 15 | @DOWN = DOWN 16 | 17 | @listeners = {} 18 | 19 | @SPECIAL_KEYS = { 20 | SHIFT: null, CTRL: null, OPTION: null, CMD: null, MAC_CTRL: null, 21 | UP: null, DOWN: null 22 | } 23 | 24 | @ALPHASET = (keyCode) -> 25 | return 65 <= keyCode and keyCode <= 90 26 | 27 | @NUMBER = (keyCode) -> 28 | return 48 <= keyCode and keyCode <= 57 29 | 30 | @crack = (evt) -> 31 | keys = [] 32 | if Hotot.detectOS() == 'osx' 33 | if evt.metaKey then keys.push(@CMD) 34 | else 35 | if evt.ctrlKey then keys.push(@CMD) 36 | if evt.shiftKey then keys.push(@SHIFT) 37 | if evt.altKey then keys.push(@OPTION) 38 | if evt.keyCode 39 | keyCode = evt.keyCode 40 | if @SPECIAL_KEYS.hasOwnProperty(evt.keyCode) or @ALPHASET(evt.keyCode) or @NUMBER(evt.keyCode) 41 | keys.push(evt.keyCode) 42 | else 43 | return 44 | keys.sort() 45 | keys = keys.join(' ') 46 | if @listeners.hasOwnProperty(keys) 47 | for lis in @listeners[keys] 48 | evt.preventDefault() 49 | lis(evt) 50 | return 51 | 52 | @register = (keys, callback) -> 53 | _keys = [] 54 | for key in keys 55 | if typeof key == "number" 56 | _keys.push(key) 57 | else if typeof key == 'string' 58 | _keys.push(key.charCodeAt(0)) 59 | _keys.sort() 60 | _keys = _keys.join(' ') 61 | if @listeners.hasOwnProperty(_keys) 62 | @listeners[_keys].push(callback) 63 | else 64 | @listeners[_keys] = [callback] 65 | return 66 | 67 | root = exports ? this 68 | root.HotkeyUtils = HotkeyUtils 69 | -------------------------------------------------------------------------------- /core/styles/new_slot.less: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | } 4 | #container { 5 | width: 560px; 6 | .side { 7 | width: 160px; 8 | height: 340px; 9 | float: left; 10 | } 11 | .body { 12 | width: 400px; 13 | height: 340px; 14 | margin-left: 162px; 15 | .page { 16 | display: block; 17 | padding: 20px 30px; 18 | } 19 | .auth { 20 | padding: 40px 60px; 21 | label { 22 | font-weight: bold; 23 | } 24 | .username, .password { 25 | width: 280px; 26 | } 27 | .signin_button { 28 | .icon { 29 | display: none; 30 | margin-right: 10px; 31 | } 32 | } 33 | .signin_button.processing { 34 | .label { 35 | content: "Processing" 36 | } 37 | } 38 | } 39 | .oauth { 40 | .pin { 41 | width: 120px; 42 | } 43 | webview { 44 | overflow: auto; 45 | } 46 | } 47 | } 48 | } 49 | #container { 50 | .services { 51 | .twitter { 52 | .icon { 53 | background-image: url(../images/service/twitter.png); 54 | } 55 | } 56 | .statusnet { 57 | .icon { 58 | background-image: url(../images/service/statusnet.png); 59 | } 60 | } 61 | .weibo { 62 | .icon { 63 | background-image: url(../images/service/weibo.png); 64 | } 65 | } 66 | .facebook { 67 | .icon { 68 | background-image: url(../images/service/facebook.png); 69 | } 70 | } 71 | .gplus { 72 | .icon { 73 | background-image: url(../images/service/gplus.png); 74 | } 75 | } 76 | } 77 | } 78 | @-webkit-keyframes spin { 79 | from { 80 | -webkit-transform: rotate(0deg); 81 | } 82 | to { 83 | -webkit-transform: rotate(360deg); 84 | } 85 | } -------------------------------------------------------------------------------- /core/partials/settings_general_page.html: -------------------------------------------------------------------------------- 1 |
2 | 13 | 25 |

Preview

26 |
27 |

The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.

28 |

The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.

29 |
30 |
-------------------------------------------------------------------------------- /core/scripts/ctrl.column_area.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | root.app.controller('ColumnAreaCtrl', ['$scope', 'SliderService', 'AppService', 'HototBus', 'HototSlot', 'HototColumn', 3 | ($scope, SliderService, AppService, HototBus, HototSlot, HototColumn) -> 4 | $scope.columnTable = SliderService.columnTable 5 | $scope.slotList = SliderService.slotList 6 | $scope.columnCommProps = {width:300, lastWidth:300, bodyHeight: '100%'} 7 | $scope.props = 8 | areaHeight: 'auto' 9 | areaWidth: 'auto' 10 | columnAreaTransformX: '' 11 | 12 | maxColNum = 1 13 | currentSlotIdx = 0 14 | currentSlot = null 15 | 16 | resize = (mainWidth, mainHeight, maxColNum) -> 17 | slotColNum = SliderService.getSlotColumnNum() 18 | if slotColNum < maxColNum 19 | maxColNum = slotColNum 20 | if maxColNum > 0 21 | # adjust all views' width 22 | $scope.columnCommProps.width = parseInt(mainWidth/maxColNum) 23 | # adjust the last one 24 | $scope.columnCommProps.lastWidth = mainWidth - (maxColNum - 1) * $scope.columnCommProps.width 25 | $scope.columnCommProps.bodyHeight = mainHeight 26 | $scope.props.areaHeight = mainHeight 27 | $scope.props.areaWidth = mainWidth 28 | 29 | sync = -> 30 | colObj = SliderService.currentColumnTableObject() 31 | if colObj 32 | # do slide 33 | # $scope.props.cantainerMarginLeft = 0 - colObj.activeOffset * $scope.columnCommProps.width 34 | offset = 0 - colObj.activeOffset * $scope.columnCommProps.width 35 | $scope.props.columnAreaTransformX = 'translateX('+offset+'px)'; 36 | 37 | 38 | SliderService.stateChanged(sync) 39 | 40 | $scope.$on('AppResize', () -> 41 | resize(AppService.mainWidth, AppService.mainHeight, AppService.maxColNum) 42 | $scope.$apply() 43 | ) 44 | 45 | $scope.getColumnWidth = (isLastOne) -> 46 | val = if not isLastOne then $scope.columnCommProps.width else $scope.columnCommProps.lastWidth 47 | return {'width': val + 'px'} 48 | 49 | $scope.getAreaHeight = () -> 50 | return {'height': $scope.props.areaHeight + 'px', 'width': $scope.props.areaWidth + 'px'} 51 | 52 | $scope.getColumnBodyStyles = () -> 53 | sty = {'height': $scope.columnCommProps.bodyHeight + 'px'} 54 | if Hotot.detectOS() != 'osx' 55 | sty['overflow-y'] = 'auto' 56 | return sty 57 | 58 | $scope.getCantainerStyles = () -> 59 | return {'-webkit-transform': $scope.props.columnAreaTransformX} 60 | 61 | return 62 | ]) 63 | -------------------------------------------------------------------------------- /core/partials/settings_behavior_page.html: -------------------------------------------------------------------------------- 1 |
2 | 14 | 32 |
-------------------------------------------------------------------------------- /core/dialogs/new_slot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | New Account 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 25 | 27 | 28 | 29 | 30 | 31 | 37 | 43 | 45 | 46 | 47 | 48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 | 56 |
57 |
58 | 59 | -------------------------------------------------------------------------------- /core/styles/component.less: -------------------------------------------------------------------------------- 1 | .account_selector { 2 | -webkit-app-region: no-drag; 3 | height: 28px; 4 | width: 28px; 5 | display: block; 6 | border: none; 7 | cursor: default; 8 | background: #ccc; 9 | position: relative; 10 | border-radius: 2px; 11 | .popup_menu { 12 | width: 120px; 13 | display: none; 14 | z-index: 100; 15 | padding: 10px 2px 6px 10px; 16 | top: 30px; 17 | .accounts { 18 | .slot { 19 | width: 32px; 20 | height: 32px; 21 | display: block; 22 | float: left; 23 | position: relative; 24 | margin: 0 8px 8px 0; 25 | opacity: 0.9; 26 | border-radius: 2px; 27 | .icon { 28 | display: block; 29 | width: 32px; 30 | height: 32px; 31 | background: white none 0 0 no-repeat; 32 | background-size: contain; 33 | border-radius: 2px; 34 | } 35 | .mask { 36 | display: block; 37 | width: 32px; 38 | height: 32px; 39 | position: absolute; 40 | top: 0px; 41 | left: 0px; 42 | box-shadow: inset 0 0 6px rgba(0,0,0,0.3), inset 0 0 1px 1px rgba(0,0,0,0.3); 43 | border-radius: 2px; 44 | } 45 | } 46 | .slot.selected { 47 | box-shadow: 0 0 0 2px rgb(70, 139, 223); 48 | } 49 | .add_slot { 50 | display: block; 51 | height: 32px; 52 | float: left; 53 | width: 32px; 54 | text-align: center; 55 | border: 2px dotted #ccc; 56 | margin: 0 8px 8px 0; 57 | font-size: 14px; 58 | font-weight: bold; 59 | color: #ccc; 60 | box-sizing: border-box; 61 | border-radius: 2px; 62 | i { 63 | opacity: 0.4; 64 | margin: 6px; 65 | } 66 | } 67 | } 68 | .sharp { 69 | top: -8px; 70 | left: 10px; 71 | } 72 | .helper { 73 | top: -16px; 74 | left: 13px; 75 | width: 64px; 76 | height: 48px; 77 | -webkit-transform: rotateZ(20deg); 78 | } 79 | .bottom { 80 | clear: both; 81 | } 82 | } 83 | } 84 | .account_selector > .icon { 85 | display: inline-block; 86 | width: 22px; 87 | height: 22px; 88 | background: #ffffff none 0 0 no-repeat; 89 | background-size: contain; 90 | margin: 3px; 91 | } 92 | .account_selector:hover { 93 | .popup_menu { 94 | display: block; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /core/dialogs/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Profile 16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 53 |
54 |
55 |
56 |
57 | 58 | -------------------------------------------------------------------------------- /core/scripts/fake_source.coffee: -------------------------------------------------------------------------------- 1 | class FakeSource 2 | @buildItemFromTweet: (tweet)-> 3 | item = 4 | # principal 5 | serv: 'twitter' 6 | id: tweet.id_str 7 | # core 8 | title: tweet.user.name 9 | URL: "https://twitter.com/#{tweet.user.screen_name}/status/#{tweet.id_str}" 10 | text: tweet.text 11 | time: new Date(tweet.created_at).toDateString() 12 | author_id: tweet.user.id_str 13 | author_name: tweet.user.screen_name 14 | author_url: "https://twitter.com/#{tweet.user.screen_name}" 15 | # extend 16 | previous_item_id: tweet.in_reply_to_status_id_str 17 | next_item_id: '' 18 | recipient_id: tweet.in_reply_to_user_id_str 19 | recipient_name: tweet.in_reply_to_screen_name 20 | feature_pic_url: tweet.user.profile_image_url 21 | source: tweet.source 22 | entities: tweet.entities 23 | deletable: false 24 | repostable: true 25 | favorited: tweet.favorited 26 | reposted: tweet.retweeted 27 | original_id: '' 28 | reposter_id: '' 29 | reposter_name: '' 30 | has_comments: (tweet.in_reply_to_status_id_str != null) 31 | media_type: 'none' 32 | media: [] 33 | 34 | return item 35 | 36 | @items = [] 37 | for t in fake_home_timeline 38 | @items.push(@buildItemFromTweet(t)) 39 | 40 | @item = 41 | # principal 42 | serv: 'twitter' 43 | id: "272614791342862336" 44 | # core 45 | title: "weimin" 46 | URL: "https://twitter.com/dr_filter_ch/status/91017586954862592" 47 | text: "18. MBP好暖和!" 48 | time: 1310535258000 49 | author_id: "17481583" 50 | author_name: "dr_filter_ch" 51 | author_url: "https://twitter.com/dr_filter_ch" 52 | # extend 53 | minor_title: "dr_filter_ch" 54 | previous_item_id: "91016252461547520" 55 | next_item_id: '' 56 | recipient_id: '' 57 | recipient_name: '' 58 | feature_pic_url: "http://a0.twimg.com/sticky/default_profile_images/default_profile_1_normal.png" 59 | source: ["hotot", "http://hotot.org"] 60 | entities: { 61 | hashtags: [], 62 | urls: [], 63 | user_mentions: [ 64 | { 65 | "screen_name": "shellex", 66 | "name": "壳酱", 67 | "id": '17481583', 68 | "indices": [0, 8] 69 | } 70 | ] 71 | }, 72 | deletable: false 73 | repostable: true 74 | favorited: false 75 | reposted: false 76 | original_id: '' 77 | reposter_id: '' 78 | reposter_name: '' 79 | media_type: 'none' 80 | media: [] 81 | 82 | @getSingleItem: -> 83 | xx = {} 84 | angular.extend(xx, @item) 85 | return xx 86 | 87 | @getItems: (start, end)-> 88 | start = start or 0 89 | shuffle = (items) -> 90 | for i in [0...items.length] 91 | a = parseInt(Math.random()*10000)%items.length 92 | b = parseInt(Math.random()*10000)%items.length 93 | t = items[a] 94 | items[a] = items[b] 95 | items[b] = t 96 | xx = [] 97 | for it in @items 98 | xxx = {} 99 | angular.extend(xxx, it) 100 | xx.push(xxx) 101 | return xx.slice(start, end) 102 | 103 | 104 | 105 | root = exports ? this 106 | root.FakeSource = FakeSource 107 | 108 | -------------------------------------------------------------------------------- /core/scripts/interface.coffee: -------------------------------------------------------------------------------- 1 | platform = 0 2 | PLATFORM_CHROME_OS = 0 3 | PLATFORM_MAC = 1 4 | 5 | # the follow functions use wrapper form instead of link form to prevent api changes 6 | 7 | storageAddChangedListener = (callback) -> 8 | if platform == PLATFORM_CHROME_OS 9 | return chrome.storage.onChanged.addListener(callback) 10 | return null 11 | 12 | storageLocalGet = (id, callback) -> 13 | if platform == PLATFORM_CHROME_OS 14 | return chrome.storage.local.get(id, callback) 15 | return null 16 | 17 | storageLocalSet = (pair, callback) -> 18 | if platform == PLATFORM_CHROME_OS 19 | return chrome.storage.local.set(pair, callback) 20 | return null 21 | 22 | # @TODO the createOptions and the window object should be wrapped up 23 | windowCreate = (page, opts, callback) -> 24 | if platform == PLATFORM_CHROME_OS 25 | if not callback 26 | return chrome.app.window.create(page, opts) 27 | return chrome.app.window.create(page, opts, callback) 28 | return null 29 | 30 | windowCurrent = -> 31 | if platform == PLATFORM_CHROME_OS 32 | return chrome.app.window.current() 33 | return null 34 | 35 | windowAddClosedListener = (callback) -> 36 | if platform == PLATFORM_CHROME_OS 37 | return chrome.app.window.onClosed.addListener(callback) 38 | return null 39 | 40 | busAddMessageListener = (callback) -> 41 | if platform == PLATFORM_CHROME_OS 42 | return chrome.runtime.onMessage.addListener(callback) 43 | return null 44 | 45 | busSendMessage = (message, callback) -> 46 | if platform == PLATFORM_CHROME_OS 47 | if not callback 48 | return chrome.runtime.sendMessage(message) 49 | return chrome.runtime.sendMessage(message, callback) 50 | return null 51 | 52 | notificationsCreate = (id, opts, callback) -> 53 | if platform == PLATFORM_CHROME_OS 54 | _opts = { 55 | type: "basic", 56 | title: opts.title, 57 | message: opts.summary, 58 | eventTime: opts.timeout, 59 | iconUrl: "../icons/128x128/apps/hotot.png" 60 | } 61 | if not callback 62 | return chrome.notifications.create(id, _opts, ()->) 63 | return chrome.notifications.create(id, _opts, callback) 64 | return null 65 | 66 | runtimeReload = -> 67 | if platform == PLATFORM_CHROME_OS 68 | return chrome.runtime.reload() 69 | return null 70 | 71 | fsChooseEntry = (opts, callback)-> 72 | if platform == PLATFORM_CHROME_OS 73 | if not callback 74 | return chrome.fileSystem.chooseEntry(opts, ()->) 75 | return chrome.fileSystem.chooseEntry(opts, callback) 76 | return null 77 | 78 | hotot = 79 | storage: 80 | onChanged: 81 | addListener: storageAddChangedListener 82 | local: 83 | get: storageLocalGet 84 | set: storageLocalSet 85 | window: 86 | onClosed: 87 | addListener: windowAddClosedListener 88 | create: windowCreate 89 | current: windowCurrent 90 | bus: { 91 | onMessage: 92 | addListener: busAddMessageListener 93 | sendMessage: busSendMessage 94 | } 95 | fs: { 96 | chooseEntry: fsChooseEntry 97 | } 98 | notifications: 99 | create: notificationsCreate 100 | runtime: 101 | reload: runtimeReload 102 | 103 | this.hotot = hotot 104 | -------------------------------------------------------------------------------- /core/styles/settings.less: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | width: 640px; 4 | overflow: auto; 5 | background: rgb(237,237,237); //-webkit-gradient(linear, left top, left bottom, from(#FEFEFE), to(#EDEDED)); 6 | } 7 | 8 | #container { 9 | .side { 10 | width: 160px; 11 | height: 440px; 12 | float: left; 13 | position: relative; 14 | .shadow { 15 | position: absolute; 16 | top: 0px; 17 | right: 0px; 18 | width: 20px; 19 | height: 100%; 20 | background: transparent url(../images/side_shadow.png) 0 0 repeat-y; 21 | } 22 | } 23 | .body { 24 | margin-left: 162px; 25 | .page { 26 | display: block; 27 | height: 420px; 28 | overflow: auto; 29 | h3 { 30 | font-weight: bold; 31 | padding: 5px 0; 32 | } 33 | .mochi_list_item .label { 34 | font-weight: normal; 35 | color: #333; 36 | } 37 | } 38 | .account_page { 39 | .wrapper { 40 | border: 1px #bbb solid; 41 | } 42 | .account_box { 43 | display: block; 44 | height: 380px; 45 | overflow: auto; 46 | background-color: white; 47 | border-bottom: 1px #bbb solid; 48 | .account_list { 49 | list-style: none; 50 | } 51 | .account { 52 | height: 22px; 53 | padding: 3px 8px; 54 | .avatar { 55 | height: 22px; 56 | width: 22px; 57 | vertical-align: middle; 58 | } 59 | .name { 60 | vertical-align: middle; 61 | } 62 | .serv { 63 | vertical-align: middle; 64 | } 65 | } 66 | .account:nth-child(even) { 67 | background-color: #f4f4f4; 68 | } 69 | .account.selected { 70 | background-color: #29f; 71 | color: white; 72 | } 73 | } 74 | .control { 75 | height: 24px; 76 | background: -webkit-gradient( 77 | linear, left top, left bottom, 78 | color-stop(0%, rgb(249,249,249)), color-stop(49%, rgb(242,242,242)), 79 | color-stop(50%, rgb(237,237,237)), color-stop(100%, rgb(240,240,240)) 80 | ); 81 | .button { 82 | display: inline-block; 83 | height: 24px; 84 | width: 24px; 85 | text-decoration: none; 86 | color: black; 87 | text-align: center; 88 | line-height: 24px; 89 | opacity: 0.6; 90 | border-right: 1px #aaa solid; 91 | text-shadow: 0 1px 1px #fff; 92 | } 93 | .button:hover { 94 | opacity: 1; 95 | } 96 | .button.disabled { 97 | color: #aaa; 98 | } 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /core/scripts/serv.cache.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('HototCache', ['$rootScope', ($rootScope) -> 4 | HototCache = {} 5 | HototCache.capacity = 100 6 | HototCache.visibleRegion = 32 7 | HototCache.cache = {} 8 | HototCache.comments = {} 9 | 10 | HototCache.bind = (key) -> 11 | if not this.cache.hasOwnProperty(key) 12 | this.cache[key] = [] 13 | return this.cache[key] 14 | 15 | HototCache.unbind = (key) -> 16 | if this.cache.hasOwnProperty(key) 17 | delete this.cache[key] 18 | return 19 | 20 | HototCache.add = (key, item) -> 21 | if this.cache.hasOwnProperty(key) 22 | this.cache[key].push(item) 23 | return 24 | 25 | HototCache.fetch = (key, item) -> 26 | if this.cache.hasOwnProperty(key) 27 | return this.cache[key] 28 | return [] 29 | 30 | HototCache.trim = (key) -> 31 | if this.cache.hasOwnProperty(key) 32 | if this.cache[key].length > this.capacity 33 | for i in [this.capacity/2 ... this.cache[key].length] 34 | if this.comments.hasOwnProperty(this.cache[key][i].id) 35 | delete this.comments[this.cache[key][i].id] 36 | this.cache[key].splice(this.capacity/2, this.capacity/2) 37 | return 38 | 39 | HototCache.compress = (key) -> 40 | if this.cache.hasOwnProperty(key) 41 | if this.cache[key].length > this.visibleRegion 42 | for i in [this.visibleRegion ... this.cache[key].length] 43 | this.cache[key][i].hide = true 44 | return 45 | 46 | HototCache.expand = (key) -> 47 | if this.cache.hasOwnProperty(key) 48 | if this.cache[key].length > this.visibleRegion 49 | for i in [this.visibleRegion ... this.cache[key].length] 50 | this.cache[key][i].hide = false 51 | return 52 | 53 | HototCache.findById = (keys, id) -> 54 | # @TODO use bsearch 55 | for key in keys 56 | if this.cache.hasOwnProperty(key) 57 | for item, idx in this.cache[key] 58 | if item.id == id 59 | return [item, idx] 60 | return [null, -1] 61 | 62 | HototCache.getComments = (key, id) -> 63 | if this.comments.hasOwnProperty(key) 64 | if this.comments[key].hasOwnProperty(id) 65 | return [this.comments[key][id].state, this.comments[key][id].data] 66 | return [0, []] 67 | 68 | HototCache.setComments = (key, id, state, comments) -> 69 | if not this.comments.hasOwnProperty(key) 70 | this.comments[key] = {} 71 | if not this.comments[key].hasOwnProperty(id) 72 | this.comments[key][id] = {} 73 | this.comments[key][id].state = state # 0 = fold, 1 = open, 2 = loading 74 | this.comments[key][id].data = comments 75 | return 76 | 77 | HototCache.getCommentsState = (key, id) -> 78 | if this.comments.hasOwnProperty(key) 79 | if this.comments[key].hasOwnProperty(id) 80 | return this.comments[key][id].state 81 | return 0 82 | 83 | HototCache.setCommentsState = (key, id, state) -> 84 | if not this.comments.hasOwnProperty(key) 85 | this.comments[key] = {} 86 | if not this.comments[key].hasOwnProperty(id) 87 | this.comments[key][id] = {} 88 | this.comments[key][id].state = state 89 | return 90 | 91 | return HototCache 92 | ]) 93 | -------------------------------------------------------------------------------- /core/styles/dialog.preview.less: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | #container { 7 | height: 100%; 8 | width: 100%; 9 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f2f2f2)); 10 | overflow: hidden; 11 | -webkit-flex-direction: column; 12 | display: -webkit-flex; 13 | position: relative; 14 | .control { 15 | position: absolute; 16 | right: 20px; 17 | bottom: 80px; 18 | a { 19 | width: 32px; 20 | height: 32px; 21 | display: block; 22 | text-align: center; 23 | line-height: 30px; 24 | margin-bottom: 1px; 25 | background: white; 26 | opacity: 0.2; 27 | transition: opacity 0.2s ease; 28 | } 29 | .download { 30 | margin-top: 4px; 31 | } 32 | a:hover { 33 | opacity: 0.4; 34 | } 35 | } 36 | .previewer { 37 | -webkit-flex: 1 0 auto; 38 | -webkit-order: 1; 39 | overflow: auto; 40 | height: 100%; 41 | width: 100%; 42 | .image { 43 | display: block; 44 | margin: 2% auto 2% auto; 45 | box-shadow: 1px 1px 4px #999; 46 | opacity: 1; 47 | -webkit-animation-name: ani_fade_in; 48 | -webkit-animation-duration: 0.5s; 49 | -webkit-animation-iteration-count: 1; 50 | -webkit-animation-timing-function: ease; 51 | transition: all 0.2s ease; 52 | } 53 | .image.hide { 54 | display: none; 55 | } 56 | .placeholder { 57 | display: block; 58 | box-shadow: none; 59 | width: 200px; 60 | height: 50px; 61 | position: absolute; 62 | top: 50%; 63 | left: 50%; 64 | margin-top: -30px; 65 | margin-left: -100px; 66 | text-align: center; 67 | } 68 | } 69 | .slider { 70 | padding: 5px; 71 | height: 44px; 72 | -webkit-order: 2; 73 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(255,255,255,0.15)), color-stop(100%, rgba(255,255,255,0.2))); 74 | position: fixed; 75 | bottom: 0; 76 | left: 0; 77 | right: 0; 78 | border-top: 1px rgba(255,255,255,0.2) solid; 79 | opacity: 0.1; 80 | transition: opacity 0.2s ease; 81 | .pictures { 82 | margin-left: 0; 83 | width: 1000px; 84 | left: 50%; 85 | position: absolute; 86 | transition: all 0.2s ease; 87 | } 88 | .picture { 89 | color: #333; 90 | width: 42px; 91 | height: 42px; 92 | display: block; 93 | margin: 0 4px; 94 | float: left; 95 | img { 96 | max-width: 38px; 97 | max-height: 38px; 98 | border: 2px solid black; 99 | } 100 | } 101 | .glass { 102 | position: absolute; 103 | width: 54px; 104 | height: 54px; 105 | bottom: 0; 106 | left: 50%; 107 | margin-left: -27px; 108 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(255, 255, 255, 0.3)), color-stop(100%, rgba(255, 255, 255, 0.3))); 109 | } 110 | } 111 | .slider:hover { 112 | opacity: 1; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /core/scripts/com.account_selector.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | root.app.directive('accountSelector', () -> 3 | return { 4 | restrict: 'E', 5 | transclude: true, 6 | scope: {slots: '=', current: '=', selection: '=', useAddButton: '@', multiSelection: '@', onChanged: '=', onAdd: '='}, 7 | controller: ($scope, $element) -> 8 | $scope.useAddButton = true 9 | $scope.multiSelection = false 10 | $scope.selection = null 11 | $scope.getCurrentAvatar = () -> 12 | if $scope.current and $scope.current.avatar 13 | return $scope.current.avatar 14 | return '' 15 | 16 | $scope.isAddButtonShow = -> 17 | return $scope.useAddButton 18 | 19 | $scope.selectSlot = (index) -> 20 | if not $scope.multiSelection 21 | for i in [0...$scope.slots.length] 22 | $scope.slots[i].selected = false 23 | $scope.slots[index].selected = true 24 | $scope.current = $scope.selection = $scope.slots[index] 25 | else 26 | if $scope.slots[index].selected 27 | if $scope.selection.length == 1 # don't allow to remove disselect 28 | $scope.current = $scope.slots[0] 29 | else 30 | $scope.slots[index].selected = false 31 | for c, i in $scope.selection 32 | if c.name == $scope.slots[index].name and c.serv == $scope.slots[index].serv 33 | $scope.selection.splice(i, 1) 34 | break 35 | else 36 | $scope.slots[index].selected = true 37 | $scope.selection.push($scope.slots[index]) 38 | if $scope.onChanged 39 | $scope.onChanged($scope.selection, index) 40 | 41 | $scope.addSlot = -> 42 | if $scope.onAdd 43 | $scope.onAdd() 44 | 45 | $scope.getSlotSelectedStyle = (slot) -> 46 | return if slot.selected then 'selected' else '' 47 | 48 | $scope.getAvatar = (slot) -> 49 | return slot.avatar 50 | 51 | , 52 | link: (scope, element, attrs, ctrl) -> 53 | scope.$watch('current', ()-> 54 | if scope.current and not scope.current.selected 55 | scope.current.selected = true 56 | ) 57 | return 58 | , 59 | template: 60 | """ 61 |
62 | 63 | 78 |
79 | """ 80 | , 81 | replace: true 82 | } 83 | ) -------------------------------------------------------------------------------- /core/scripts/angular.sanitize.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.5 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(I,g){'use strict';function i(a){var d={},a=a.split(","),b;for(b=0;b=0;e--)if(f[e]==b)break;if(e>=0){for(c=f.length-1;c>=e;c--)d.end&&d.end(f[c]);f.length= 7 | e}}var c,h,f=[],j=a;for(f.last=function(){return f[f.length-1]};a;){h=!0;if(!f.last()||!q[f.last()]){if(a.indexOf("<\!--")===0)c=a.indexOf("--\>"),c>=0&&(d.comment&&d.comment(a.substring(4,c)),a=a.substring(c+3),h=!1);else if(B.test(a)){if(c=a.match(r))a=a.substring(c[0].length),c[0].replace(r,e),h=!1}else if(C.test(a)&&(c=a.match(s)))a=a.substring(c[0].length),c[0].replace(s,b),h=!1;h&&(c=a.indexOf("<"),h=c<0?a:a.substring(0,c),a=c<0?"":a.substring(c),d.chars&&d.chars(k(h)))}else a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+ 8 | f.last()+"[^>]*>","i"),function(b,a){a=a.replace(D,"$1").replace(E,"$1");d.chars&&d.chars(k(a));return""}),e("",f.last());if(a==j)throw"Parse Error: "+a;j=a}e()}function k(a){l.innerHTML=a.replace(//g,">")}function u(a){var d=!1,b=g.bind(a,a.push);return{start:function(a,c,h){a=g.lowercase(a);!d&&q[a]&&(d=a);!d&&v[a]== 9 | !0&&(b("<"),b(a),g.forEach(c,function(a,c){var e=g.lowercase(c);if(G[e]==!0&&(w[e]!==!0||a.match(H)))b(" "),b(c),b('="'),b(t(a)),b('"')}),b(h?"/>":">"))},end:function(a){a=g.lowercase(a);!d&&v[a]==!0&&(b(""));a==d&&(d=!1)},chars:function(a){d||b(t(a))}}}var s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,r=/^<\s*\/\s*([\w:-]+)[^>]*>/,A=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,C=/^/g, 10 | E=//g,H=/^((ftp|https?):\/\/|mailto:|#)/,F=/([^\#-~| |!])/g,p=i("area,br,col,hr,img,wbr"),x=i("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=i("rp,rt"),o=g.extend({},y,x),m=g.extend({},x,i("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),n=g.extend({},y,i("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")), 11 | q=i("script,style"),v=g.extend({},p,m,n,o),w=i("background,cite,href,longdesc,src,usemap"),G=g.extend({},w,i("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),l=document.createElement("pre");g.module("ngSanitize",[]).value("$sanitize",function(a){var d=[]; 12 | z(a,u(d));return d.join("")});g.module("ngSanitize").directive("ngBindHtml",["$sanitize",function(a){return function(d,b,e){b.addClass("ng-binding").data("$binding",e.ngBindHtml);d.$watch(e.ngBindHtml,function(c){c=a(c);b.html(c||"")})}}]);g.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,d=/^mailto:/;return function(b){if(!b)return b;for(var e=b,c=[],h=u(c),f,g;b=e.match(a);)f=b[0],b[2]==b[3]&&(f="mailto:"+f),g=b.index, 13 | h.chars(e.substr(0,g)),h.start("a",{href:f}),h.chars(b[0].replace(d,"")),h.end("a"),e=e.substring(g+b[0].length);h.chars(e);return c.join("")}})})(window,window.angular); 14 | -------------------------------------------------------------------------------- /core/scripts/serv.daemon.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('HototDaemon', ['$rootScope', 'Logger', 4 | ($rootScope, Logger) -> 5 | daemon = {} 6 | daemon.funcMap = {} 7 | daemon.streamListeners = {} 8 | daemon.slotVerifiers = {} 9 | daemon.watchers = {} 10 | daemon.working = true 11 | daemon.timerInterval = 60000 # default, 60 sec per loop 12 | daemon.tick = 0 13 | 14 | 15 | daemon.bind = (key, interval, callback) -> 16 | if daemon.funcMap.hasOwnProperty(key) 17 | delete daemon.funcMap[key] 18 | else # first time run 19 | callback() 20 | daemon.funcMap[key] = {interval:interval, callback:callback} 21 | return true 22 | 23 | daemon.unbind = (key) -> 24 | if daemon.funcMap.hasOwnProperty(key) 25 | delete daemon.funcMap[key] 26 | 27 | daemon.bindVerifier = (serv, slotName, callback) -> 28 | key = "#{serv}/#{slotName}" 29 | if daemon.slotVerifiers.hasOwnProperty(key) 30 | delete daemon.slotVerifiers[key] 31 | else # first time run 32 | callback() 33 | daemon.slotVerifiers[key] = callback 34 | return true 35 | 36 | daemon.unbindVerifier = (serv, slotName) -> 37 | key = "#{serv}/#{slotName}" 38 | if daemon.slotVerifiers.hasOwnProperty(key) 39 | daemon.slotVerifiers[key] 40 | 41 | daemon.verifySlots = (tick)-> 42 | for key, verifier of daemon.slotVerifiers 43 | if verifier and tick % 600 == 0 # verify each slots per 10 minutes 44 | verifier() 45 | return 46 | 47 | daemon.poll = (key) -> 48 | if daemon.funcMap.hasOwnProperty(key) 49 | daemon.funcMap[key].callback() 50 | 51 | daemon.checkPoll = (tick) -> 52 | for key, func of daemon.funcMap 53 | if func and tick % (Math.ceil(func.interval / 60) * 60) == 0 54 | func.callback() 55 | return 56 | 57 | daemon.bindStream = (serv, slotName, colName, name, callback) -> 58 | key = "#{serv}/#{slotName}" 59 | streamKey = "#{serv}/#{slotName}/#{name}" 60 | if daemon.watchers.hasOwnProperty(key) 61 | daemon.watchers[key].bind(name, streamKey, callback) 62 | else 63 | daemon.streamListeners[key].push([streamKey, callback]) 64 | 65 | daemon.unbindStream = (serv, slotName, colName, name) -> 66 | key = "#{serv}/#{slotName}" 67 | streamKey = "#{serv}/#{slotName}/#{name}" 68 | if daemon.watchers.hasOwnProperty(key) 69 | daemon.watchers[key].unbind(name, streamKey) 70 | 71 | daemon.registerStream = (serv, slotName, watcher) -> 72 | key = "#{serv}/#{slotName}" 73 | if daemon.watchers.hasOwnProperty(key) 74 | return false 75 | else 76 | daemon.watchers[key] = watcher 77 | daemon.streamListeners[key] = [] 78 | return true 79 | 80 | daemon.checkStreams = -> 81 | for key, watcher of daemon.watchers 82 | if watcher.running 83 | # the watchers have responsibility about following: 84 | # - check the stream state, if it's broken, try to reconnect it. 85 | # - check the queue states, to make sure all JSON objects pass to invokers are vaild (@TODO) 86 | watcher.cleanup() 87 | else 88 | watcher.start() 89 | watcher.updateBinding(daemon.streamListeners[key]) 90 | 91 | daemon.run = -> 92 | Logger.info("Daemon runs, tick=#{daemon.tick}") 93 | if daemon.working 94 | daemon.verifySlots(daemon.tick) 95 | daemon.checkPoll(daemon.tick) 96 | daemon.checkStreams() 97 | Hotot.saveBounds() 98 | daemon.tick += daemon.timerInterval/1000 99 | if daemon.tick == 3600 # reset timer per hour 100 | daemon.tick = 0 101 | setTimeout( 102 | () -> 103 | daemon.run() 104 | , daemon.timerInterval 105 | ) 106 | 107 | return daemon 108 | 109 | ]) 110 | -------------------------------------------------------------------------------- /core/scripts/dialog.settings.coffee: -------------------------------------------------------------------------------- 1 | this.app = angular.module('HototSettingsDialog', []) 2 | this.app.controller('SettingsCtrl', ['$scope', ($scope) -> 3 | $scope.pages = 4 | general: 5 | show: true 6 | account: 7 | show: false 8 | firewall: 9 | show: false 10 | $scope.settings = {} 11 | $scope.accounts = [] 12 | 13 | reset = (content) -> 14 | console.log content 15 | $scope.settings = content.settings 16 | for obj, i in $scope.settings.language.list 17 | if $scope.settings.language.value == obj.value 18 | $scope.settings.language.value = $scope.settings.language.list[i] 19 | break 20 | for obj, i in $scope.settings.readlater_services.list 21 | if $scope.settings.readlater_services.value == obj.value 22 | $scope.settings.readlater_services.value = $scope.settings.readlater_services.list[i] 23 | break 24 | $scope.settings.line_height.value *= 10 25 | $scope.settings.line_height.min *= 10 26 | $scope.settings.line_height.max *= 10 27 | $scope.settings.line_height.step *= 10 28 | 29 | $scope.accounts = content.accounts 30 | if $scope.accounts.length != 0 31 | $scope.accounts[0].selected = true 32 | $scope.$apply() 33 | 34 | hotot.bus.onMessage.addListener((request, sender, senderResponse) -> 35 | if not request.cmd 36 | return 37 | if request.cmd == 'reset_settings' 38 | reset(request.content) 39 | ) 40 | 41 | hotot.window.onClosed.addListener(()-> 42 | hotot.bus.sendMessage({role: 'settings_dialog', cmd:'save_settings', content: {serv: _acc.serv, slot_name: _acc.name}}) 43 | ) 44 | 45 | $scope.selectPage = (idx) -> 46 | $scope.pages.general.show = false 47 | $scope.pages.account.show = false 48 | $scope.pages.firewall.show = false 49 | switch idx 50 | when 0 51 | $scope.pages.general.show = true 52 | when 1 53 | $scope.pages.account.show = true 54 | when 2 55 | $scope.pages.firewall.show = true 56 | return 57 | 58 | $scope.selectAccount = (acc) -> 59 | for _acc in $scope.accounts 60 | _acc.selected = false 61 | acc.selected = true 62 | return 63 | 64 | $scope.accountItemCls = (acc) -> 65 | return if acc.selected then 'selected' else '' 66 | 67 | $scope.accountDeleteButtonCls = -> 68 | return if $scope.accounts.length == 0 then 'disabled' else '' 69 | 70 | $scope.addAccount = -> 71 | hotot.bus.sendMessage({role: 'settings_dialog', cmd:'create_account', content: {}}) 72 | return 73 | 74 | $scope.deleteAccount = -> 75 | if $scope.accounts.length == 0 then return 76 | for _acc in $scope.accounts 77 | if _acc.selected == true 78 | hotot.bus.sendMessage({role: 'settings_dialog', cmd:'delete_account', content: {serv: _acc.serv, slot_name: _acc.name}}) 79 | if $scope.accounts.length == 1 80 | window.close() 81 | break 82 | return 83 | 84 | changeSettings = (key) -> 85 | if $scope.settings.hasOwnProperty(key) 86 | if key == 'line_height' 87 | val = $scope.settings.line_height.value / 10.0 88 | else 89 | val = $scope.settings[key].value 90 | hotot.bus.sendMessage({role: 'settings_dialog', cmd:'change_settings', content: {key: key, value: val}}) 91 | 92 | $scope.changeLanguage = -> 93 | changeSettings('language') 94 | return 95 | 96 | $scope.changeFontsize = -> 97 | changeSettings('font_size') 98 | return 99 | 100 | $scope.changeLineheight = -> 101 | changeSettings('line_height') 102 | return 103 | 104 | $scope.changeMediaPreview = -> 105 | changeSettings('preview_media') 106 | return 107 | 108 | $scope.changeReadlaterService = -> 109 | changeSettings('readlater_services') 110 | return 111 | ]) 112 | -------------------------------------------------------------------------------- /core/scripts/lib.oauth1.coffee: -------------------------------------------------------------------------------- 1 | 2 | # A JavaScript implementation for twitter's OAuth(OAuth 1.0) 3 | # Version 1.1 Copyright Shellex Wai<5h3ll3x@gmail.com> 2009 - 2013. 4 | # Distributed under the MIT License 5 | # See http://oauth.net/ for details. 6 | root = exports ? this 7 | 8 | root.app.factory('OAuth1', ['$http', ($http) -> 9 | buildFn = () -> 10 | lib = {} 11 | lib.oauth_base = 'https://api.twitter.com/oauth/' 12 | lib.sign_oauth_base = 'https://api.twitter.com/oauth/' 13 | lib.use_same_sign_oauth_base = false 14 | lib.request_token_url = 'request_token' 15 | lib.access_token_url = 'access_token' 16 | lib.user_auth_url = 'authorize' 17 | lib.key = '' 18 | lib.secret = '' 19 | lib.request_token = null 20 | lib.access_token = null 21 | 22 | lib.timestamp = () -> 23 | t = (new Date()).getTime() 24 | return Math.floor(t / 1000) 25 | 26 | lib.nonce = (length) -> 27 | return Math.random().toString().substring(2) 28 | 29 | lib.form_signed_url = (url, token, method, params) -> 30 | url = url + '?' + lib.form_signed_params(url, token, method, params) 31 | return url 32 | 33 | lib.form_signed_params = (url, token, method, addition_params, use_dict) -> 34 | kwargs = 35 | oauth_consumer_key: lib.key 36 | oauth_signature_method: 'HMAC-SHA1' 37 | oauth_version: '1.0' 38 | oauth_timestamp: lib.timestamp() 39 | oauth_nonce: lib.nonce() 40 | 41 | if addition_params != null 42 | angular.extend(kwargs, addition_params) 43 | 44 | service_key = lib.secret + '&' 45 | if token != null 46 | kwargs['oauth_token'] = token['oauth_token'] 47 | service_key = service_key + Hotot.quote(token['oauth_token_secret']) 48 | 49 | # normalize_params 50 | params = Hotot.normalizeParams(kwargs) 51 | message = Hotot.quote(method) + '&' + Hotot.quote(url) + '&' + Hotot.quote(params) 52 | 53 | # sign 54 | b64pad = '=' 55 | signature = b64_hmac_sha1(service_key, message) 56 | kwargs['oauth_signature'] = signature + b64pad 57 | if use_dict 58 | return kwargs 59 | else 60 | return Hotot.normalizeParams(kwargs) 61 | 62 | lib.get_request_token = (on_success, on_error) -> 63 | sign_base = if lib.use_same_sign_oauth_base then lib.oauth_base else lib.sign_oauth_base 64 | url = lib.oauth_base + lib.request_token_url + '?' + lib.form_signed_params(sign_base + lib.request_token_url, null, 'GET', null) 65 | $http({method: 'GET', url: url}). 66 | success((data, status, headers, config) -> 67 | token_info = data 68 | lib.request_token = Hotot.unserializeDict(token_info) 69 | if on_success then on_success(data) 70 | ). 71 | error((data, status, headers, config) -> 72 | if on_error then on_error(data) 73 | ) 74 | 75 | lib.get_auth_url = () -> 76 | return lib.oauth_base + lib.user_auth_url + '?oauth_token' + '=' + lib.request_token['oauth_token'] 77 | 78 | lib.get_access_token = (pin, on_success, on_error) -> 79 | if lib.request_token == {} 80 | return 81 | sign_base = if lib.use_same_sign_oauth_base then lib.oauth_base else lib.sign_oauth_base 82 | addition_params = { 83 | 'oauth_verifier': pin 84 | } 85 | params = lib.form_signed_params(sign_base + lib.access_token_url, lib.request_token, 'GET', addition_params) 86 | $http({method: 'GET', url: lib.oauth_base + lib.access_token_url + '?' + params}). 87 | success((data, status, headers, config) -> 88 | token_info = data 89 | lib.access_token = Hotot.unserializeDict(token_info) 90 | if on_success then on_success(data) 91 | ). 92 | error((data, status, headers, config) -> 93 | if on_error then on_error(data) 94 | ) 95 | return 96 | return lib 97 | return buildFn 98 | ]) 99 | 100 | -------------------------------------------------------------------------------- /core/pin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Twitter / Authorize an application 8 | 9 | 10 | 13 | 14 | 15 | 16 | 19 | 24 | 29 | 30 | 31 | 32 | 62 | 63 |
64 | 65 |

You've granted access to Hotot for Chrome!

66 | 67 |
68 |

69 | Next, return to Hotot for Chrome and enter this PIN to complete the authorization process: 70 | 0608448 71 |

72 |
73 | 74 | 75 | 76 |
77 | 78 | 79 | 80 | 81 | 91 | 92 | 93 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /core/scripts/ctrl.nav.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | root.app.controller('NavigatorCtrl', ['$scope', 'SliderService', 'AppService', 'HototSlot', 'HototColumn', 'Logger', 3 | ($scope, SliderService, AppService, HototSlot, HototColumn, Logger) -> 4 | $scope.columnTable = SliderService.columnTable 5 | $scope.slotList = SliderService.slotList 6 | 7 | $scope.$on('AppResize', () -> 8 | resize(AppService.mainWidth, AppService.maxColNum) 9 | $scope.$apply() 10 | ) 11 | 12 | $scope.$on('SliderUpdate', () -> 13 | $scope.$digest() 14 | ) 15 | 16 | $scope.$on('SliderChangeSlot', () -> 17 | changeSlot(SliderService.currentSlot) 18 | ) 19 | 20 | $scope.$on('SliderMoveToColumn', () -> 21 | slideTo(SliderService.currentColIdx) 22 | ) 23 | 24 | $scope.navigate = (index, col) -> 25 | slideTo(index) 26 | return 27 | 28 | $scope.toTop = (col) -> 29 | SliderService.broadcast('move_to_top', col) 30 | return 31 | 32 | $scope.getVisibilityState = (slot) -> 33 | return if slot.visibility then '' else 'folding' 34 | 35 | changeSlot = (newSlot) -> 36 | for slotWrapper, idx in $scope.slotList 37 | slot = slotWrapper.slot 38 | if slot.name == newSlot.name and slot.serv == newSlot.serv 39 | SliderService.curSlotIdx = idx 40 | slotWrapper.visibility = true 41 | else 42 | slotWrapper.visibility = false 43 | Logger.info("Change Slot #{SliderService.curSlotIdx}") 44 | SliderService.sync() 45 | 46 | resize = (mainWidth, maxColNum) -> 47 | slotColNum = SliderService.getSlotColumnNum() 48 | if slotColNum < maxColNum 49 | maxColNum = slotColNum 50 | if maxColNum > 0 51 | # update active pages' style 52 | idx = $scope.columnTable[$scope.slotList[SliderService.curSlotIdx].key].currentIdx 53 | SliderService.updateActiveOffset(idx, maxColNum) 54 | SliderService.updateActiveStatus(maxColNum) 55 | # use timeout to delay... 56 | setTimeout(() -> 57 | $scope.$apply(() -> 58 | SliderService.sync() 59 | ) 60 | , 100 61 | ) 62 | 63 | 64 | slideTo = (idx, col) => 65 | #* = 3 columns as example = 66 | #* idx: 0 1 2 3 4 5 67 | #* fixed_idx: 0 1 2 3 3 3 68 | #* active_page_offset: 0 0 1 2 3 3 69 | #* active_pages: 012 012 123 234 345 335 70 | #*/ 71 | maxColNum = AppService.maxColNum 72 | slotColNum = SliderService.getSlotColumnNum() 73 | if slotColNum < maxColNum 74 | maxColNum = slotColNum 75 | if idx == -1 76 | idx = 0 77 | if $scope.slotList.length != 0 78 | SliderService.curSlotIdx = if SliderService.curSlotIdx == -1 then 0 else SliderService.curSlotIdx 79 | $scope.columnTable[$scope.slotList[SliderService.curSlotIdx].key].currentIdx = idx 80 | SliderService.updateActiveOffset(idx, maxColNum) 81 | # update active pages 82 | SliderService.updateActiveStatus(maxColNum) 83 | col = $scope.columnTable[$scope.slotList[SliderService.curSlotIdx].key].columns[idx] 84 | setTimeout(() -> 85 | SliderService.selectItem(SliderService.currentSlot.serv, SliderService.currentSlot.name, col.name) 86 | , 500 87 | ) 88 | return 89 | 90 | $scope.getNavStyles = -> 91 | # if SliderService.currentSlot 92 | # len = SliderService.currentSlot.columns.length * 32 93 | # return {'width': len + 'px', 'margin-left': -1*Math.round(len/2) + 'px'} 94 | return {} 95 | 96 | $scope.getNavMaskStyles = -> 97 | # slotObj = SliderService.currentSlotObject() 98 | # if slotObj and $scope.columnTable[slotObj.key] 99 | # left = $scope.columnTable[slotObj.key].activeOffset * 32 100 | # len = AppService.maxColNum * 32 101 | # return {'width': len + 'px', '-webkit-transform': 'translateX('+left + 'px)'} 102 | return {} 103 | 104 | $scope.getNavMaskIconCls = -> 105 | slotObj = SliderService.currentSlotObject() 106 | if slotObj 107 | obj = $scope.columnTable[slotObj.key] 108 | return obj.columns[obj.activeOffset].icon_name 109 | return "" 110 | 111 | 112 | return 113 | ]) 114 | -------------------------------------------------------------------------------- /core/dialogs/columns.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Settings 15 | 16 | 17 |
18 |
19 | 22 | 23 | 24 |
25 |
26 | 37 |
38 |

39 | {{selectedColumn.display_name}} 40 |

41 |
42 | 44 | 46 |
47 |
48 |

Params

49 |
{{pa}}
50 |
51 |

No parameter.

52 |
53 |
54 |
55 |

Column mute

56 | 57 |
58 |
59 |

Global mute

60 | 61 |
62 |
63 |
64 |
65 |
    66 |
  • 67 | 70 | 71 |
  • 72 |
73 |
    74 |
  • 75 | 76 | 77 |
  • 78 |
  • 79 |

    No parameter needed.

    80 |
  • 81 |
82 |

83 | 84 | 85 |

86 |
87 |
88 | 89 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {exec} = require 'child_process' 2 | # exec = require "exec-sync" 3 | fs = require 'fs' 4 | 5 | DIST_BASE = "dist" 6 | SRC_BASE = "core" 7 | 8 | COFFEE_SRC = "#{SRC_BASE}/scripts" 9 | LESS_SRC = "#{SRC_BASE}/styles" 10 | 11 | JS_DIST = "#{DIST_BASE}/scripts" 12 | CSS_DIST = "#{DIST_BASE}/styles" 13 | MEDIA_LIST = ["icons", "images", "dialogs/*", "partials/*", "manifest.json", "index.html", "sandbox.html"] 14 | 15 | lastChange = {} 16 | 17 | copyFile = (src, dist) -> 18 | console.log "copy #{src} to #{dist}" 19 | exec("cp -a #{src} #{dist}") 20 | 21 | handleMedia = (fn) -> 22 | for item in MEDIA_LIST 23 | if item[item.length - 1] == '*' # files in the dir 24 | item = item.substring(0, item.length - 1) 25 | files = fs.readdirSync("#{SRC_BASE}/#{item}") 26 | for file in files 27 | fullpath = "#{SRC_BASE}/#{item}/#{file}" 28 | # st = fs.statSync(fullpath) 29 | # if st.isDirectory() # ignore directory 30 | # continue 31 | fn(fullpath, "#{DIST_BASE}/#{item}/#{file}") 32 | else 33 | fn("#{SRC_BASE}/#{item}", "#{DIST_BASE}/") 34 | 35 | 36 | handleCoffeeFiles = (fn)-> 37 | if not fs.existsSync(JS_DIST) 38 | fs.mkdirSync(JS_DIST) 39 | fs.readdir(COFFEE_SRC, (err, files) -> 40 | for file in files 41 | file = "#{COFFEE_SRC}/#{file}" 42 | fn file 43 | ) 44 | 45 | handleLessFiles = (fn)-> 46 | if not fs.existsSync(CSS_DIST) 47 | fs.mkdirSync(CSS_DIST) 48 | fs.readdir(LESS_SRC, (err, files) -> 49 | for file in files 50 | file = "#{LESS_SRC}/#{file}" 51 | fn file 52 | ) 53 | 54 | compileCoffee = (file) => 55 | if file.lastIndexOf('.coffee') == file.length - 1 - 'coffee'.length 56 | console.log "Compile coffee", file 57 | exec "coffee -c -o #{JS_DIST} #{file}", (err, stdout, stderr) -> 58 | return console.error err if err 59 | console.log "Compiled #{file}" 60 | else 61 | _file = file.split('/') 62 | _file = _file[_file.length - 1] 63 | console.log "copy only", file 64 | copyFile(file, "#{JS_DIST}/#{_file}") 65 | 66 | compileLess = (file) => 67 | _file = file.split('/') 68 | _file = _file[_file.length - 1] 69 | if file.lastIndexOf('.less') == file.length - 1 - 'less'.length 70 | console.log "Compile less", file 71 | exec "lessc #{file} #{CSS_DIST}/#{_file.replace('.less', '.css')}", (err, stdout, stderr) -> 72 | return console.error err if err 73 | console.log "Compiled #{file}" 74 | else 75 | console.log "copy only", file 76 | copyFile(file, "#{CSS_DIST}/#{_file}") 77 | 78 | watchFile = (file, fn) -> 79 | lastChange[file] = 0 80 | try 81 | fs.watch file, (event, filename) -> 82 | return if event isnt 'change' 83 | # ignore repeated event misfires 84 | fn file if Date.now() - lastChange[file] > 1000 85 | lastChange[file] = Date.now() 86 | catch e 87 | console.log "Error watching #{file}" 88 | 89 | watchCoffee = (file) -> 90 | watchFile file, compileCoffee 91 | 92 | watchLess = (file) -> 93 | watchFile file, compileLess 94 | 95 | watchMedia = (src, dist) -> 96 | lastChange[src] = 0 97 | try 98 | fs.watch src, (event, filename) => 99 | return if event isnt 'change' 100 | # ignore repeated event misfires 101 | copyFile src, dist if Date.now() - lastChange[src] > 1000 102 | lastChange[src] = Date.now() 103 | catch e 104 | console.log "Error watching #{src}", e 105 | 106 | build = -> 107 | if not fs.existsSync(DIST_BASE) 108 | fs.mkdirSync(DIST_BASE) 109 | handleMedia(copyFile) 110 | handleCoffeeFiles(compileCoffee) 111 | handleLessFiles(compileLess) 112 | 113 | compressScripts = -> 114 | fs.readdir(JS_DIST, (err, files) -> 115 | for file in files 116 | file = "#{JS_DIST}/#{file}" 117 | exec("uglifyjs -o #{file} #{file}") 118 | ) 119 | 120 | compressStyles = -> 121 | fs.readdir(CSS_DIST, (err, files) -> 122 | for file in files 123 | file = "#{CSS_DIST}/#{file}" 124 | exec("cleancss -o #{file} #{file}") 125 | ) 126 | 127 | task 'sbuild', '', -> 128 | build() 129 | 130 | task 'watch', 'Compile + watch *.coffee and *.less', -> 131 | handleCoffeeFiles watchCoffee 132 | handleLessFiles watchLess 133 | handleMedia watchMedia 134 | 135 | task 'compress', 'Compress scripts and css files', -> 136 | compressScripts() 137 | compressStyles() 138 | 139 | -------------------------------------------------------------------------------- /core/dialogs/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Settings 13 | 14 | 15 |
16 | 27 |
28 |
29 |
    30 |
  • 31 | 33 | 34 |
  • 35 |
36 |
    37 |
  • 38 | 39 | 40 | 41 |
  • 42 |
  • 43 | 44 | 45 | 46 |
  • 47 |
48 |
    49 |
  • 50 | 51 |
    52 |
  • 53 |
  • 54 | 55 | 56 |
  • 57 |
58 |
59 | 75 |
76 |
77 | 78 | -------------------------------------------------------------------------------- /core/scripts/dialog.profile.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module('HototProfileDialog', []) 2 | bindDriective(app, ['DND', 'MISC']) 3 | app.controller('ProfileCtrl', ['$scope', ($scope) -> 4 | $scope.slots = [] 5 | $scope.currentSlot = null 6 | $scope.newAvatar = {} 7 | majorAccount = null 8 | notSetBg = true 9 | master = {} 10 | $scope.uploadButton = 11 | label: 'Upload' 12 | disabled: false 13 | $scope.saveButton = 14 | label: 'Save Changes' 15 | 16 | hotot.bus.onMessage.addListener((request, sender, senderResponse) => 17 | if not request.cmd 18 | return 19 | if request.cmd == 'reset_profile' 20 | content = request.content 21 | majorAccount = content.major_account 22 | $scope.$apply(() -> 23 | $scope.slots = content.accounts 24 | $scope.user = majorAccount.user 25 | ) 26 | for slot, i in $scope.slots 27 | if slot.name == majorAccount.name and slot.serv = majorAccount.serv 28 | $scope.$apply(() -> 29 | $scope.currentSlot = slot 30 | $scope.selectSlot(slot, i) 31 | ) 32 | break 33 | ) 34 | 35 | $scope.selectSlot = (slot, index) -> 36 | master = {} 37 | angular.extend(master, slot.profile) 38 | return 39 | 40 | $scope.updateProfile = () -> 41 | handleResponse = (resp) -> 42 | if resp.result == 'ok' 43 | window.close() 44 | else 45 | console.log "Error, reason: #{resp.reason}" 46 | $scope.saveButton.disabled = false 47 | $scope.saveButton.label = 'Save Changes' 48 | 49 | $scope.saveButton.disabled = true 50 | $scope.saveButton.label = 'Saving...' 51 | hotot.bus.sendMessage( 52 | { 53 | 'role': 'profile_dialog', 54 | 'cmd': "update_profile", 55 | 'content': {account: $scope.currentSlot, profile: $scope.currentSlot.profile} 56 | }, (resp) -> 57 | $scope.$apply(()-> 58 | handleResponse(resp) 59 | ) 60 | ) 61 | return 62 | 63 | $scope.isUnchanged = (profile) -> 64 | return angular.equals(profile, master) and not $scope.saveButton.disabled 65 | 66 | loadFile = (file) -> 67 | # if not Hotot.imageTest(file.name) 68 | # console.log "not a image file" 69 | # return false 70 | $scope.newAvatar.filename = file.name 71 | $scope.newAvatar.type = file.type 72 | $scope.newAvatar.size = file.size 73 | reader = new FileReader() 74 | reader.onerror = (err) -> 75 | console.log "failed to load file", err 76 | reader.onloadend = (e) -> 77 | $scope.$apply(() -> 78 | $scope.newAvatar.base64_data = e.target.result 79 | $scope.newAvatar.base64_data_bk = e.target.result 80 | uploadAvatar() 81 | ) 82 | reader.readAsDataURL(file) 83 | return false 84 | 85 | uploadAvatar = () -> 86 | current = $scope.currentSlot 87 | handleResponse = (resp) -> 88 | if resp.result == 'ok' 89 | current.profile = resp.profile 90 | Hotot.fetchImage(current.profile.avatar_url, (data) -> 91 | $scope.$apply(()-> 92 | current.avatar = window.webkitURL.createObjectURL(data) 93 | ) 94 | ) 95 | else 96 | console.log "Error, reason: #{resp.reason}" 97 | $scope.uploadButton.disabled = false 98 | $scope.uploadButton.label = 'upload' 99 | 100 | $scope.uploadButton.disabled = true 101 | $scope.uploadButton.label = 'uploading...' 102 | hotot.bus.sendMessage( 103 | { 104 | 'role': 'profile_dialog', 105 | 'cmd': "update_avatar", 106 | 'content': {account: current, avatar: $scope.newAvatar} 107 | }, (resp) -> 108 | $scope.$apply(()-> 109 | handleResponse(resp) 110 | ) 111 | ) 112 | return 113 | 114 | $scope.updateAvatar = (evt) -> 115 | hotot.fs.chooseEntry({type: 'openFile'}, (fileEntry) -> 116 | if fileEntry 117 | fileEntry.file((file) -> loadFile(file)) 118 | ) 119 | return 120 | 121 | $scope.fetchAvatar = () -> 122 | if $scope.currentSlot 123 | Hotot.fetchImage($scope.currentSlot.profile.avatar_url, (data) -> 124 | $scope.currentSlot.avatar = window.webkitURL.createObjectURL(data) 125 | ) 126 | 127 | ]) 128 | this.app = app 129 | -------------------------------------------------------------------------------- /core/dialogs/message.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Message 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | 25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 | 35 | 36 |
37 |
38 |
39 |
40 |
41 |
    42 |
  • 43 |
    44 | 45 |
    46 |
    47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
  • 54 |
55 |
56 |
57 |
58 |
59 | 60 |
{{props.remainCharNum}}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
    72 |
  • 73 | 74 | 75 | 79 | 80 | 81 |
  • 82 |
83 |
    84 |
  • 85 | 86 | 87 |
  • 88 |
89 |

90 | 91 | 92 |

93 |
94 |
95 | 96 | -------------------------------------------------------------------------------- /core/styles/dialog.message.less: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | } 4 | .enable_select { 5 | -webkit-touch-callout: text; 6 | -webkit-user-select: text; 7 | user-select: text; 8 | } 9 | #container { 10 | height: 100%; 11 | width: 100%; 12 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #e6e6e8)); 13 | overflow: hidden; 14 | display: -webkit-flex; 15 | -webkit-flex-direction: column; 16 | .top { 17 | -webkit-flex: 0 0 28px; 18 | padding: 2px 6px; 19 | -webkit-app-region: drag; 20 | box-shadow: 0 0 10px rgba(0,0,0,0.1); 21 | position: fixed; 22 | right: 0; 23 | left: 0; 24 | z-index: 100; 25 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, rgba(255,255,255,0.9))); 26 | } 27 | .add { 28 | float: right; 29 | height: 28px; 30 | line-height: 26px; 31 | font-size: 12px; 32 | } 33 | } 34 | .body { 35 | -webkit-flex: 1 0 auto; 36 | -webkit-order: 1; 37 | .side { 38 | width: 160px; 39 | height: 340px; 40 | padding-top: 32px; 41 | .people_name { 42 | font-size: 12px; 43 | } 44 | } 45 | .tabs_body { 46 | width: 400px; 47 | height: 340px; 48 | margin-left: 162px; 49 | font-size: 14px; 50 | .messages { 51 | overflow: auto; 52 | border-bottom: 1px #ccc solid; 53 | position: absolute; 54 | left: 160px; 55 | right: 0px; 56 | top: 0px; 57 | bottom: 38px; 58 | .messages_inner { 59 | padding: 32px 10px 0 5px; 60 | } 61 | .message { 62 | min-height: 32px; 63 | position: relative; 64 | margin: 8px 0; 65 | .avatar { 66 | position: absolute; 67 | left: 0; 68 | bottom: 0; 69 | width: 32px; 70 | height: 32px; 71 | img { 72 | width: 32px; 73 | height: 32px; 74 | } 75 | } 76 | .message_body { 77 | margin-left: 40px; 78 | bottom: 0; 79 | border-radius: 6px; 80 | max-width: 80%; 81 | display: inline-block; 82 | .message_text { 83 | padding: 5px 8px; 84 | font-size: 12px; 85 | text-shadow: 0 1px 1px white; 86 | min-height: 22px; 87 | line-height: 22px; 88 | } 89 | .message_caret { 90 | top: auto; 91 | bottom: 4px; 92 | } 93 | } 94 | } 95 | .message.right { 96 | text-align: right; 97 | .avatar { 98 | left: auto; 99 | right: 0; 100 | } 101 | .message_body { 102 | margin-right: 40px; 103 | .message_text { 104 | text-align: right; 105 | } 106 | } 107 | } 108 | } 109 | .control { 110 | height: 24px; 111 | padding: 4px 10px 10px 5px; 112 | position: absolute; 113 | right: 0; 114 | bottom: 0; 115 | left: 160px; 116 | .text_wrapper { 117 | background: transparent; 118 | padding: 4px 0px; 119 | margin: 0 6px; 120 | .text_box { 121 | background: transparent; 122 | border: 0; 123 | outline: none; 124 | margin: 0; 125 | width: 100%; 126 | padding: 0; 127 | height: 18px; 128 | resize: none; 129 | font-size: 13px; 130 | } 131 | .char_counter { 132 | position: absolute; 133 | width: 3em; 134 | bottom: 2px; 135 | right: 3px; 136 | text-align: right; 137 | font-size: 12px; 138 | color: #d0d4da; 139 | } 140 | .char_counter.red { 141 | color: #E44; 142 | } 143 | } 144 | } 145 | } 146 | } 147 | .add_con_box { 148 | .widget { 149 | width: 400px; 150 | } 151 | .textbox_wrapper { 152 | height: 100px; 153 | textarea { 154 | height: 100px; 155 | } 156 | } 157 | } 158 | @-webkit-keyframes spin { 159 | from { 160 | -webkit-transform: rotate(0deg); 161 | } 162 | to { 163 | -webkit-transform: rotate(360deg); 164 | } 165 | } -------------------------------------------------------------------------------- /core/scripts/serv.settings.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('SettingsService', ['$rootScope', 'Logger', 'HototSettings', 'HototForbiddens', 'HototMute', 4 | ($rootScope, Logger, HototSettings, HototForbiddens, HototMute) -> 5 | SettingsService = {} 6 | SettingsService.settings = {} 7 | 8 | SettingsService.broadcast = (cmd, value) -> 9 | this.cmd = cmd 10 | console.info('Settings Service deliver a message', this.cmd) 11 | switch cmd 12 | when 'update_mute' 13 | this.updateMuteParams = value 14 | $rootScope.$broadcast('SettingsUpdateMute') 15 | return 16 | 17 | SettingsService.reset = -> 18 | SettingsService.settings = { 19 | language: { label: 'Language', type: 'combo_list', value: 'auto', value_type: 'String', list: [ 20 | {value: 'auto', text: 'Auto'}, 21 | {value: 'en', text: 'English'}, 22 | {value: 'zh_CN', text: '简体中文'}, 23 | ] 24 | }, 25 | font_size: {label: 'Font Size', type: 'range', value: 9, value_type: 'Number', max: 16, min: 6, step: 0.5}, 26 | line_height: {label: 'Line Height', type: 'range', value: 1.4, value_type: 'Number', max: 2.0, min: 1.0, step: 0.1}, 27 | accounts: [], 28 | preview_media: {label: 'Preview Embedded Media', type: 'checkbox', value: true, value_type: 'Boolean'}, 29 | readlater_services: { label: 'Read Later Services', type: 'combo_list', value: 'none', value_type: 'String', list: [ 30 | {value: 'pocket', text: 'Pocket'}, 31 | {value: 'instapaper', text: 'Instapaper'}, 32 | {value: 'none', text: 'None'}, 33 | ] 34 | }, 35 | } 36 | SettingsService.forbiddens = { 37 | theme: {value:'auto', default:'auto'} 38 | } 39 | SettingsService.mute = [] 40 | 41 | SettingsService.setForbiddens = (key, value) -> 42 | if this.forbiddens.hasOwnProperty(key) 43 | if value == null 44 | this.forbiddens[key].value = this.forbiddens[key].default 45 | else 46 | this.forbiddens[key].value = value 47 | HototForbiddens.dumps(SettingsService.forbiddens) 48 | return value 49 | 50 | SettingsService.getForbiddens = (key) -> 51 | if this.forbiddens.hasOwnProperty(key) 52 | return this.forbiddens[key].value 53 | return null 54 | 55 | SettingsService.setMute = (mute) -> 56 | HototMute.dumps(mute, () -> 57 | SettingsService.mute = mute 58 | SettingsService.broadcast('update_mute', {mute: SettingsService.mute}) 59 | ) 60 | 61 | SettingsService.getMute = () -> 62 | return SettingsService.mute 63 | 64 | SettingsService.init = -> 65 | SettingsService.reset() 66 | SettingsService.loads() 67 | Logger.info("init Settings Service") 68 | hotot.bus.onMessage.addListener((message, sender, respond) -> 69 | # console.log('control', ev) 70 | cmd = message.cmd 71 | content = message.content 72 | switch cmd 73 | when 'change_settings' 74 | if SettingsService.settings.hasOwnProperty(content.key) 75 | switch SettingsService.settings[content.key].value_type 76 | when 'Number' 77 | val = Number(content.value) 78 | when 'Boolean' 79 | val = Boolean(content.value) 80 | else 81 | val = content.value 82 | SettingsService.settings[content.key].value = val 83 | when 'load_settings' 84 | SettingsService.loads() 85 | when 'save_settings' 86 | SettingsService.dumps() 87 | return true 88 | ) 89 | SettingsService.autoDumps() 90 | 91 | SettingsService.loads = -> 92 | HototForbiddens.loads((_forbiddens) -> 93 | if _forbiddens 94 | for key, value of _forbiddens 95 | if SettingsService.forbiddens.hasOwnProperty(key) 96 | SettingsService.forbiddens[key] = value 97 | ) 98 | HototSettings.loads((_settings) -> 99 | if _settings 100 | for field, fv of _settings 101 | if SettingsService.settings.hasOwnProperty(field) 102 | tmpl = SettingsService.settings[field] 103 | if fv.value and fv.value.constructor.name == tmpl.value_type 104 | SettingsService.settings[field].value = fv.value 105 | ) 106 | HototMute.loads((_mute) -> 107 | if _mute 108 | SettingsService.mute = _mute 109 | ) 110 | return 111 | 112 | SettingsService.autoDumps = -> 113 | setInterval(SettingsService.dumps, 60000) 114 | 115 | SettingsService.dumps = -> 116 | HototSettings.dumps(SettingsService.settings) 117 | HototForbiddens.dumps(SettingsService.forbiddens) 118 | HototMute.dumps(SettingsService.mute) 119 | return 120 | 121 | return SettingsService 122 | ]) 123 | -------------------------------------------------------------------------------- /core/scripts/serv.relation.coffee: -------------------------------------------------------------------------------- 1 | root = exports ? this 2 | 3 | root.app.factory('RelationService', ['$rootScope', 'HototRelationships', 'Logger', ($rootScope, HototRelationships, Logger) -> 4 | RelationService = {} 5 | RelationService.relation = {} 6 | RelationService.init = -> 7 | RelationService.loads() 8 | Logger.info("init Relationships Service") 9 | hotot.bus.onMessage.addListener((message, sender, respond) -> 10 | # console.log('control', ev) 11 | cmd = message.cmd 12 | switch cmd 13 | when 'load_relationships' 14 | RelationService.loads() 15 | when 'save_relationships' 16 | RelationService.dumps() 17 | return true 18 | ) 19 | RelationService.autoDumps() 20 | 21 | RelationService.bind = (serv, slotname) -> 22 | key = "#{serv}/#{slotname}" 23 | if not this.relation.hasOwnProperty(key) 24 | this.relation[key] = 25 | following_ids: {} 26 | users: {} 27 | idIndex: {} 28 | return this.relation[key] 29 | 30 | RelationService.unbind = (serv, slotname) -> 31 | key = "#{serv}/#{slotname}" 32 | if this.relation.hasOwnProperty(key) 33 | delete this.relation[key] 34 | return 35 | 36 | RelationService.add = (serv, slotname, users) -> 37 | key = "#{serv}/#{slotname}" 38 | if this.relation.hasOwnProperty(key) 39 | for user in users 40 | username = user.name.toLowerCase() 41 | this.relation[key].users[username] = user 42 | this.relation[key].users[username].createTime = Date.now() 43 | this.relation[key].idIndex[user.id] = this.relation[key].users[username] 44 | if this.relation[key].following_ids.hasOwnProperty(user.id) 45 | this.relation[key].users[username].following = true 46 | return 47 | 48 | RelationService.updateFollowingIds = (serv, slotname, ids) -> 49 | key = "#{serv}/#{slotname}" 50 | if this.relation.hasOwnProperty(key) 51 | for id in ids 52 | if id.constructor == Number 53 | id = id.toString() 54 | if RelationService.relation[key].idIndex.hasOwnProperty(id) 55 | RelationService.relation[key].idIndex[id].following = true 56 | this.relation[key].following_ids[id] = null 57 | 58 | RelationService.isFollowing = (serv, slotname, user) -> 59 | key = "#{serv}/#{slotname}" 60 | if this.relation.hasOwnProperty(key) 61 | if this.relation[key].following_ids.hasOwnProperty(user.id) 62 | return true 63 | return false 64 | 65 | RelationService.setRelationship = (serv, slotname, name, rel) -> 66 | key = "#{serv}/#{slotname}" 67 | if this.relation.hasOwnProperty(key) 68 | name = name.toLowerCase() 69 | if this.relation[key].users.hasOwnProperty(name) 70 | return this.relation[key].users[name].relationship = rel 71 | return null 72 | 73 | RelationService.find = (serv, slotname, piece) -> 74 | result = [] 75 | filterProc = (name, piece) -> name.toLowerCase().indexOf(piece.toLowerCase()) != -1 76 | process = (key) -> 77 | rel = RelationService.relation[key] 78 | for name, user of rel.users 79 | if filterProc(name, piece) 80 | result.push(user) 81 | if slotname == '*' 82 | for key, rel of this.relation 83 | process(key) 84 | else 85 | key = "#{serv}/#{slotname}" 86 | if this.relation.hasOwnProperty(key) 87 | process(key) 88 | return result 89 | 90 | RelationService.getByName = (serv, slotname, name) -> 91 | key = "#{serv}/#{slotname}" 92 | if this.relation.hasOwnProperty(key) 93 | name = name.toLowerCase() 94 | if this.relation[key].users.hasOwnProperty(name) 95 | return this.relation[key].users[name] 96 | return null 97 | 98 | RelationService.getById = (serv, slotname, id) -> 99 | key = "#{serv}/#{slotname}" 100 | if this.relation.hasOwnProperty(key) 101 | if this.relation[key].idIndex.hasOwnProperty(id) 102 | return this.relation[key].idIndex[id] 103 | return null 104 | 105 | RelationService.autoDumps = -> 106 | setInterval(RelationService.dumps, 60000) 107 | 108 | RelationService.dumps = -> 109 | relation = {} 110 | for key, rel of RelationService.relation 111 | relation[key] = { 112 | idIndex: {} 113 | following_ids: {} 114 | users: rel.users 115 | } 116 | HototRelationships.dumps(relation) 117 | return 118 | 119 | RelationService.loads = -> 120 | HototRelationships.loads((rels) -> 121 | a = [] 122 | for key, rel of rels 123 | for k, v of rel.users 124 | a.push(rel.users[k]) 125 | Logger.info("load relationships: #{key}, count=#{a.length}") 126 | RelationService.relation[key] = rel 127 | ) 128 | return 129 | 130 | return RelationService 131 | ]) 132 | -------------------------------------------------------------------------------- /core/styles/dialog.columns.less: -------------------------------------------------------------------------------- 1 | html,body { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | #container { 7 | height: 100%; 8 | width: 100%; 9 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #e6e6e8)); 10 | overflow: auto; 11 | display: -webkit-flex; 12 | -webkit-flex-direction: column; 13 | .small_button { 14 | min-width: 0; 15 | padding: 0px; 16 | width: 25px; 17 | line-height: 0px; 18 | height: 25px; 19 | text-align: center; 20 | margin: 2px; 21 | } 22 | .top { 23 | -webkit-flex: 0 0 28px; 24 | -webkit-app-region: drag; 25 | padding: 2px 6px; 26 | box-shadow: 0 0 10px #ddd; 27 | } 28 | .add { 29 | float: right; 30 | } 31 | } 32 | .body { 33 | -webkit-flex: 1 0 auto; 34 | -webkit-order: 1; 35 | .column_list { 36 | background: transparent url(../images/grid_cell@2x.png); 37 | background-size: 40px; 38 | padding: 5px 10px; 39 | .columns { 40 | display: -webkit-flex; 41 | -webkit-flex-direction: row; 42 | .column { 43 | -webkit-flex: 1 0 auto; 44 | -webkit-order: 1; 45 | height: 64px; 46 | display: block; 47 | padding: 10px; 48 | margin: 4px; 49 | border-radius: 6px; 50 | position: relative; 51 | .column_inner { 52 | display: block; 53 | text-align: center; 54 | box-shadow: 0px 1px 3px #aaa; 55 | border-radius: 6px; 56 | transition: all 0.2s ease; 57 | cursor: pointer; 58 | background: white; 59 | height: 64px; 60 | } 61 | .icon { 62 | opacity: 0.3; 63 | margin-top: 16px; 64 | } 65 | .caret_frame { 66 | position: absolute; 67 | bottom: -10px; 68 | left: 50%; 69 | margin-left: -6px; 70 | border-left: 8px transparent solid; 71 | border-right: 8px transparent solid; 72 | border-bottom: 6px #ddd solid; 73 | display: block; 74 | content: " "; 75 | height: 0; 76 | width: 0; 77 | display: none; 78 | } 79 | .caret { 80 | position: absolute; 81 | bottom: -11px; 82 | left: 50%; 83 | margin-left: -6px; 84 | border-left: 8px transparent solid; 85 | border-right: 8px transparent solid; 86 | border-bottom: 5px #f2f2f2 solid; 87 | display: block; 88 | content: " "; 89 | height: 0; 90 | width: 0; 91 | display: none; 92 | } 93 | } 94 | .column.selected { 95 | background: #f2f2f2; 96 | box-shadow: inset 0px 1px 3px #aaa; 97 | .caret_frame { 98 | display: block; 99 | } 100 | .caret { 101 | display: block; 102 | } 103 | } 104 | .column.selected:before { 105 | border-color: #29f; 106 | } 107 | } 108 | } 109 | .column_info { 110 | border-top: #ddd 1px solid; 111 | padding: 2px 10px; 112 | clear: both; 113 | .column_title { 114 | float: left; 115 | height: 44px; 116 | line-height: 60px; 117 | font-size: 22px; 118 | text-shadow: 0 1px 1px #fff; 119 | padding-bottom: 10px; 120 | } 121 | h3 { 122 | font-size: 13px; 123 | padding: 6px 0 3px 0; 124 | text-shadow: 0 1px 1px #fff; 125 | } 126 | .column_ctrl { 127 | height: 30px; 128 | margin-top: 16px; 129 | button, select { 130 | float: right; 131 | } 132 | } 133 | .column_desc { 134 | clear: both; 135 | height: 80px; 136 | border-top: 1px #ccc solid; 137 | font-size: 12px; 138 | .param { 139 | background: gray; 140 | color: white; 141 | padding: 2px 4px 2px 4px; 142 | border-radius: 2px; 143 | vertical-align: text-top; 144 | margin: 4px; 145 | display: inline-block; 146 | } 147 | } 148 | } 149 | .column_info { 150 | .column_mute { 151 | input { 152 | width: 100%; 153 | font-size: 12px; 154 | } 155 | } 156 | } 157 | } 158 | .hint { 159 | text-align: center; 160 | padding: 4px 30px; 161 | font-size: 14px; 162 | border-radius: 4px; 163 | p { 164 | padding: 7px; 165 | color: #aaa; 166 | } 167 | } 168 | .add_column_box { 169 | .ctrl { 170 | margin-top: 10px; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /core/scripts/dialog.preview.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module('HototPreviewDialog', []) 2 | bindDriective(app, ['MISC']) 3 | app.controller('PreviewCtrl', ['$scope', ($scope) -> 4 | $scope.slider = 5 | show: true 6 | offset: -25 7 | $scope.previewer = 8 | data: '' 9 | title: '' 10 | scale: 0.92 11 | show: false 12 | placeholder_show: true 13 | $scope.media = [] 14 | currentIndex = 0 15 | holding = false 16 | win = null 17 | offsetY = 0 18 | offsetX= 0 19 | win = hotot.window.current() 20 | pic = null 21 | 22 | hotot.bus.onMessage.addListener((request, sender, senderResponse) -> 23 | if request.recipient != win.id 24 | return 25 | if request.cmd == 'reset_preview' 26 | reset(request.content.opts, request.content.media) 27 | pic = document.querySelector('.previewer img') 28 | ) 29 | 30 | loadFirstMedia = () -> 31 | if $scope.media.length != 0 32 | Hotot.fetchImage($scope.media[0].url, (data) -> 33 | $scope.$apply(() -> 34 | $scope.previewer.data = window.webkitURL.createObjectURL(data) 35 | ) 36 | ) 37 | 38 | loadSlider = () -> 39 | loadThumb = (m) -> 40 | Hotot.fetchImage(m.thumb_url, (data) -> 41 | $scope.$apply(() -> 42 | m.thumb_data = window.webkitURL.createObjectURL(data) 43 | ) 44 | ) 45 | loadPhoto = (m) -> 46 | Hotot.fetchImage(m.url, (data) -> 47 | $scope.$apply(() -> 48 | m.data = window.webkitURL.createObjectURL(data) 49 | ) 50 | ) 51 | for m, i in $scope.media 52 | if not m.thumb_data 53 | loadThumb(m) 54 | if not m.data 55 | loadPhoto(m) 56 | if $scope.media.length < 2 57 | $scope.slider.show = false 58 | 59 | setCurrent = (index) -> 60 | if index > $scope.media.length - 1 61 | index = $scope.media.length - 1 62 | if index < 0 63 | index = 0 64 | $scope.slider.offset = - (index * 50) - 25 65 | currentIndex = index 66 | $scope.previewer.show = false 67 | $scope.previewer.placeholder_show = true 68 | $scope.previewer.data = $scope.media[index].data 69 | 70 | $scope.onMousedown = (evt) -> 71 | win = hotot.window.current() 72 | bounds = win.getBounds() 73 | offsetX = evt.screenX - bounds.left 74 | offsetY = evt.screenY - bounds.top 75 | holding = true 76 | return 77 | 78 | $scope.onMouseup = (evt) -> 79 | holding = false 80 | return 81 | 82 | $scope.onMousemove = (evt) -> 83 | if holding 84 | win.moveTo(evt.screenX - offsetX, evt.screenY - offsetY) 85 | return 86 | 87 | $scope.previewerShow = -> 88 | return if $scope.previewer.show then '' else 'hide' 89 | 90 | $scope.previewerLoaded = (evt) -> 91 | $scope.previewer.show = true 92 | $scope.previewer.placeholder_show = false 93 | sH = 1 94 | sW = 1 95 | if pic 96 | if pic.naturalHeight > document.height 97 | sH = (0.0+document.height)/pic.naturalHeight 98 | if pic.naturalWidth > document.width 99 | sW = (0.0+document.width)/pic.naturalWidth 100 | $scope.previewer.scale = if sH < sW then sH else sW 101 | 102 | $scope.getSliderStyles = -> 103 | return {'margin-left': $scope.slider.offset + 'px'} 104 | 105 | $scope.enableSlider = -> 106 | return $scope.media.length > 1 107 | 108 | $scope.select = (index) -> 109 | setCurrent(index) 110 | return 111 | 112 | $scope.close = -> 113 | window.close() 114 | return 115 | 116 | $scope.download = -> 117 | hotot.fs.chooseEntry({type: 'saveFile', suggestedName: 'download', accepts:[{description: 'Image Types',mimeTypes: ["image/jpeg", "image/png", "image/gif"], extensions:['jpg', 'png', 'gif']}], acceptsAllTypes:true}, (fileEntry) -> 118 | fileEntry.createWriter((writer) -> 119 | writer.onerror = (e) -> 120 | console.log "write error", e 121 | writer.onwriteend = (e) -> 122 | console.log 'write complete' 123 | Hotot.fetchImage($scope.media[currentIndex].data, (blob) -> 124 | writer.write(blob) 125 | ) 126 | , 127 | (e) -> console.log "write error", e 128 | ) 129 | ) 130 | return 131 | 132 | $scope.share = -> 133 | hotot.bus.sendMessage( 134 | {'win': 'previewer', 'cmd': "share_media", 'content': {'media': $scope.media[currentIndex]}} 135 | ) 136 | 137 | $scope.zoomIn = -> 138 | $scope.previewer.scale += 0.3 139 | return 140 | 141 | $scope.zoomOut = -> 142 | $scope.previewer.scale -= 0.3 143 | if $scope.previewer.scale < 0.1 144 | $scope.previewer.scale = 0.1 145 | return 146 | 147 | $scope.previewerStyles = -> 148 | if pic 149 | return {'height': ($scope.previewer.scale * pic.naturalHeight) + 'px', 'width': ($scope.previewer.scale * pic.naturalWidth) + 'px'} 150 | 151 | reset = (opts, media) -> 152 | $scope.$apply(() -> 153 | $scope.opts = opts 154 | $scope.media = media 155 | loadSlider() 156 | loadFirstMedia() 157 | ) 158 | 159 | return 160 | ]) 161 | -------------------------------------------------------------------------------- /core/scripts/dialog.new_slot.coffee: -------------------------------------------------------------------------------- 1 | DialogCtrl = ($scope) -> 2 | return 3 | 4 | AuthPageCtrl = ($scope, $routeParams) -> 5 | $scope.service = $routeParams.service 6 | $scope.error_box = 7 | show: false 8 | text: '' 9 | $scope.signin_button = 10 | label: '' 11 | cls: '' 12 | working = false 13 | resetAuthButton = () => 14 | $scope.signin_button.label = 'Sign In' 15 | $scope.signin_button.cls = '' 16 | working = false 17 | resetAuthButton() 18 | $scope.auth = (username, password) => 19 | if working 20 | hotot.bus.sendMessage({role: 'new_slot_dialog', cmd: 'cancel_auth', content: {}}) 21 | return 22 | working = true 23 | $scope.error_box.show = false 24 | $scope.signin_button.label = 'Processing' 25 | $scope.signin_button.cls = 'processing' 26 | hotot.bus.sendMessage( 27 | {role: 'new_slot_dialog', cmd: "auth", content: { serv: $scope.service, username: username, password: password }}, 28 | (response) -> 29 | if response.result == 'error' 30 | $scope.error_box.text = response.reason 31 | $scope.error_box.show = true 32 | resetAuthButton() 33 | $scope.$apply() 34 | else 35 | resetAuthButton() 36 | $scope.$apply() 37 | if response.result == 'ok' 38 | window.close() 39 | ) 40 | $scope.hoverSignInButton = -> 41 | if working 42 | $scope.signin_button.label = 'Cancel' 43 | 44 | $scope.noHoverSignInButton = -> 45 | if working 46 | $scope.signin_button.label = 'Processing' 47 | return 48 | 49 | OAuthPageCtrl = ($scope, $routeParams) -> 50 | $scope.service = $routeParams.service 51 | $scope.authorizeURL = '' 52 | $scope.username = '' 53 | $scope.password = '' 54 | $scope.service = $routeParams.service 55 | $scope.error_box = 56 | show: false 57 | text: '' 58 | $scope.signin_button = 59 | label: '' 60 | cls: '' 61 | working = false 62 | webview = null 63 | step = 1 64 | 65 | resetAuthButton = () => 66 | $scope.signin_button.label = 'Sign In' 67 | $scope.signin_button.cls = '' 68 | working = false 69 | 70 | startAutoFill = (username, password) -> 71 | if webview 72 | webview.executeScript({code:'document.getElementById("userId").value="'+username+'"'}) 73 | webview.executeScript({code:'document.getElementById("passwd").value="'+password+'"'}) 74 | setTimeout(() -> 75 | webview.executeScript({code: 'document.forms[0].submit()'}) 76 | step = 2 77 | , 500 78 | ) 79 | 80 | getPINCode = -> 81 | if webview 82 | console.log(webview.location.search) 83 | step = 1 84 | 85 | $scope.initPage = () -> 86 | resetAuthButton() 87 | 88 | $scope.webviewLoadcommit = (evt) -> 89 | console.log('webview commit!') 90 | webview = document.getElementById('webview') 91 | if step == 2 92 | # console.log evt 93 | code = evt.url.substring(evt.url.indexOf('?code=')+6) 94 | resetAuthButton() 95 | hotot.bus.sendMessage( 96 | {role: 'new_slot_dialog', cmd: "oauth_with_pin", content: { serv: $scope.service, username: $scope.username, password: $scope.password, pin: code}}, 97 | (response) -> 98 | if response.result == 'error' 99 | $scope.error_box.text = response.reason 100 | $scope.error_box.show = true 101 | else 102 | if response.result == 'ok' 103 | window.close() 104 | $scope.$apply() 105 | ) 106 | return 107 | 108 | $scope.webviewLoadstop = (evt) -> 109 | console.log('webview loaded!') 110 | webview = document.getElementById('webview') 111 | if step == 1 112 | setTimeout(() -> 113 | startAutoFill($scope.username, $scope.password) 114 | , 500 115 | ) 116 | return 117 | 118 | $scope.auth = (username, password)-> 119 | working = true 120 | $scope.error_box.show = false 121 | $scope.signin_button.label = 'Processing' 122 | $scope.signin_button.cls = 'processing' 123 | hotot.bus.sendMessage( 124 | {role: 'new_slot_dialog', cmd: "oauth_get_authorize_url", content: { serv: $scope.service, username: $scope.username}}, 125 | (response) -> 126 | if response.result == 'error' 127 | $scope.error_box.text = response.reason 128 | $scope.error_box.show = true 129 | else 130 | $scope.authorizeURL = response.content.url 131 | $scope.$apply() 132 | ) 133 | 134 | $scope.getAuthorizeURL = -> 135 | return $scope.authorizeURL 136 | 137 | this.app = angular.module('HototNewSlotDialog', []). 138 | config(['$routeProvider', ($routeProvider) -> 139 | $routeProvider. 140 | when('/page/:service/auth', { templateUrl: '../partials/new_slot_auth_page.html', controller: ['$scope', '$routeParams', AuthPageCtrl]}). 141 | when('/page/:service/oauth', { templateUrl: '../partials/new_slot_oauth_page.html', controller: ['$scope', '$routeParams', OAuthPageCtrl]}). 142 | when('/page/:service/building', { templateUrl: '../partials/new_slot_building_page.html', controller: ['$scope', '$routeParams', AuthPageCtrl]}). 143 | otherwise({redirectTo: '/page/twitter/auth'}); 144 | ]).controller('DialogCtrl', DialogCtrl) 145 | bindDriective(this.app, ['MISC']) 146 | 147 | -------------------------------------------------------------------------------- /core/scripts/lib.sha1.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined 3 | * in FIPS PUB 180-1 4 | * Version 2.1a Copyright Paul Johnston 2000 - 2002. 5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 6 | * Distributed under the BSD License 7 | * See http://pajhome.org.uk/crypt/md5 for details. 8 | */ 9 | 10 | /* 11 | * Configurable variables. You may need to tweak these to be compatible with 12 | * the server-side, but the defaults work in most cases. 13 | */ 14 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ 15 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ 16 | var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ 17 | 18 | /* 19 | * These are the functions you'll usually want to call 20 | * They take string arguments and return either hex or base-64 encoded strings 21 | */ 22 | function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));} 23 | function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));} 24 | function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));} 25 | function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));} 26 | function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} 27 | function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} 28 | 29 | /* 30 | * Perform a simple self-test to see if the VM is working 31 | */ 32 | function sha1_vm_test() 33 | { 34 | return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d"; 35 | } 36 | 37 | /* 38 | * Calculate the SHA-1 of an array of big-endian words, and a bit length 39 | */ 40 | function core_sha1(x, len) 41 | { 42 | /* append padding */ 43 | x[len >> 5] |= 0x80 << (24 - len % 32); 44 | x[((len + 64 >> 9) << 4) + 15] = len; 45 | 46 | var w = Array(80); 47 | var a = 1732584193; 48 | var b = -271733879; 49 | var c = -1732584194; 50 | var d = 271733878; 51 | var e = -1009589776; 52 | 53 | for(var i = 0, l = x.length; i < l; i += 16) 54 | { 55 | var olda = a; 56 | var oldb = b; 57 | var oldc = c; 58 | var oldd = d; 59 | var olde = e; 60 | 61 | for(var j = 0; j < 80; j++) 62 | { 63 | if(j < 16) w[j] = x[i + j]; 64 | else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); 65 | var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), 66 | safe_add(safe_add(e, w[j]), sha1_kt(j))); 67 | e = d; 68 | d = c; 69 | c = rol(b, 30); 70 | b = a; 71 | a = t; 72 | } 73 | 74 | a = safe_add(a, olda); 75 | b = safe_add(b, oldb); 76 | c = safe_add(c, oldc); 77 | d = safe_add(d, oldd); 78 | e = safe_add(e, olde); 79 | } 80 | return Array(a, b, c, d, e); 81 | 82 | } 83 | 84 | /* 85 | * Perform the appropriate triplet combination function for the current 86 | * iteration 87 | */ 88 | function sha1_ft(t, b, c, d) 89 | { 90 | if(t < 20) return (b & c) | ((~b) & d); 91 | if(t < 40) return b ^ c ^ d; 92 | if(t < 60) return (b & c) | (b & d) | (c & d); 93 | return b ^ c ^ d; 94 | } 95 | 96 | /* 97 | * Determine the appropriate additive constant for the current iteration 98 | */ 99 | function sha1_kt(t) 100 | { 101 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : 102 | (t < 60) ? -1894007588 : -899497514; 103 | } 104 | 105 | /* 106 | * Calculate the HMAC-SHA1 of a key and some data 107 | */ 108 | function core_hmac_sha1(key, data) 109 | { 110 | var bkey = str2binb(key); 111 | if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz); 112 | 113 | var ipad = Array(16), opad = Array(16); 114 | for(var i = 0; i < 16; i++) 115 | { 116 | ipad[i] = bkey[i] ^ 0x36363636; 117 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 118 | } 119 | 120 | var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); 121 | return core_sha1(opad.concat(hash), 512 + 160); 122 | } 123 | 124 | /* 125 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 126 | * to work around bugs in some JS interpreters. 127 | */ 128 | function safe_add(x, y) 129 | { 130 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 131 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 132 | return (msw << 16) | (lsw & 0xFFFF); 133 | } 134 | 135 | /* 136 | * Bitwise rotate a 32-bit number to the left. 137 | */ 138 | function rol(num, cnt) 139 | { 140 | return (num << cnt) | (num >>> (32 - cnt)); 141 | } 142 | 143 | /* 144 | * Convert an 8-bit or 16-bit string to an array of big-endian words 145 | * In 8-bit function, characters >255 have their hi-byte silently ignored. 146 | */ 147 | function str2binb(str) 148 | { 149 | var bin = Array(); 150 | var mask = (1 << chrsz) - 1; 151 | for(var i = 0; i < str.length * chrsz; i += chrsz) 152 | bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); 153 | return bin; 154 | } 155 | 156 | /* 157 | * Convert an array of big-endian words to a string 158 | */ 159 | function binb2str(bin) 160 | { 161 | var str = ""; 162 | var mask = (1 << chrsz) - 1; 163 | for(var i = 0; i < bin.length * 32; i += chrsz) 164 | str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); 165 | return str; 166 | } 167 | 168 | /* 169 | * Convert an array of big-endian words to a hex string. 170 | */ 171 | function binb2hex(binarray) 172 | { 173 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 174 | var str = ""; 175 | for(var i = 0; i < binarray.length * 4; i++) 176 | { 177 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + 178 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); 179 | } 180 | return str; 181 | } 182 | 183 | /* 184 | * Convert an array of big-endian words to a base-64 string 185 | */ 186 | function binb2b64(binarray) 187 | { 188 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 189 | var str = ""; 190 | for(var i = 0; i < binarray.length * 4; i += 3) 191 | { 192 | var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) 193 | | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) 194 | | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); 195 | for(var j = 0; j < 4; j++) 196 | { 197 | if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; 198 | else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); 199 | } 200 | } 201 | return str; 202 | } 203 | -------------------------------------------------------------------------------- /core/styles/dialog.people.less: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | body { 5 | overflow-x: hidden; 6 | overflow-y: auto; 7 | } 8 | .scroller { 9 | } 10 | .container { 11 | width: 100%; 12 | } 13 | .vcard { 14 | width: 100%; 15 | background: #f2f2f2 transparent center center repeat; 16 | background-size: cover; 17 | position: relative; 18 | font-weight: lighter; 19 | .avatar { 20 | text-align: center; 21 | position: absolute; 22 | width: 48px; 23 | height: 48px; 24 | border-radius: 2px; 25 | bottom: 10px; 26 | left: 10px; 27 | display: inline-block; 28 | border: rgba(255,255,255,0.2) 3px solid; 29 | .icon { 30 | display: block; 31 | width: 48px; 32 | height: 48px; 33 | background: white none 0 0 no-repeat; 34 | background-size: contain; 35 | border-radius: 2px; 36 | } 37 | .mask { 38 | display: block; 39 | width: 48px; 40 | height: 48px; 41 | position: absolute; 42 | top: 0px; 43 | left: 0px; 44 | box-shadow: inset 0 0 6px rgba(0,0,0,0.3), inset 0 0 1px 1px rgba(0,0,0,0.3); 45 | border-radius: 2px; 46 | } 47 | } 48 | .body { 49 | text-align: left; 50 | padding: 4px 4px 4px 68px; 51 | font-size: 12px; 52 | background-color: rgba(0, 0, 0, 0.5); 53 | margin: 120px 0 0 0; 54 | color: white; 55 | box-shadow: 0 0 0px 1px rgba(0, 0, 0, 0.6); 56 | height: 44px; 57 | .display_name { 58 | font-size: 18px; 59 | padding: 3px 5px; 60 | } 61 | .name { 62 | padding: 3px 5px; 63 | } 64 | .description { 65 | line-height: 1.3; 66 | } 67 | .url { 68 | height: 20px; 69 | line-height: 20px; 70 | overflow: hidden; 71 | } 72 | a { 73 | color: white; 74 | } 75 | a:hover { 76 | color: yellow; 77 | } 78 | } 79 | .control { 80 | height: 30px; 81 | padding: 0; 82 | font-size: 12px; 83 | background: rgba(0, 0, 0, 0.5); 84 | display: -webkit-flex; 85 | -webkit-flex-direction: row; 86 | button { 87 | background: transparent; 88 | outline: none; 89 | border: none; 90 | color: white; 91 | cursor: pointer; 92 | transition: all 0.2s ease; 93 | height: 30px; 94 | font-family: inherit; 95 | margin: 0; 96 | padding: 0 20px; 97 | -webkit-flex: 0 0 100px; 98 | -webkit-order: 1; 99 | } 100 | button:hover { 101 | background: #48f; 102 | } 103 | .following { 104 | color: white; 105 | } 106 | .following:hover { 107 | background: red; 108 | color: white; 109 | } 110 | .non_following { 111 | color: white; 112 | } 113 | .disabled { 114 | 115 | } 116 | .follow_btn { 117 | -webkit-flex: 1 0 auto; 118 | -webkit-order: 1; 119 | } 120 | .more { 121 | .more_menu { 122 | font-size: 12px; 123 | right: 22px; 124 | margin-top: 8px; 125 | z-index: 100; 126 | min-width: 100px; 127 | .sharp { 128 | top: 0; 129 | margin-top: -8px; 130 | right: 16px; 131 | left: auto; 132 | } 133 | } 134 | } 135 | .more:hover { 136 | .more_menu { 137 | display: block; 138 | } 139 | } 140 | } 141 | } 142 | .main { 143 | -webkit-flex: 1 0 auto; 144 | -webkit-order: 2; 145 | width: 100%; 146 | .tabs { 147 | border-top: 1px #dcdcdc solid; 148 | height: 44px; 149 | display: -webkit-flex; 150 | -webkit-flex-direction: row; 151 | border-bottom: 1px #ececec solid; 152 | .tab_button { 153 | padding: 0; 154 | -webkit-flex: 1 0 auto; 155 | -webkit-order: 1; 156 | text-align: center; 157 | color: black; 158 | text-decoration: none; 159 | border-left: #fff 1px solid; 160 | border-right: #ececec 1px solid; 161 | font-size: 12px; 162 | position: relative; 163 | .label { 164 | display: block; 165 | margin-top: 8px; 166 | } 167 | .count { 168 | display: block; 169 | color: #bbb; 170 | margin-top: 4px; 171 | font-size: 0.9em; 172 | } 173 | .caret_frame { 174 | position: absolute; 175 | bottom: 0px; 176 | left: 50%; 177 | margin-left: -6px; 178 | border-left: 8px transparent solid; 179 | border-right: 8px transparent solid; 180 | border-bottom: 6px #ececec solid; 181 | display: block; 182 | content: " "; 183 | height: 0; 184 | width: 0; 185 | display: none; 186 | } 187 | .caret { 188 | position: absolute; 189 | bottom: -1px; 190 | left: 50%; 191 | margin-left: -6px; 192 | border-left: 8px transparent solid; 193 | border-right: 8px transparent solid; 194 | border-bottom: 5px #fff solid; 195 | display: block; 196 | content: " "; 197 | height: 0; 198 | width: 0; 199 | display: none; 200 | } 201 | } 202 | .tab_button.selected { 203 | .caret_frame { 204 | display: block; 205 | } 206 | .caret { 207 | display: block; 208 | } 209 | } 210 | } 211 | .tab_pages { 212 | padding-bottom: 32px; 213 | } 214 | } 215 | #main { 216 | .column { 217 | float: none; 218 | .body { 219 | min-height: 300px; 220 | .bg_placeholder { 221 | position: static; 222 | } 223 | } 224 | } 225 | } -------------------------------------------------------------------------------- /core/dialogs/compose.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Compose 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 77 |
78 |
79 |
80 |
81 |
{{context_bar.text}}
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | 94 |
{{props.remainCharNum}}
95 |
96 | 97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | 106 |
{{effects_bar.spinner_label}}
107 |
108 | 109 | -------------------------------------------------------------------------------- /core/login_b.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Twitter / Authorize an application 8 | 9 | 10 | 13 | 14 | 15 | 16 | 19 | 24 | 29 | 30 | 31 | 32 | 42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 |

Authorize Hotot for Chrome to use your account?

52 | 53 | 54 |
55 |

56 | 57 | Hotot for Chrome 58 |

59 |
60 |
Developer
61 |
By Hotot.org
62 |
Application URL
63 |
hotot.org
64 |
About this app
65 |

Hotot Client for Chrome(Chrome App)

66 |
67 |
68 | 69 | 70 | 71 |
72 |

This application will be able to:

73 |
    74 | 75 | 76 |
  • Read Tweets from your timeline.
  • 77 | 78 | 79 |
  • See who you follow, and follow new people.
  • 80 |
  • Update your profile.
  • 81 |
  • Post Tweets for you.
  • 82 | 83 | 84 | 85 | 86 |
  • Access your direct messages.
  • 87 | 88 |
89 |
90 | 91 | 92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 118 | 119 | 120 | 121 |
122 | Authorize Hotot for Chrome access to use your account? 123 | 124 | 125 |
126 |
127 |
128 |

This application will not be able to:

129 |
    130 | 131 | 132 | 133 | 134 |
  • See your Twitter password.
  • 135 |
136 |
137 | 138 |
139 | 140 | 141 | 142 | 143 |
144 | 145 | 146 | 147 | 148 | 154 | 155 | 156 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /core/login_a.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Twitter / Authorize an application 8 | 9 | 10 | 13 | 14 | 15 | 16 | 19 | 24 | 29 | 30 | 31 | 32 | 62 | 63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 |

Authorize Hotot for Chrome to use your account?

72 | 73 | 74 |
75 |

76 | 77 | Hotot for Chrome 78 |

79 |
80 |
Developer
81 |
By Hotot.org
82 |
Application URL
83 |
hotot.org
84 |
About this app
85 |

Hotot Client for Chrome(Chrome App)

86 |
87 |
88 | 89 | 90 | 91 |
92 |

This application will be able to:

93 |
    94 | 95 | 96 |
  • Read Tweets from your timeline.
  • 97 | 98 | 99 |
  • See who you follow, and follow new people.
  • 100 |
  • Update your profile.
  • 101 |
  • Post Tweets for you.
  • 102 | 103 | 104 | 105 | 106 |
  • Access your direct messages.
  • 107 | 108 |
109 |
110 | 111 | 112 |
113 | 114 | 115 | 116 |
117 | Authorize Hotot for Chrome access to use your account? 118 | 119 | 120 |
121 |
122 |
123 |

This application will not be able to:

124 |
    125 | 126 | 127 | 128 | 129 |
  • See your Twitter password.
  • 130 |
131 |
132 | 133 |
134 | 135 | 136 | 137 | 138 |
139 | 140 | 141 | 142 | 143 | 149 | 150 | 151 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /core/scripts/util.column.coffee: -------------------------------------------------------------------------------- 1 | class ColumnUtils 2 | @COMPARE_MODE_ID = 0 3 | @COMPARE_MODE_TIME = 1 4 | @COMPARE_MODE_ID_MIXED = 2 5 | @COMPARE_MODE_TIME_MIXED = 3 #defaults 6 | @compareTimestamp = (it1, it2) -> 7 | ts1 = it1.timestamp 8 | ts2 = it2.timestamp 9 | if ts1 == ts2 10 | return 0 11 | else 12 | return if ts1 < ts2 then -1 else 1 13 | 14 | @compareId = (id1, id2) -> 15 | if id1.length < id2.length 16 | return -1 17 | else if id2.length < id1.length 18 | return 1 19 | else 20 | if id1 == id2 21 | return 0 22 | else 23 | return if id1 < id2 then -1 else 1 24 | 25 | @compareItemId = (it1, it2) -> 26 | id1 = it1.id 27 | id2 = it2.id 28 | return ColumnUtils.compareId(id1, id2) 29 | 30 | @getNext = (totalSize, current, reversion) -> 31 | # reversion == true means from top to down 32 | nextIndex = -1 33 | if totalSize == 0 # empty 34 | return -1 35 | if current == -1 36 | nextIndex = if reversion then 0 else totalSize - 1 37 | else 38 | if reversion 39 | nextIndex = if current == totalSize - 1 then -1 else current + 1 40 | else 41 | nextIndex = if current == 0 then -1 else current - 1 42 | return nextIndex 43 | 44 | # column controller should supports following methods: 45 | # - append 46 | # - appendMany 47 | # - prepend 48 | # - prependMany 49 | # - insert: called by daemon, push update, insert one item to correct position 50 | # - insertMany: same to above, insert several items to correct position 51 | @append = (can, item) => 52 | can.push(item) 53 | 54 | @prepend = (can, item) => 55 | can.unshift(item) 56 | 57 | @appendMany = (can, items) => 58 | for it in items 59 | can.push(it) 60 | 61 | @prependMany = (can, items) => 62 | for it in items 63 | can.unshift(it) 64 | 65 | @insert = (can, item, opts) => 66 | # console.log 'insert', item.text 67 | reversion = true # from top to bottom by defaults 68 | compareMode = @COMPARE_MODE_TIME_MIXED 69 | compareProcMajor = @compareTimestamp 70 | compareProcMinor = @compareItemId 71 | if opts 72 | if opts.hasOwnProperty('reversion') then reversion = opts.reversion 73 | if opts.hasOwnProperty('compare_mode') then compareMode = opts.compare_mode 74 | if compareMode == @COMPARE_MODE_ID or compareMode == @COMPARE_MODE_ID_MIXED 75 | compareProcMajor = @compareItemId 76 | compareProcMinor = @compareTimestamp 77 | nextIndex = @getNext(can.length, -1, reversion) 78 | while true 79 | # console.log 'next index', nextIndex 80 | if nextIndex == -1 # empty 81 | # insert to end of container 82 | # or the top of the container, 83 | # according to argument `reversion` 84 | if reversion 85 | @prepend(can, item) 86 | else 87 | @append(can, item) 88 | return 1 89 | else # not empty 90 | nextOne = can[nextIndex] 91 | cmpRet = compareProcMajor(nextOne, item) 92 | # console.log cmpRet, nextOne.id, item.id 93 | if cmpRet == 0 94 | if compareMode == @COMPARE_MODE_ID_MIXED or compareMode == @COMPARE_MODE_TIME_MIXED 95 | # nextOne.timestamp == item.timestamp 96 | subRet = compareProcMinor(nextOne, item) 97 | if subRet == 0 98 | # console.log 'duplicate, dump' 99 | return 0 100 | else if cmpRet == -1 101 | # nextOne.id < item.id 102 | if reversion 103 | can.splice(nextIndex, 0, item) 104 | return 1 105 | else 106 | nextIndex = @getNext(can.length, nextIndex, reversion) 107 | if nextIndex == -1 108 | # console.log 'meet top' 109 | can.unshift(item) 110 | return 1 111 | # console.log 'too big, try upper one:', nextIndex 112 | else 113 | # nextOne.timestamp > item.timestamp 114 | if reversion 115 | nextIndex = @getNext(can.length, nextIndex, reversion) 116 | if nextIndex == -1 117 | # console.log 'meet bottom' 118 | can.push(item) 119 | return 1 120 | # console.log 'too small, try lower one:', nextIndex 121 | else 122 | can.splice(nextIndex + 1, 0, item) 123 | return 1 124 | else 125 | return 0 126 | else if cmpRet == -1 127 | # nextOne.timestamp < item.timestamp 128 | if reversion 129 | can.splice(nextIndex, 0, item) 130 | return 1 131 | else 132 | nextIndex = @getNext(can.length, nextIndex, reversion) 133 | if nextIndex == -1 134 | # console.log 'meet top' 135 | can.unshift(item) 136 | return 1 137 | # console.log 'too big, try upper one:', nextIndex 138 | else 139 | # nextOne.timestamp > item.timestamp 140 | if reversion 141 | nextIndex = @getNext(can.length, nextIndex, reversion) 142 | if nextIndex == -1 143 | # console.log 'meet bottom' 144 | can.push(item) 145 | return 1 146 | # console.log 'too small, try lower one:', nextIndex 147 | else 148 | can.splice(nextIndex + 1, 0, item) 149 | return 1 150 | return 0 151 | 152 | @insertMessage = (can, item) -> 153 | can.push(item) 154 | return 1 155 | 156 | @makeColumnOrder = (formalizer, columnsOrder, columns) -> 157 | out = [] 158 | outNames = [] 159 | for orderName in columnsOrder 160 | for col in columns 161 | if col.name == orderName 162 | out.push(formalizer(col)) 163 | outNames.push(orderName) 164 | break 165 | return [out, outNames] 166 | 167 | @mergeAttachments = (dst, src) -> 168 | if src.has_attachments 169 | dst.attachments = dst.attachments.concat(src.attachments) 170 | dst.has_attachments = true 171 | if dst.attachments_label.length == 0 172 | dst.attachments_label = src.attachments_label 173 | return dst 174 | 175 | root = exports ? this 176 | this.ColumnUtils = ColumnUtils 177 | -------------------------------------------------------------------------------- /core/scripts/dialog.message.coffee: -------------------------------------------------------------------------------- 1 | this.app = angular.module('HototMessageDialog', ['ngSanitize']) 2 | bindDriective(this.app, ['KEY']) 3 | this.app.controller('MessageCtrl', ['$scope', ($scope) -> 4 | $scope.conversations = [] 5 | $scope.currentConversation = null 6 | $scope.majorAccount = null 7 | $scope.currentSlot = null 8 | $scope.slots = [] 9 | textBox = null 10 | chatBox = null 11 | currentSlotKey = '' 12 | currentRecipient = null 13 | $scope.add_con_box = 14 | show: false 15 | recipient: '' 16 | text: '' 17 | 18 | hotot.bus.onMessage.addListener((request, sender, senderResponse) -> 19 | if not request.cmd 20 | return 21 | if request.cmd == 'reset_message' 22 | console.log request 23 | $scope.majorAccount = request.content.major_account 24 | $scope.slots = request.content.accounts 25 | constructConversations(request.content.conversations) 26 | for slot, i in $scope.slots 27 | if slot.name == $scope.majorAccount.name and slot.serv = $scope.majorAccount.serv 28 | $scope.$apply(() -> 29 | $scope.selectSlot(slot, i) 30 | ) 31 | break 32 | document.querySelector('.text_box').focus() 33 | textBox = angular.element(document.querySelector('.text_box')) 34 | chatBox = document.querySelector('.messages') 35 | setTimeout(() -> 36 | chatBox.scrollTop = chatBox.scrollHeight 37 | , 500 38 | ) 39 | if request.content.context and request.content.context.name 40 | $scope.$apply(() -> 41 | $scope.add_con_box.recipient = request.content.context.name 42 | $scope.add_con_box.text = '' 43 | $scope.add_con_box.show = true 44 | ) 45 | else if request.cmd == "new_messages" 46 | serv = request.content.serv 47 | slotName = request.content.slot_name 48 | messages = request.content.messages 49 | target = null 50 | if $scope.majorAccount.name == slotName and $scope.majorAccount.serv == serv 51 | for message in messages 52 | # @FIXME here: 53 | # only deal with message others send to me and message i send. 54 | ret = $scope.selectConversationByName(message.sender.name, message.recipient.name) 55 | con = ret[0] 56 | pos = ret[1] 57 | if con == null 58 | user = if message.recipient.name == $scope.majorAccount.name then message.sender else message.recipient 59 | target = 60 | last_update: msg.timestamp 61 | user: user 62 | messages: [message] 63 | $scope.$apply(() -> 64 | $scope.conversations[currentSlotKey].unshift(target) 65 | ) 66 | else 67 | target = con 68 | target.last_update = message.timestamp 69 | target.messages.push(message) 70 | $scope.$apply(() -> 71 | $scope.conversations[currentSlotKey].splice(pos, 1) 72 | $scope.conversations[currentSlotKey].unshift(target) 73 | ) 74 | setTimeout(() -> 75 | chatBox.scrollTop = chatBox.scrollHeight 76 | , 500 77 | ) 78 | ) 79 | 80 | 81 | $scope.selectConversationByName = (senderName, recipientName) -> 82 | pos = -1 83 | for con, i in $scope.conversations[currentSlotKey] 84 | if con.user.name == senderName or con.user.name == recipientName 85 | pos = i 86 | break 87 | if pos == -1 88 | return [null, -1] 89 | else 90 | return [con, pos] 91 | 92 | $scope.selectSlot = (slot, index) -> 93 | console.log "select slot #{slot.serv}/#{slot.name}" 94 | $scope.currentSlot = $scope.slots[index] 95 | currentSlotKey = "#{$scope.currentSlot.serv}/#{$scope.currentSlot.name}" 96 | if $scope.currentSlot 97 | if $scope.conversations[currentSlotKey].length != 0 98 | $scope.selectConversation($scope.conversations[currentSlotKey][0]) 99 | return 100 | 101 | 102 | constructConversations = (_cons) -> 103 | cons = [] 104 | for slotKey, _slotCons of _cons 105 | slotCons = [] 106 | for key, con of _slotCons 107 | con.messages.reverse() 108 | slotCons.push(con) 109 | slotCons.sort((a, b) -> 110 | return b.last_update - a.last_update 111 | ) 112 | cons[slotKey] = slotCons 113 | $scope.$apply(() -> 114 | $scope.conversations = cons 115 | ) 116 | 117 | $scope.selectConversation = (con) -> 118 | $scope.currentConversation = con 119 | currentRecipient = con.user 120 | setTimeout(() -> 121 | chatBox.scrollTop = chatBox.scrollHeight 122 | , 200 123 | ) 124 | 125 | $scope.getCurrentConversations = () -> 126 | return $scope.conversations[currentSlotKey] 127 | 128 | $scope.getMessageDirection = (msg) -> 129 | if msg.sender.name == $scope.majorAccount.name 130 | return 'right' 131 | return '' 132 | 133 | $scope.handleKeyUp = (evt) -> 134 | return 135 | 136 | $scope.handleKeyDown = (evt) -> 137 | # shortcut binding Enter 138 | if evt.keyCode == 13 139 | $scope.post() 140 | return false 141 | 142 | $scope.post = () -> 143 | text = textBox.val() 144 | if text.trim().length != 0 145 | hotot.bus.sendMessage( 146 | {win: 'message_dialog', cmd: "drop", content: {account: $scope.majorAccount, recipient: currentRecipient, text: text}} 147 | ) 148 | textBox.val('') 149 | return false 150 | 151 | $scope.loadAvatar = (user) -> 152 | Hotot.fetchImage(user.avatar_url, (data) -> 153 | $scope.$apply(() -> 154 | user.avatar_data = window.webkitURL.createObjectURL(data) 155 | ) 156 | ) 157 | 158 | $scope.addConversation = -> 159 | $scope.add_con_box.show = true 160 | 161 | $scope.discard = -> 162 | $scope.add_con_box.show = false 163 | 164 | $scope.sendMessage = () -> 165 | text = $scope.add_con_box.text 166 | recipient = $scope.add_con_box.recipient 167 | if text.trim().length != 0 and recipient.trim().length 168 | hotot.bus.sendMessage( 169 | { 170 | win: 'message_dialog', cmd: "drop", content: {account: $scope.majorAccount, recipient: {name: recipient}, text: text} 171 | }, (resp) -> 172 | $scope.add_con_box.recipient = '' 173 | $scope.add_con_box.text = '' 174 | currentSlotKey = "#{$scope.currentSlot.serv}/#{$scope.currentSlot.name}" 175 | if $scope.currentSlot and $scope.conversations[currentSlotKey].length != 0 176 | $scope.selectConversation($scope.conversations[currentSlotKey]) 177 | ) 178 | $scope.add_con_box.show = false 179 | 180 | 181 | return 182 | ]) 183 | -------------------------------------------------------------------------------- /core/scripts/lib.base64.js: -------------------------------------------------------------------------------- 1 | function urlDecode(str){ 2 | str=str.replace(new RegExp('\\+','g'),' '); 3 | return unescape(str); 4 | } 5 | function urlEncode(str){ 6 | str=escape(str); 7 | str=str.replace(new RegExp('\\+','g'),'%2B'); 8 | return str.replace(new RegExp('%20','g'),'+'); 9 | } 10 | 11 | var END_OF_INPUT = -1; 12 | 13 | var base64Chars = new Array( 14 | 'A','B','C','D','E','F','G','H', 15 | 'I','J','K','L','M','N','O','P', 16 | 'Q','R','S','T','U','V','W','X', 17 | 'Y','Z','a','b','c','d','e','f', 18 | 'g','h','i','j','k','l','m','n', 19 | 'o','p','q','r','s','t','u','v', 20 | 'w','x','y','z','0','1','2','3', 21 | '4','5','6','7','8','9','+','/' 22 | ); 23 | 24 | var reverseBase64Chars = new Array(); 25 | for (var i=0, l = base64Chars.length; i < l; i++){ 26 | reverseBase64Chars[base64Chars[i]] = i; 27 | } 28 | 29 | var base64Str; 30 | var base64Count; 31 | function setBase64Str(str){ 32 | base64Str = str; 33 | base64Count = 0; 34 | } 35 | function readBase64(){ 36 | if (!base64Str) return END_OF_INPUT; 37 | if (base64Count >= base64Str.length) return END_OF_INPUT; 38 | var c = base64Str.charCodeAt(base64Count) & 0xff; 39 | base64Count++; 40 | return c; 41 | } 42 | function encodeBase64(str){ 43 | setBase64Str(str); 44 | var result = ''; 45 | var inBuffer = new Array(3); 46 | var lineCount = 0; 47 | var done = false; 48 | while (!done && (inBuffer[0] = readBase64()) != END_OF_INPUT){ 49 | inBuffer[1] = readBase64(); 50 | inBuffer[2] = readBase64(); 51 | result += (base64Chars[ inBuffer[0] >> 2 ]); 52 | if (inBuffer[1] != END_OF_INPUT){ 53 | result += (base64Chars [(( inBuffer[0] << 4 ) & 0x30) | (inBuffer[1] >> 4) ]); 54 | if (inBuffer[2] != END_OF_INPUT){ 55 | result += (base64Chars [((inBuffer[1] << 2) & 0x3c) | (inBuffer[2] >> 6) ]); 56 | result += (base64Chars [inBuffer[2] & 0x3F]); 57 | } else { 58 | result += (base64Chars [((inBuffer[1] << 2) & 0x3c)]); 59 | result += ('='); 60 | done = true; 61 | } 62 | } else { 63 | result += (base64Chars [(( inBuffer[0] << 4 ) & 0x30)]); 64 | result += ('='); 65 | result += ('='); 66 | done = true; 67 | } 68 | lineCount += 4; 69 | if (lineCount >= 76){ 70 | result += ('\n'); 71 | lineCount = 0; 72 | } 73 | } 74 | return result; 75 | } 76 | function readReverseBase64(){ 77 | if (!base64Str) return END_OF_INPUT; 78 | while (true){ 79 | if (base64Count >= base64Str.length) return END_OF_INPUT; 80 | var nextCharacter = base64Str.charAt(base64Count); 81 | base64Count++; 82 | if (reverseBase64Chars[nextCharacter]){ 83 | return reverseBase64Chars[nextCharacter]; 84 | } 85 | if (nextCharacter == 'A') return 0; 86 | } 87 | return END_OF_INPUT; 88 | } 89 | 90 | function ntos(n){ 91 | n=n.toString(16); 92 | if (n.length == 1) n="0"+n; 93 | n="%"+n; 94 | return unescape(n); 95 | } 96 | 97 | function decodeBase64(str){ 98 | setBase64Str(str); 99 | var result = ""; 100 | var inBuffer = new Array(4); 101 | var done = false; 102 | while (!done && (inBuffer[0] = readReverseBase64()) != END_OF_INPUT 103 | && (inBuffer[1] = readReverseBase64()) != END_OF_INPUT){ 104 | inBuffer[2] = readReverseBase64(); 105 | inBuffer[3] = readReverseBase64(); 106 | result += ntos((((inBuffer[0] << 2) & 0xff)| inBuffer[1] >> 4)); 107 | if (inBuffer[2] != END_OF_INPUT){ 108 | result += ntos((((inBuffer[1] << 4) & 0xff)| inBuffer[2] >> 2)); 109 | if (inBuffer[3] != END_OF_INPUT){ 110 | result += ntos((((inBuffer[2] << 6) & 0xff) | inBuffer[3])); 111 | } else { 112 | done = true; 113 | } 114 | } else { 115 | done = true; 116 | } 117 | } 118 | return result; 119 | } 120 | 121 | var digitArray = new Array('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'); 122 | function toHex(n){ 123 | var result = '' 124 | var start = true; 125 | for (var i=32; i>0;){ 126 | i-=4; 127 | var digit = (n>>i) & 0xf; 128 | if (!start || digit != 0){ 129 | start = false; 130 | result += digitArray[digit]; 131 | } 132 | } 133 | return (result==''?'0':result); 134 | } 135 | 136 | function pad(str, len, pad){ 137 | var result = str; 138 | for (var i=str.length; i