├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── assets ├── js │ ├── app.js │ ├── config.sample.js │ ├── controllers │ │ ├── appFrameCtrl.js │ │ ├── bookCtrl.js │ │ ├── booksCtrl.js │ │ ├── editorCtrl.js │ │ ├── evernoteCtrl.js │ │ ├── favCtrl.js │ │ ├── friendsCtrl.js │ │ ├── infoCtrl.js │ │ ├── loginCtrl.js │ │ ├── module.js │ │ └── noteCtrl.js │ ├── directives.js │ ├── douban-auth.js │ ├── enml.js │ ├── evernote-auth.js │ ├── filters.js │ ├── services │ │ ├── analyticsService.js │ │ ├── authService.js │ │ ├── bookService.js │ │ ├── evernoteService.js │ │ ├── favouriteService.js │ │ ├── filesystemService.js │ │ ├── friendsService.js │ │ ├── httpLoadingIntercepter.js │ │ ├── httpOAuthIntercepter.js │ │ ├── noteService.js │ │ ├── serializeService.js │ │ ├── translateService.js │ │ └── userService.js │ └── ui-directives.js ├── lib │ ├── angular-bootstrap │ │ ├── ui-bootstrap-tpls.js │ │ └── ui-bootstrap.js │ ├── angular │ │ ├── angular-cookies.js │ │ ├── angular-resource.js │ │ ├── angular-route.js │ │ └── angular.js │ ├── codemirror │ │ └── codemirror.js │ ├── color-thief │ │ └── color-thief.js │ ├── marked │ │ └── marked.js │ ├── ng-infinite-scroll │ │ └── ng-infinite-scroll.js │ └── underscore │ │ └── underscore.js ├── resource │ ├── background.js │ ├── css │ │ ├── codemirror.css │ │ └── themes │ │ │ ├── solarized_dark.css │ │ │ ├── tomorrow.css │ │ │ └── zenburn.css │ ├── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.json │ │ ├── icomoon.svg │ │ ├── icomoon.ttf │ │ └── icomoon.woff │ ├── img │ │ ├── books.png │ │ └── login_with_douban_32.png │ └── manifest.json └── stylus │ ├── edit.styl │ ├── fonticon.styl │ ├── lib │ └── vendor.styl │ ├── mod_about.styl │ ├── mod_aside.styl │ ├── mod_auth.styl │ ├── mod_friends.styl │ ├── mod_info.styl │ ├── note.styl │ ├── style.styl │ ├── ui │ ├── alert.styl │ ├── bar.styl │ ├── button.styl │ ├── card.styl │ ├── form.styl │ ├── loading.styl │ ├── modal.styl │ ├── reset.styl │ └── typography.styl │ └── widgets │ ├── book-3d.styl │ ├── book.styl │ ├── editor.styl │ ├── note-card.styl │ └── reader.styl ├── bower.json ├── package.json ├── public ├── background.js ├── css │ ├── codemirror.css │ ├── style.css │ └── themes │ │ ├── solarized_dark.css │ │ ├── tomorrow.css │ │ └── zenburn.css ├── fonts │ ├── icomoon.eot │ ├── icomoon.json │ ├── icomoon.svg │ ├── icomoon.ttf │ └── icomoon.woff ├── img │ ├── bg.png │ ├── books.png │ ├── fabric_of_squares_gray.png │ └── login_with_douban_32.png ├── index.html ├── js │ ├── app.min.js │ ├── controllers.min.js │ ├── directives.min.js │ ├── douban-auth.min.js │ ├── enml.min.js │ ├── evernote-auth.min.js │ ├── filters.min.js │ ├── services.min.js │ └── ui-directives.min.js ├── lib │ ├── angular-bootstrap │ │ ├── ui-bootstrap-tpls.min.js │ │ └── ui-bootstrap.min.js │ ├── angular │ │ ├── angular-cookies.min.js │ │ ├── angular-resource.min.js │ │ ├── angular-route.min.js │ │ ├── angular-sanitize.min.js │ │ └── angular.min.js │ ├── codemirror │ │ └── codemirror.min.js │ ├── color-thief │ │ └── color-thief.min.js │ ├── evernote │ │ └── evernote-sdk.min.js │ ├── marked │ │ └── marked.min.js │ ├── ng-infinite-scroll │ │ └── ng-infinite-scroll.min.js │ └── underscore │ │ └── underscore.min.js ├── manifest.json └── partials │ ├── about.html │ ├── book.html │ ├── books.html │ ├── edit.html │ ├── error.html │ ├── fav.html │ ├── friends.html │ ├── info.html │ ├── login.html │ ├── note.html │ └── widgets │ ├── editor.html │ └── reader.html └── views ├── index.jade ├── layout.jade └── partials ├── about.jade ├── book.jade ├── books.jade ├── edit.jade ├── error.jade ├── fav.jade ├── friends.jade ├── info.jade ├── login.jade ├── note.jade └── widgets ├── editor.jade └── reader.jade /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *~ 3 | 4 | *.orig 5 | *.swp 6 | 7 | node_modules/ 8 | bower_components/ 9 | 10 | 11 | assets/js/config.js 12 | assets/js/controllers.js 13 | assets/js/services.js 14 | public/js/config.min.js 15 | public/css/style.css 16 | public/ 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 NdYAG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ANNO 2 | 3 | ![](http://ww4.sinaimg.cn/small/6143ba6fjw1eel7jb3eolj203k03kjr8.jpg) 4 | 5 | ## 豆瓣读书笔记第三方客户端 6 | 7 | 这是一个基于Angular开发的Chrome App,可以独立安装在系统中。下载地址:[https://chrome.google.com/webstore/detail/读书笔记/cekpldeffalionmgoedldkdnlcbphamp](https://chrome.google.com/webstore/detail/读书笔记/cekpldeffalionmgoedldkdnlcbphamp) 8 | 9 | ## 基本功能 10 | 11 | 读书笔记的管理,写作,收藏,导出。 12 | 13 | ## 参与开发 14 | 15 | ANNO是一个开源的应用,目前基本支持了豆瓣的 OAuth2 读书笔记读写 API,还有以下功能正在开发: 16 | 17 | * 加入数学公式支持 18 | * ~~带格式导出笔记到Evernote~~ (已集成) 19 | * …… 20 | 21 | ### 开发准备 22 | 23 | 以下操作都在项目目录下执行 24 | 25 | * `npm install` 安装 Grunt 等依赖 26 | * `bower install` 安装前端使用的库文件(如果不需要使用到新的库文件,这一步和下一步可以忽略) 27 | * 将`bower_components/`中所需文件移到 `assets/lib/` 中 28 | * 申请豆瓣开发者帐号 [http://developers.douban.com/](http://developers.douban.com/) 29 | * 复制一份 `assets/config.sample.js` 到 `assets/config.js` ,填写你的 `api_key` 和 `api_secret` 30 | * 开发时执行 `grunt watch`,将 `assets/` 下的文件编译、压缩到 `public/` 中。 31 | 32 | 在Chrome扩展中加载项目目录,开启应用调试。 33 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // shortcut 4 | var $ = function(selector) { 5 | if (typeof selector === 'string') { 6 | return angular.element(document.querySelectorAll(selector)) 7 | } else if (selector.nodeType === 1) { 8 | return angular.element(selector) 9 | } else { 10 | return null 11 | } 12 | } 13 | 14 | var app = angular.module('ANNO', [ 15 | 'ngRoute', 16 | 'ANNO.controllers', 17 | 'ANNO.directives', 18 | 'ANNO.ui-directives', 19 | 'ui.bootstrap' 20 | ]).config(['$routeProvider', function($routeProvider) { 21 | $routeProvider.when('/login', { 22 | templateUrl: '/partials/login.html', 23 | controller: 'LoginCtrl' 24 | }).when('/', { 25 | templateUrl: '/partials/books.html', 26 | controller: 'BooksCtrl' 27 | }).when('/:uid/book/:bid', { 28 | templateUrl: '/partials/book.html', 29 | controller: 'BookCtrl' 30 | }).when('/:uid/book/:bid/new', { 31 | templateUrl: '/partials/edit.html', 32 | controller: 'EditorCtrl' 33 | }).when('/:uid/info', { 34 | templateUrl: '/partials/info.html', 35 | controller: 'InfoCtrl' 36 | }).when('/note/:nid', { 37 | templateUrl: '/partials/note.html', 38 | controller: 'NoteCtrl' 39 | }).when('/note/:nid/edit', { 40 | templateUrl: '/partials/edit.html', 41 | controller: 'EditorCtrl' 42 | }).when('/friends', { 43 | templateUrl: '/partials/friends.html', 44 | controller: 'FriendsCtrl' 45 | }).when('/fav', { 46 | templateUrl: '/partials/fav.html', 47 | controller: 'FavCtrl' 48 | }).when('/about', { 49 | templateUrl: '/partials/about.html' 50 | }).when('/:uid/', { 51 | templateUrl: '/partials/books.html', 52 | controller: 'BooksCtrl' 53 | }).otherwise({ 54 | templateUrl: '/partials/error.html' 55 | }) 56 | }]) 57 | .config(['$httpProvider', function($httpProvider) { 58 | $httpProvider.interceptors.push('HttpLoadingIntercepter') 59 | $httpProvider.interceptors.push('HttpOAuthIntercepter') 60 | }]) 61 | .config(['$compileProvider', function($compileProvider) { 62 | $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|blob):|data:image\//) 63 | }]) 64 | .run(function($route, $rootScope, $location, UserService) { 65 | $rootScope.$on( "$routeChangeStart", function(event, next, current) { 66 | if (next.templateUrl == "/partials/about.html" || next.templateUrl == "/partials/login.html") { 67 | return 68 | } 69 | UserService.isLoggedIn().catch(function() { 70 | $location.path('/login') 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /assets/js/config.sample.js: -------------------------------------------------------------------------------- 1 | var DoubanAuthConf = { 2 | "api_key": "", 3 | "api_secret": "", 4 | "scopes": "douban_basic_common,book_basic_r,book_basic_w" 5 | } 6 | var EvernoteAuthConf = { 7 | "consumer_key": "", 8 | "consumer_secret": "", 9 | "hostname": { 10 | "evernote": "https://www.evernote.com", 11 | "yinxiang": "https://app.yinxiang.com" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/js/controllers/appFrameCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('appFrameCtrl', ['$scope', '$rootScope', '$location', '$modal', '$compile', 'UserService', 'AnalyticsService', function($scope, $rootScope, $location, $modal, $compile, UserService, AnalyticsService) { 2 | $scope.asideVisible = 0 3 | $scope.toggleSidebar = function() { 4 | $scope.asideVisible ^= 1 5 | } 6 | $scope.$on('$routeChangeStart', function() { 7 | $scope.lastBook = null 8 | $scope.globalTitle = '' 9 | }) 10 | $scope.$on('nav:lastBook', function(e, author, book) { 11 | $scope.author = author 12 | $scope.lastBook = book 13 | }) 14 | $scope.$on('page:change', function(e, mode, title) { 15 | $scope.headerInvisible = (mode == 'note' || mode == 'editor') 16 | $scope.globalCSSClass = 'global_' + mode 17 | $scope.globalTitle = title || '' 18 | AnalyticsService.sendAppView(mode) 19 | }) 20 | 21 | $scope.logout = function() { 22 | var modalInstance = $modal.open({ 23 | templateUrl: 'modalLogout.html', 24 | controller: function($scope, $modalInstance) { 25 | $scope.ok = function (bid) { 26 | UserService.logout(function() { 27 | $location.path('/login') 28 | $modalInstance.close() 29 | }) 30 | } 31 | $scope.cancel = function () { 32 | $modalInstance.dismiss('cancel') 33 | } 34 | } 35 | }) 36 | } 37 | }]) -------------------------------------------------------------------------------- /assets/js/controllers/bookCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('BookCtrl', ['$scope', '$routeParams', '$modal', '$timeout', 'UserService', 'SerializeService', 'TranslateService', 'FileSystemService', function($scope, $routeParams, $modal, $timeout, UserService, SerializeService, TranslateService, FileSystemService) { 2 | 3 | var books, book, notes 4 | , uid = $routeParams.uid 5 | , bid = $routeParams.bid 6 | UserService.login().then(function(user) { 7 | $scope.user = user 8 | }) 9 | UserService.getUser(uid).then(function(user) { 10 | // retrieve book 11 | $scope.dominateColor = { 12 | background: '#fff', 13 | foreground: '#000' 14 | } 15 | $scope.author = user 16 | SerializeService.fetchBook(user.id, bid).then(function(book) { 17 | $scope.$emit('page:change', 'book', (user.is_self? '我的书架 ❯ ': '' + user.name + '的书架 ❯ ') + book.title) 18 | $scope.book = book 19 | $scope.order = 'time' 20 | $scope.reverse = true 21 | $scope.book_cover = book.images.large 22 | }) 23 | 24 | 25 | $scope.book_opened = 0 26 | $scope.openBook = function() { 27 | $scope.book_opened = 1 28 | } 29 | $scope.closeBook = function() { 30 | $scope.book_opened = 0 31 | } 32 | $scope.book_sided = false 33 | $scope.$watch('dominateColor', function(colors) { 34 | if (colors.background != '#fff') { // after processed by color thief, background will be rgb. Thus #fff means color thief has not stealed the color 35 | $scope.book_sided = true 36 | } 37 | }) 38 | 39 | $scope.exportNotes = function() { 40 | SerializeService.fetchBook(user.id, bid).then(function(book) { 41 | var exports 42 | exports = book.notes.map(function(note) { 43 | return (note.chapter? ('#' + note.chapter + '\n\n') : '') 44 | + TranslateService.doubanToMarkdown(note.content, note.photos) 45 | }).join('\n\n-----\n\n') 46 | 47 | var modalInstance = $modal.open({ 48 | templateUrl: 'modalExport.html', 49 | controller: function($scope, $modalInstance) { 50 | $scope.download = function () { 51 | if ($scope.exportFormat == 'markdown') { 52 | var blob = new Blob([exports], {type: "text/plain;charset=utf-8"}) 53 | // saveAs(blob, book.title + ".md") 54 | FileSystemService.save(blob, book.title, 'md', function(is_saved) { 55 | is_saved && $modalInstance.close() 56 | }) 57 | } else if ($scope.exportFormat == 'html') { 58 | var el = angular.element(document.querySelector('.preview-html')).clone() 59 | Array.prototype.slice.call(el.find('img'), 0).forEach(function(img) { 60 | img = $(img) 61 | img.attr('src', img.attr('data-src')) 62 | img.removeAttr('data-src') 63 | }) 64 | var blob = new Blob([el.html()], {type: "text/html;charset=utf-8"}) 65 | // saveAs(blob, book.title + '.html') 66 | FileSystemService.save(blob, book.title, 'html', function(is_saved) { 67 | is_saved && $modalInstance.close() 68 | }) 69 | } 70 | } 71 | $scope.cancel = function () { 72 | $modalInstance.dismiss('cancel') 73 | } 74 | $scope.exports = exports 75 | $scope.exportFormat = 'markdown' 76 | $scope.changeExportFormat = function(mode) { 77 | $scope.exportFormat = mode 78 | } 79 | 80 | $scope.popupEvernote = function() { 81 | $modal.open({ 82 | templateUrl: 'modalEvernote.html', 83 | controller: 'EvernoteCtrl', 84 | resolve: { 85 | title: function() { 86 | return book.title 87 | } 88 | }, 89 | windowClass: 'mask-evernote' 90 | }) 91 | } 92 | 93 | } 94 | }) 95 | 96 | }) 97 | } 98 | }) 99 | 100 | }]) -------------------------------------------------------------------------------- /assets/js/controllers/booksCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('BooksCtrl', ['$scope', '$routeParams', '$modal', '$timeout', 'UserService', 'SerializeService', function($scope, $routeParams, $modal, $timeout, UserService, SerializeService) { 2 | 3 | // Save user data after login 4 | var uid = $routeParams.uid 5 | 6 | UserService.getUser(uid).then(function(user) { 7 | $scope.user = user 8 | $scope.$emit('page:change', 'books', (user.name || '我') + '的书架') 9 | // fetch books 10 | SerializeService.fetchAllBooks(user.id).then(function(books) { 11 | $scope.books = books 12 | $scope.tipVisible = !user.is_self && _.isEmpty(books) 13 | }) 14 | 15 | $scope.searchBook = function() { 16 | var modalInstance = $modal.open({ 17 | templateUrl: 'modalSearchBook.html', 18 | controller: 'SearchBookCtrl', 19 | resolve: { 20 | 'user': function() { 21 | return user 22 | } 23 | } 24 | }).opened.then(function() { 25 | $timeout(function() { 26 | // not that way angular 27 | var firstInput = $('.modal-dialog input[type="text"]') 28 | if (firstInput.length) { 29 | firstInput[0].focus() 30 | } 31 | }, 50) 32 | }) 33 | } 34 | }) 35 | 36 | }]).controller('SearchBookCtrl', ['$scope', '$modalInstance', '$location', '$http', 'BookService', 'user', function($scope, $modalInstance, $location, $http, BookService, user) { 37 | $scope.searchBook = function(query) { 38 | if (!query.match(/^\d*$/)) { // search by keyword 39 | $http.get('/api/v2/book/search', { 40 | params: { 41 | q: query 42 | } 43 | }).success(function(res) { 44 | $scope.books = res.books 45 | if (res.books.length) { 46 | $scope.selectedIndex = 0 47 | $scope.selectedBook = res.books[0] 48 | } 49 | }) 50 | } else { // search by subject id 51 | BookService.get(query).then(function(book) { 52 | $scope.selectedIndex = 0 53 | $scope.selectedBook = book 54 | $scope.books = [book] 55 | }) 56 | } 57 | $scope.has_searched = true 58 | } 59 | $scope.selectBook = function($index, book) { 60 | $scope.selectedIndex = $index 61 | $scope.selectedBook = book 62 | } 63 | $scope.ok = function (bid) { 64 | $location.path('/' + user.uid + '/book/' + bid +'/new') 65 | $modalInstance.close() 66 | } 67 | $scope.cancel = function () { 68 | $modalInstance.dismiss('cancel') 69 | } 70 | }]) -------------------------------------------------------------------------------- /assets/js/controllers/editorCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('EditorCtrl', ['$scope', '$routeParams', '$http', '$location', 'UserService', 'BookService', 'NoteService', 'TranslateService', function($scope, $routeParams, $http, $location, UserService, BookService, NoteService, TranslateService) { 2 | $scope.$emit('page:change', 'editor') 3 | 4 | UserService.login().then(function(user) { 5 | var note 6 | if ($routeParams.nid) { // update note 7 | var nid = $routeParams.nid 8 | 9 | $scope.action = 'put' 10 | 11 | $scope.note = NoteService.get({ 12 | id: nid 13 | }).success(function(resp) { 14 | note = _.pick(resp, 'id', 'chapter', 'page_no', 'privacy', 'content', 'book_id', 'photos', 'last_photo', 'book') 15 | note.page_no = note.page_no || null // api returns 0, turn it to null 16 | note.content = TranslateService.doubanToMarkdown(note.content, note.photos) 17 | note.privacy = note.privacy == '2'? 'public': 'private' 18 | $scope.note = note 19 | $scope.$emit('nav:lastBook', user, _.pick(resp.book, 'id', 'title')) 20 | }) 21 | } else if ($routeParams.bid) { // create new note 22 | $scope.note = { 23 | chapter: '', 24 | content: '', 25 | page_no: null, 26 | photos: {}, 27 | privacy: 'public' 28 | } 29 | $scope.action = 'post' 30 | BookService.get($routeParams.bid).then(function(book) { 31 | $scope.note.book = book 32 | $scope.$emit('nav:lastBook', user, book) 33 | }) 34 | } 35 | }) 36 | 37 | $scope.mode = 'column-mode' 38 | $scope.changeMode = function(mode) { 39 | $scope.mode = mode 40 | } 41 | 42 | $scope.is_fullscreen = 0 43 | $scope.fullscreen = function() { 44 | if ($scope.is_fullscreen) { 45 | chrome.app.window.current().restore() 46 | } else { 47 | chrome.app.window.current().fullscreen() 48 | } 49 | $scope.is_fullscreen ^= 1 50 | } 51 | 52 | $scope.images = [] 53 | $scope.save = function() { 54 | var payload = { 55 | content: TranslateService.markdownToDouban($scope.note.content), 56 | chapter: $scope.note.chapter, 57 | privacy: $scope.note.privacy 58 | } 59 | , last_photo = $scope.note.last_photo 60 | $scope.note.page_no && (payload['page'] = $scope.note.page_no) 61 | if ($scope.action == 'put') { 62 | NoteService.put({ 63 | id: $routeParams.nid 64 | }, payload, $scope.images).success(function(note) { 65 | $location.path('/note/' + note.id) 66 | }) 67 | } else if ($scope.action == 'post') { 68 | NoteService.post({ 69 | bid: $routeParams.bid 70 | }, payload, $scope.images).success(function(note) { 71 | $location.path('/note/' + note.id) 72 | }) 73 | } 74 | } 75 | 76 | }]) -------------------------------------------------------------------------------- /assets/js/controllers/evernoteCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('EvernoteCtrl', ['$scope', '$rootScope', '$modalInstance', 'EvernoteService', 'title', function($scope, $rootScope, $modalInstance, EvernoteService, title) { 2 | var STATUS = { 3 | 'NOTEBOOK': { 4 | 'PENDING': '正在获取Evernote笔记本列表...', 5 | 'SUCCESS': '', 6 | 'FAIL': '与Evernote服务器通信失败,请检查Internet是否正常。' 7 | }, 8 | 'NOTE': { 9 | 'PENDING': '正在保存', 10 | 'SUCCESS': '', 11 | 'FAIL': '保存失败,请检查Internet是否正常,或者关闭窗口重试一次。' 12 | } 13 | } 14 | $scope.status = STATUS.NOTEBOOK.PENDING 15 | EvernoteService.listNoteBooks(function(res) { 16 | $scope.notebooks = Array.prototype.slice.call(res, 0) // [{name: ,quid:}] 17 | $scope.selectedNotebook = $scope.notebooks[0] 18 | $scope.status = STATUS.NOTEBOOK.SUCCESS 19 | $scope.$apply() 20 | }, function() { 21 | $scope.status = STATUS.NOTEBOOK.FAIL 22 | $scope.$apply() 23 | }) 24 | $scope.choice_notebook = 'exist' 25 | 26 | function saveNoteTo(notebook) { 27 | EvernoteService.save(title 28 | , $('.content') 29 | , notebook.guid 30 | , function() { 31 | $rootScope.$broadcast('alert:success', '保存成功 :)') 32 | $modalInstance.close() 33 | } 34 | , function(msg) { 35 | $scope.status = STATUS.NOTE.FAIL + (msg && ('错误:' + msg + ' 告知开发者让他修复吧:)')) 36 | $scope.$apply() 37 | }) 38 | } 39 | $scope.ok = function(choice, existbook, newbook) { 40 | if (choice == 'exist') { 41 | saveNoteTo(existbook) 42 | } else if (choice == 'new') { 43 | EvernoteService.createNoteBook(newbook, function(newbook) { 44 | saveNoteTo(newbook) 45 | }) 46 | } 47 | } 48 | $scope.cancel = function() { 49 | $modalInstance.close() 50 | } 51 | }]) -------------------------------------------------------------------------------- /assets/js/controllers/favCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('FavCtrl', ['$scope', 'FavouriteService', function($scope, FavouriteService) { 2 | $scope.$emit('page:change', 'fav', '我收藏的笔记*') 3 | FavouriteService.fetchAll(function(notes) { 4 | $scope.notes = notes 5 | }) 6 | }]) -------------------------------------------------------------------------------- /assets/js/controllers/friendsCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('FriendsCtrl', ['$scope', '$http', 'FriendsService', function($scope, $http, FriendsService) { 2 | $scope.$emit('page:change', 'friends', '友邻的笔记') 3 | 4 | $scope.showMoreFriends = function() { 5 | FriendsService.getMore().then(function(friends) { 6 | $scope.and_more = FriendsService.has_more 7 | }) 8 | } 9 | 10 | $scope.friends = FriendsService.getAllLocal() 11 | $scope.and_more = FriendsService.has_more 12 | 13 | if (!$scope.friends.length) { 14 | $scope.showMoreFriends() 15 | } 16 | }]) -------------------------------------------------------------------------------- /assets/js/controllers/infoCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('InfoCtrl', ['$scope', 'AuthService', 'UserService', 'SerializeService', 'AnalyticsService', function($scope, AuthService, UserService, SerializeService, AnalyticsService) { 2 | $scope.$emit('page:change', 'info', '个人信息') 3 | 4 | UserService.getUserInfo().then(function(user) { 5 | $scope.user = user 6 | 7 | SerializeService.fetchAll(user.id).then(function(resp) { 8 | var books = resp[0] 9 | , notes = resp[1] 10 | 11 | $scope.total_books = _.size(books) 12 | var max = 0, max_id 13 | _.each(books, function(book, bid) { 14 | if (book.notes_count > max) { 15 | max = book.notes_count 16 | max_id = bid 17 | } 18 | }) 19 | $scope.max_book = books[max_id] 20 | 21 | $scope.total_notes = notes.length 22 | var start, start_bid, start_nid 23 | , end, end_id 24 | , time 25 | start = end = new Date(notes[0].time) 26 | start_bid = notes[0].book_id 27 | start_nid = notes[0].id 28 | _.each(notes, function(note, nid) { 29 | time = new Date(note.time) 30 | if (time < start) { 31 | start = time 32 | start_bid = note.book_id 33 | start_nid = nid 34 | } 35 | if (time > end) { 36 | end = time 37 | end_id = nid 38 | } 39 | }) 40 | $scope.start = start.toLocaleDateString() 41 | $scope.end = end.toLocaleDateString() 42 | $scope.start_book = books[start_bid] 43 | $scope.start_note = notes[start_nid] 44 | 45 | // find first five unique 46 | var note_group = _.groupBy(notes, function(note) { return note.book_id }) 47 | $scope.recent_books = _.map(_.sortBy(_.values(note_group), function(notes) {return new Date(notes[0].time) }).reverse().slice(0,5), function(notes) {return books[notes[0].book_id] }) 48 | 49 | }) 50 | }) 51 | 52 | AuthService.isEvernoteAuthed().then(function(type) { 53 | $scope[type + '_bound'] = true 54 | }) 55 | $scope.authEvernote = function(type) { 56 | // type: evernote or yinxiang 57 | AuthService.evernote(type, function() { 58 | $scope[type + '_bound'] = true 59 | $scope.$apply() 60 | }) 61 | } 62 | $scope.unbind = function(type) { 63 | AuthService.unbindEvernote(type, function() { 64 | $scope[type + '_bound'] = false 65 | $scope.$apply() 66 | }) 67 | } 68 | 69 | AnalyticsService.isTrackingPermitted().then(function(isTrackingPermitted) { 70 | $scope.analytics_allowed = isTrackingPermitted 71 | }) 72 | $scope.alter_analytics = function(isTrackingPermitted) { 73 | AnalyticsService.setTrackingPermission($scope.analytics_allowed) 74 | } 75 | }]) -------------------------------------------------------------------------------- /assets/js/controllers/loginCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('LoginCtrl', ['$scope', '$window', 'AuthService', function($scope, $window, AuthService) { 2 | $scope.reload = function() { 3 | $window.location.href = '/auth' 4 | } 5 | $scope.getToken = AuthService.auth 6 | }]) -------------------------------------------------------------------------------- /assets/js/controllers/module.js: -------------------------------------------------------------------------------- 1 | var annoCtrl = angular.module('ANNO.controllers', ['infinite-scroll']) 2 | -------------------------------------------------------------------------------- /assets/js/controllers/noteCtrl.js: -------------------------------------------------------------------------------- 1 | annoCtrl.controller('NoteCtrl', ['$scope', '$http', '$routeParams', '$modal', '$location', 'UserService', 'NoteService', 'TranslateService', 'FavouriteService', 'EvernoteService', function($scope, $http, $routeParams, $modal, $location, UserService, NoteService, TranslateService, FavouriteService, EvernoteService) { 2 | $scope.$emit('page:change', 'note') 3 | 4 | var note 5 | , nid = $routeParams.nid 6 | 7 | UserService.login().then(function(user) { 8 | NoteService.get({ 9 | id: nid 10 | }).success(function(resp) { 11 | note = _.pick(resp, 'id', 'chapter', 'page_no', 'privacy', 'summary', 'content', 'photos', 'last_photo') 12 | note.content = TranslateService.doubanToMarkdown(note.content, note.photos) 13 | $scope.note = note 14 | $scope.book = _.pick(resp.book, 'id', 'title') 15 | 16 | $scope.author = resp.author_user 17 | 18 | $scope.can_operate = (resp.author_id == user.id) 19 | 20 | $scope.$emit('nav:lastBook', $scope.author, $scope.book) 21 | 22 | FavouriteService.getState(note, function(state) { 23 | $scope.has_starred = state 24 | $scope.$apply() 25 | }) 26 | $scope.toggleStar = function(state) { 27 | FavouriteService[state? 'star': 'unstar'](_.omit(note, 'content'), function(state) { 28 | $scope.has_starred = state.is_starred 29 | $scope.$apply() 30 | }) 31 | } 32 | $scope.popupEvernote = function() { 33 | var modalInstance = $modal.open({ 34 | templateUrl: 'modalEvernote.html', 35 | controller: 'EvernoteCtrl', 36 | resolve: { 37 | title: function() { 38 | return (note.chapter || (book.title + '第' + note.page_no + '页')) 39 | } 40 | } 41 | }) 42 | } 43 | }).catch(function(error) { 44 | $scope.error = error 45 | }) 46 | 47 | }) 48 | function deleteNote(id, bid) { 49 | return NoteService.remove({ 50 | id: id, 51 | bid: $scope.book.id 52 | }).success(function() { 53 | $location.path('/' + $scope.user.uid + '/book/' + $scope.book.id) 54 | }) 55 | } 56 | $scope.open = function() { 57 | var modalInstance = $modal.open({ 58 | templateUrl: 'modalDeleteNote.html', 59 | controller: function($scope, $modalInstance) { 60 | $scope.ok = function () { 61 | deleteNote(nid) 62 | $modalInstance.close() 63 | } 64 | $scope.cancel = function () { 65 | $modalInstance.dismiss('cancel') 66 | } 67 | }, 68 | resolve: function() { 69 | } 70 | }) 71 | 72 | modalInstance.result.then(function(selectedItem){ 73 | $scope.selected = selectedItem 74 | }) 75 | } 76 | }]) -------------------------------------------------------------------------------- /assets/js/douban-auth.js: -------------------------------------------------------------------------------- 1 | var DoubanAuth = (function() { 2 | 'use strict' 3 | 4 | var conf = DoubanAuthConf 5 | , redirectURL = 'https://' + chrome.runtime.id + '.chromiumapp.org/auth/callback' 6 | , redirectRe = new RegExp(redirectURL + '[#\?](.*)') 7 | , access_token = null 8 | , userinfo 9 | 10 | return { 11 | getToken: function(callback) { 12 | if (access_token) { 13 | callback(null, access_token) 14 | return 15 | } 16 | var options = { 17 | interactive: true, 18 | url: 'https://www.douban.com/service/auth2/auth?client_id=' + conf.api_key + 19 | '&response_type=code' + 20 | '&redirect_uri=' + encodeURIComponent(redirectURL) + 21 | '&scope=' + conf.scopes 22 | } 23 | chrome.identity.launchWebAuthFlow(options, function(redirectUri) { 24 | console.log('launchWebAuthFlow completed', chrome.runtime.lastError, redirectUri) 25 | if (chrome.runtime.lastError) { 26 | callback(new Error(chrome.runtime.lastError)) 27 | return 28 | } 29 | var matches = redirectUri.match(redirectRe) 30 | if (matches && matches.length > 1) 31 | handleProviderResponse(parseRedirectFragment(matches[1])) 32 | else 33 | callback(new Error('Invalid redirect URI')) 34 | 35 | }) 36 | function parseRedirectFragment(fragment) { 37 | var pairs = fragment.split(/&/); 38 | var values = {}; 39 | 40 | pairs.forEach(function(pair) { 41 | var nameval = pair.split(/=/); 42 | values[nameval[0]] = nameval[1]; 43 | }); 44 | 45 | return values; 46 | } 47 | 48 | function handleProviderResponse(values) { 49 | console.log('providerResponse', values); 50 | if (values.hasOwnProperty('access_token')) 51 | setAccessToken(values.access_token); 52 | // If response does not have an access_token, it might have the code, 53 | // which can be used in exchange for token. 54 | else if (values.hasOwnProperty('code')) 55 | exchangeCodeForToken(values.code); 56 | else 57 | callback(new Error('Neither access_token nor code avialable.')); 58 | } 59 | 60 | function exchangeCodeForToken(code) { 61 | var xhr = new XMLHttpRequest(); 62 | xhr.open('POST', 63 | 'https://www.douban.com/service/auth2/token?' + 64 | 'client_id=' + conf.api_key + 65 | '&client_secret=' + conf.api_secret + 66 | '&redirect_uri=' + redirectURL + 67 | '&grant_type=authorization_code' + 68 | '&code=' + code); 69 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 70 | xhr.setRequestHeader('Accept', 'application/json'); 71 | xhr.onload = function () { 72 | // When exchanging code for token, the response comes as json, which 73 | // can be easily parsed to an object. 74 | if (this.status === 200) { 75 | var response = JSON.parse(this.responseText); 76 | console.log(response); 77 | if (response.hasOwnProperty('access_token')) { 78 | setAccessToken(response.access_token); 79 | } else { 80 | callback(new Error('Cannot obtain access_token from code.')); 81 | } 82 | } else { 83 | console.log('code exchange status:', this.status); 84 | callback(new Error('Code exchange failed')); 85 | } 86 | }; 87 | xhr.send(); 88 | } 89 | function setAccessToken(token) { 90 | access_token = token; 91 | console.log('Setting access_token: ', access_token); 92 | callback(null, access_token); 93 | } 94 | 95 | } 96 | } 97 | 98 | })() -------------------------------------------------------------------------------- /assets/js/enml.js: -------------------------------------------------------------------------------- 1 | // require $ alias of angular.element 2 | // todo: put this to translateService... 3 | // extract style 4 | // html to xhmlt
to
, hr to
, img to 5 | var enml = (function() { 6 | var EXTRACT_RULE = { 7 | 'blockquote': ['border-left'], 8 | 'span': ['color'], 9 | 'code': ['border', 'background-color'], 10 | 'pre': ['background-color', 'margin'] 11 | } 12 | 13 | function html2enml(node) { 14 | var $$ = angular.element 15 | var clone = node.cloneNode(true) 16 | , process = $$('
') 17 | , nodeClassName = $(node).attr('class').split(' ')[0] 18 | , result 19 | $('body').append(process) 20 | process = $('#process') 21 | process.append($(clone)) 22 | nodewalk(clone.childNodes, process_node) 23 | 24 | result = $('#process .' + nodeClassName).html() 25 | .replace(/
/g, '
') 26 | .replace(/
/g, '
') 27 | .replace(//g, '') 28 | process.remove() 29 | return result 30 | } 31 | 32 | function nodewalk(node, fn) { 33 | for (var i = 0; i < node.length; i++) { 34 | if (node[i].hasChildNodes()) { // todo: filter by node.nodeName 35 | nodewalk(node[i].childNodes, fn) 36 | } 37 | if (1 === node[i].nodeType) { // element 38 | fn(node[i]) 39 | } 40 | } 41 | } 42 | 43 | function process_node(node) { 44 | if (node.nodeName == "IMG") { 45 | node.removeAttribute('src') 46 | node.removeAttribute('alt') 47 | } 48 | style_inline(node) 49 | node.removeAttribute('class') 50 | node.removeAttribute('id') 51 | } 52 | 53 | // example: style="color: red; background: green;" 54 | function style_inline(node) { 55 | var nodename = node.nodeName.toLowerCase() 56 | , rules 57 | , str_style 58 | if (nodename in EXTRACT_RULE) { 59 | rules = EXTRACT_RULE[nodename] 60 | str_style = '' 61 | for(var i = 0, l = rules.length, rule; i < l; i++) { 62 | rule = rules[i] 63 | str_style += rule + ':' + 64 | getComputedStyle(node).getPropertyValue(rule) + ';' 65 | } 66 | node.setAttribute('style', str_style) 67 | } 68 | } 69 | return { 70 | html2enml: html2enml 71 | } 72 | })() 73 | -------------------------------------------------------------------------------- /assets/js/evernote-auth.js: -------------------------------------------------------------------------------- 1 | var EvernoteAuth = (function() { 2 | 'use strict' 3 | 4 | var conf = EvernoteAuthConf 5 | , redirectURL = 'https://' + chrome.runtime.id + '.chromiumapp.org/auth/callback' 6 | , redirectRe = new RegExp(redirectURL + '[#\?](.*)') 7 | , access_token = null 8 | , userinfo 9 | 10 | return { 11 | getToken: function(type, callback) { 12 | if (access_token) { 13 | callback(null, access_token) 14 | return 15 | } 16 | var options = { 17 | interactive: true, 18 | url: conf.hostname[type] + '/oauth?oauth_consumer_key=' + conf.consumer_key + 19 | '&oauth_signature=' + conf.consumer_secret + "%26" + 20 | '&oauth_signature_method=PLAINTEXT' + 21 | '&oauth_timestamp=' + (new Date).valueOf() + 22 | '&oauth_nonce' + (new Date).valueOf() + 23 | '&oauth_callback=' + encodeURIComponent(redirectURL) 24 | } 25 | // oauth step1: get temporary credential 26 | var xhr = new XMLHttpRequest() 27 | xhr.open('GET', options.url) 28 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') 29 | xhr.onload = function() { 30 | if (this.status === 200) { 31 | var resp = parseRedirectFragment(this.responseText) 32 | console.log(resp) 33 | if (!resp.oauth_callback_confirmed) return 34 | chrome.identity.launchWebAuthFlow({ 35 | interactive: true, 36 | url: conf.hostname[type] + '/OAuth.action?oauth_token=' + resp.oauth_token 37 | }, function(redirectUri) { 38 | console.log('launchWebAuthFlow completed', chrome.runtime.lastError, redirectUri) 39 | if (chrome.runtime.lastError) { 40 | callback(new Error(chrome.runtime.lastError)) 41 | return 42 | } 43 | var matches = redirectUri.match(redirectRe) 44 | if (matches && matches.length > 1) 45 | handleProviderResponse(resp, parseRedirectFragment(redirectUri)) 46 | else 47 | callback(new Error('Invalid redirect URI')) 48 | }) 49 | } 50 | } 51 | xhr.send() 52 | 53 | function parseRedirectFragment(fragment, options) { 54 | var pairs = fragment.split(/&/); 55 | var values = {}; 56 | 57 | pairs.forEach(function(pair) { 58 | var nameval = pair.split(/=/); 59 | if (options && options.decode) { 60 | values[nameval[0]] = decodeURIComponent(nameval[1]); 61 | } else { 62 | values[nameval[0]] = nameval[1]; 63 | } 64 | }); 65 | 66 | return values; 67 | } 68 | 69 | function handleProviderResponse(values, values2) { 70 | console.log('providerResponse', values); 71 | if (values.hasOwnProperty('access_token')) 72 | setAccessToken(values.access_token); 73 | // oauth step2: exhange temporary credentials for token 74 | else if (values.hasOwnProperty('oauth_token')) 75 | exchangeCodeForToken(values.oauth_token, values2.oauth_verifier); 76 | else 77 | callback(new Error('Neither access_token nor code avialable.')); 78 | } 79 | 80 | function exchangeCodeForToken(token, verifier) { 81 | var xhr = new XMLHttpRequest(); 82 | xhr.open('POST', 83 | conf.hostname[type] + '/oauth?' + 84 | 'oauth_consumer_key=' + conf.consumer_key + 85 | '&oauth_signature=' + conf.consumer_secret + "%26" + 86 | '&oauth_signature_method=PLAINTEXT' + 87 | '&oauth_timestamp=' + (new Date).valueOf() + 88 | '&oauth_nonce=' + (new Date).valueOf() + 89 | '&oauth_token=' + token + 90 | '&oauth_verifier=' + verifier); 91 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 92 | xhr.onload = function () { 93 | if (this.status === 200) { 94 | // var response = JSON.parse(this.responseText); 95 | var response = parseRedirectFragment(this.responseText, {decode: true}); 96 | console.log(response); 97 | if (response.hasOwnProperty('oauth_token')) { 98 | callback(null, response) 99 | // setAccessToken(response.oauth_token); 100 | // save notestore url 101 | // save(decodeURIComponent(response.edam_noteStoreUrl)) 102 | } else { 103 | callback(new Error('Cannot obtain access_token from code.')); 104 | } 105 | } else { 106 | console.log('code exchange status:', this.status); 107 | callback(new Error('Code exchange failed')); 108 | } 109 | }; 110 | xhr.send(); 111 | } 112 | // function setAccessToken(token) { 113 | // access_token = token; 114 | // console.log('Setting access_token: ', access_token); 115 | // callback(null, access_token); 116 | // } 117 | 118 | } 119 | } 120 | 121 | })() -------------------------------------------------------------------------------- /assets/js/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | angular.module('ANNO') 4 | .filter('characters', function() { 5 | return function(input, chars) { 6 | if (input && input.length >= chars) { 7 | input = input.substring(0, chars) 8 | 9 | var lastspace = input.lastIndexOf(' ') 10 | if (lastspace !== -1) { 11 | input = input.substr(0, lastspace) 12 | } 13 | return input + '...' 14 | } 15 | return input 16 | } 17 | }) -------------------------------------------------------------------------------- /assets/js/services/analyticsService.js: -------------------------------------------------------------------------------- 1 | app.factory('AnalyticsService', ['$q', function($q) { 2 | var service = analytics.getService(GAConf.service) 3 | , tracker = service.getTracker(GAConf.tracker) 4 | 5 | var configDefer = new $q.defer() 6 | service.getConfig().addCallback(function(config) { 7 | configDefer.resolve(config) 8 | }) 9 | return { 10 | isTrackingPermitted: function() { 11 | var defer = new $q.defer() 12 | configDefer.promise.then(function(config) { 13 | defer.resolve(config.isTrackingPermitted()) 14 | }) 15 | return defer.promise 16 | }, 17 | setTrackingPermission: function(isTrackingPermitted) { 18 | configDefer.promise.then(function(config) { 19 | config.setTrackingPermitted(isTrackingPermitted) 20 | }) 21 | }, 22 | sendAppView: function(view) { 23 | tracker.sendEvent('ChangeView', 'Change', view) 24 | }, 25 | reportError: function(error) { 26 | tracker.sendEvent('Error', String(error.id), error.msg) 27 | } 28 | } 29 | }]) -------------------------------------------------------------------------------- /assets/js/services/authService.js: -------------------------------------------------------------------------------- 1 | app.factory('AuthService', ['$rootScope', '$http', '$q', '$location', function($rootScope, $http, $q, $location) { 2 | return { 3 | auth: function() { 4 | DoubanAuth.getToken(function(error, access_token) { 5 | if (error) return 6 | $http({ 7 | method: 'GET', 8 | url: 'https://api.douban.com/v2/user/~me', 9 | headers: { 10 | 'Authorization': 'Bearer ' + access_token 11 | } 12 | }).success(function(resp) { 13 | chrome.storage.local.set({'logintoken': { 14 | id: resp.id, 15 | uid: resp.uid, 16 | avatar: resp.large_avatar, 17 | token: access_token 18 | }}) 19 | $location.path('/') 20 | }) 21 | 22 | }) 23 | }, 24 | evernote: function(type, callback) { 25 | EvernoteAuth.getToken(type, function(error, res) { 26 | if (!res.oauth_token) { 27 | return 28 | } 29 | if ($rootScope.user) { 30 | // set and save 31 | var token = {} 32 | token[type] = res // type: evernote or yinxiang 33 | chrome.storage.local.set({'logintoken': _.extend($rootScope.user, token)}) 34 | } 35 | callback && callback() 36 | }) 37 | }, 38 | unbindEvernote: function(type, callback) { 39 | if ($rootScope.user[type]) { 40 | delete $rootScope.user[type] 41 | } 42 | chrome.storage.local.set({ 'logintoken': $rootScope.user }, callback) 43 | }, 44 | isEvernoteAuthed: function() { 45 | var defer = new $q.defer() 46 | , valid = function(conf) { 47 | return (conf && conf.oauth_token && (new Date) < conf.edam_expires) 48 | } 49 | chrome.storage.local.get('logintoken', function(res) { 50 | var evernote = res.logintoken.evernote 51 | , yinxiang = res.logintoken.yinxiang 52 | if (valid(evernote)) { 53 | defer.resolve('evernote') 54 | } else if (valid(yinxiang)) { 55 | defer.resolve('yinxiang') 56 | } else { 57 | defer.reject() 58 | } 59 | }) 60 | return defer.promise 61 | } 62 | } 63 | }]) -------------------------------------------------------------------------------- /assets/js/services/bookService.js: -------------------------------------------------------------------------------- 1 | app.factory('BookService', ['$q', '$http', '$rootScope', function($q, $http, $rootScope) { 2 | // fetch book from sessionStorage or from $http 3 | return { 4 | get: function(bid) { 5 | var user = $rootScope.user 6 | , id = user.id 7 | 8 | var defer = $q.defer() 9 | var books = sessionStorage.getItem(id + '_books') 10 | , book 11 | if (books) { 12 | books = JSON.parse(books) 13 | book = books[bid] 14 | } 15 | if (!book) { 16 | $http.get('/api/v2/book/' + bid).success(function(book) { 17 | defer.resolve(_.pick(book, 'id', 'title', 'author', 'images', 'summary')) 18 | }) 19 | } else { 20 | defer.resolve(book) 21 | } 22 | return defer.promise 23 | } 24 | } 25 | }]) -------------------------------------------------------------------------------- /assets/js/services/evernoteService.js: -------------------------------------------------------------------------------- 1 | app.factory('EvernoteService', ['$rootScope', function($rootScope) { 2 | var user = $rootScope.user 3 | , conf = user.evernote || user.yinxiang 4 | 5 | if (!conf) return {} 6 | 7 | var noteStoreURL = conf.edam_noteStoreUrl 8 | , authenticationToken = conf.oauth_token 9 | , noteStoreTransport = new Thrift.BinaryHttpTransport(noteStoreURL) 10 | , noteStoreProtocol = new Thrift.BinaryProtocol(noteStoreTransport) 11 | , noteStore = new NoteStoreClient(noteStoreProtocol) 12 | return { 13 | listNoteBooks: function(done, fail) { 14 | noteStore.listNotebooks(authenticationToken, function (notebooks) { 15 | done && done(notebooks) 16 | }, function onerror(error) { 17 | fail(error) 18 | }) 19 | }, 20 | createNoteBook: function(name, callback) { 21 | this.listNoteBooks(function(notebooks) { 22 | var notebook = _.filter(notebooks, function(nb) { 23 | return nb.name == name 24 | }) 25 | if (!notebook.length) { 26 | var nnb = new Notebook() 27 | nnb.name = name 28 | noteStore.createNotebook(authenticationToken, nnb, function(res) { 29 | callback && callback(res) 30 | }) 31 | } else { 32 | callback && callback(notebook[0]) 33 | } 34 | }) 35 | }, 36 | save: function(title, content_node, notebook_id, done, fail) { 37 | var note = new Note 38 | note.title = title 39 | note.content = '' + enml.html2enml(content_node[0]) + '' 40 | note.notebookGuid = notebook_id 41 | noteStore.createNote(authenticationToken, note, function(res) { 42 | if (res.errorCode) { 43 | fail && fail(res.parameter) 44 | } else { 45 | done && done() 46 | } 47 | }) 48 | } 49 | } 50 | }]) -------------------------------------------------------------------------------- /assets/js/services/favouriteService.js: -------------------------------------------------------------------------------- 1 | app.factory('FavouriteService', ['$rootScope', function($rootScope) { 2 | // use chrome.storage.sync to save data 3 | var storage = chrome.storage.sync 4 | , STORAGE_STAR = $rootScope.user.id + '_favourites' 5 | return { 6 | getState: function(note, cb) { 7 | this.fetchAll(function(favourites) { 8 | var is_faved = false 9 | _.each(favourites, function(n) { 10 | if (n.id == note.id) { 11 | is_faved = true 12 | } 13 | }) 14 | cb(is_faved) 15 | }) 16 | }, 17 | star: function(note, cb) { 18 | this.fetchAll(function(favourites) { 19 | favourites.push(note) 20 | var item = {} 21 | item[STORAGE_STAR] = favourites 22 | storage.set(item, function() { 23 | cb({ is_starred: true }) 24 | }) 25 | }) 26 | }, 27 | unstar: function(note, cb) { 28 | this.fetchAll(function(favourites) { 29 | favourites = favourites.filter(function(n) { 30 | return n.id != note.id 31 | }) 32 | var item = {} 33 | item[STORAGE_STAR] = favourites 34 | storage.set(item, function() { 35 | cb({ is_starred: false }) 36 | }) 37 | }) 38 | }, 39 | fetchAll: function(callback) { 40 | storage.get( STORAGE_STAR , function(resp) { 41 | if (resp && resp[STORAGE_STAR]) { 42 | callback(resp[STORAGE_STAR]) 43 | } else { 44 | callback([]) 45 | } 46 | }) 47 | } 48 | } 49 | }]) -------------------------------------------------------------------------------- /assets/js/services/filesystemService.js: -------------------------------------------------------------------------------- 1 | // for packaged app 2 | app.factory('FileSystemService', [function() { 3 | function errorHandler(e) { 4 | console.error(e); 5 | } 6 | function waitForIO(writer, callback) { 7 | // set a watchdog to avoid eventual locking: 8 | var start = Date.now(); 9 | // wait for a few seconds 10 | var reentrant = function() { 11 | if (writer.readyState===writer.WRITING && Date.now()-start<4000) { 12 | setTimeout(reentrant, 100); 13 | return; 14 | } 15 | if (writer.readyState===writer.WRITING) { 16 | console.error("Write operation taking too long, aborting!"+ 17 | " (current writer readyState is "+writer.readyState+")"); 18 | writer.abort(); 19 | } 20 | else { 21 | callback(); 22 | } 23 | }; 24 | setTimeout(reentrant, 100); 25 | } 26 | function writeEntry(writableEntry, opt_blob, callback) { 27 | 28 | writableEntry.createWriter(function(writer) { 29 | 30 | writer.onerror = errorHandler; 31 | writer.onwriteend = callback; 32 | 33 | // If we have data, write it to the file. Otherwise, just use the file we 34 | // loaded. 35 | if (opt_blob) { 36 | writer.truncate(opt_blob.size); 37 | waitForIO(writer, function() { 38 | writer.seek(0); 39 | writer.write(opt_blob); 40 | }); 41 | } 42 | }, errorHandler); 43 | } 44 | return { 45 | 'save': function(blob, filename, extension, callback) { 46 | chrome.fileSystem.chooseEntry({ 47 | type: 'saveFile', 48 | suggestedName: filename, 49 | accepts: [{ 50 | extensions: [extension] 51 | }] 52 | }, function(writableEntry) { 53 | if (!writableEntry) { 54 | callback(false) 55 | return 56 | } 57 | writeEntry(writableEntry, blob, function() { 58 | callback(true) 59 | }) 60 | }) 61 | } 62 | } 63 | }]) -------------------------------------------------------------------------------- /assets/js/services/friendsService.js: -------------------------------------------------------------------------------- 1 | app.factory('FriendsService', ['$q', '$http', '$rootScope', function($q, $http, $rootScope) { 2 | 3 | var localData 4 | , start = 0 5 | , count = 50 6 | , has_more = true 7 | 8 | function getAllLocal() { 9 | if (!localData) { 10 | var id = $rootScope.user.id 11 | localData = sessionStorage.getItem(id + '_friends') 12 | localData = JSON.parse(localData) 13 | localData = localData || [] 14 | } 15 | return localData 16 | } 17 | 18 | function fetchFriendsList(start, count) { 19 | var defer = $q.defer() 20 | , user = $rootScope.user 21 | $http({ 22 | method: 'GET', 23 | url: '/api/shuo/v2/users/' + user.uid + '/following', 24 | params: { 25 | start: start, 26 | count: count 27 | }, 28 | cache: true 29 | }) 30 | .success(function(resp) { 31 | Array.prototype.push.apply(localData, _.filter(resp, function(u) { return u.type == 'user' })) 32 | if (!resp.length) { 33 | has_more = false 34 | } 35 | defer.resolve(localData) 36 | }) 37 | .error(defer.reject) 38 | 39 | return defer.promise 40 | } 41 | 42 | return { 43 | friends: localData, 44 | has_more: has_more, 45 | getAllLocal: getAllLocal, 46 | getMore: function() { 47 | var promise = fetchFriendsList(start, count) 48 | start += count 49 | return promise 50 | } 51 | } 52 | }]) -------------------------------------------------------------------------------- /assets/js/services/httpLoadingIntercepter.js: -------------------------------------------------------------------------------- 1 | app.factory('HttpLoadingIntercepter', ['$q', '$rootScope', function($q, $rootScope) { 2 | var n_loading = 0 3 | function isSerious(rejection) { 4 | if (rejection.data.type === 'text/html') { 5 | return false 6 | } 7 | return true 8 | } 9 | 10 | return { 11 | 'request': function(config) { 12 | n_loading++ 13 | $rootScope.$broadcast('loading:show') 14 | return config || $q.when(config) 15 | }, 16 | 'requestError': function(rejection) { 17 | n_loading-- 18 | return $q.reject(rejection) 19 | }, 20 | 'response': function(response) { 21 | if (!(--n_loading)) { 22 | $rootScope.$broadcast('loading:hide') 23 | } 24 | return response || $q.when(response) 25 | }, 26 | 'responseError': function(rejection) { 27 | if (isSerious(rejection)) { 28 | $rootScope.$broadcast('alert:error', rejection.data) 29 | } 30 | if (!(--n_loading)) { 31 | $rootScope.$broadcast('loading:hide') 32 | } 33 | return $q.reject(rejection) 34 | } 35 | } 36 | }]) -------------------------------------------------------------------------------- /assets/js/services/httpOAuthIntercepter.js: -------------------------------------------------------------------------------- 1 | app.factory('HttpOAuthIntercepter', ['$q', '$rootScope', function($q, $rootScope) { 2 | return { 3 | request: function(req) { 4 | if (req.url.indexOf('/api') == 0) { // api.douban.com 5 | req.headers['Authorization'] = 'Bearer ' + $rootScope.user.token 6 | req.url = 'https://api.douban.com' + req.url.slice(4) 7 | } 8 | return req 9 | } 10 | } 11 | }]) -------------------------------------------------------------------------------- /assets/js/services/noteService.js: -------------------------------------------------------------------------------- 1 | app.factory('NoteService', ['$http', '$rootScope', 'UserService', 'BookService', function($http, $rootScope, UserService, BookService) { 2 | var user = $rootScope.user 3 | , id = user.id 4 | , STORAGE_BOOK = id + '_books' 5 | , STORAGE_NOTE = id + '_notes' 6 | var buildFormData = function(payload, files) { 7 | var formData = new FormData() 8 | _.each(payload, function(val, key) { 9 | formData.append(key, val) 10 | }) 11 | _.each(files, function(file) { 12 | formData.append(file.name, file.obj) 13 | }) 14 | return formData 15 | } 16 | 17 | function increaseNoteCount(bid, delta) { 18 | delta = delta || 1 19 | var books = JSON.parse(sessionStorage.getItem(STORAGE_BOOK)) 20 | , book = books[bid] 21 | if (book) { 22 | book.notes_count += delta 23 | sessionStorage.setItem(STORAGE_BOOK, JSON.stringify(books)) 24 | } else { // add a new book 25 | BookService.get(bid).then(function(book) { 26 | book.notes_count = 1 27 | books[bid] = book 28 | sessionStorage.setItem(STORAGE_BOOK, JSON.stringify(books)) 29 | }) 30 | } 31 | } 32 | return { 33 | get: function(params) { 34 | return $http.get('/api/v2/book/annotation/' + params.id) 35 | }, 36 | put: function(params, payload, photos) { 37 | var formData = buildFormData(payload, photos) 38 | return $http.put('/api/v2/book/annotation/' + params.id, formData, { 39 | transformRequest: angular.identity, 40 | headers: {'Content-Type': undefined} 41 | }).success(function(note) { 42 | var notes = sessionStorage.getItem(STORAGE_NOTE) 43 | if (notes) { 44 | notes = JSON.parse(notes) 45 | notes.forEach(function(n, i) { 46 | if (n.id == note.id) { 47 | notes[i] = note 48 | } 49 | }) 50 | sessionStorage.setItem(STORAGE_NOTE, JSON.stringify(notes)) 51 | } 52 | }) 53 | }, 54 | post: function(params, payload, photos) { 55 | var formData = buildFormData(payload, photos) 56 | return $http.post('/api/v2/book/' + params.bid + '/annotations', formData, { 57 | transformRequest: angular.identity, 58 | headers: {'Content-Type': undefined} 59 | }).success(function(note) { 60 | var notes = sessionStorage.getItem(STORAGE_NOTE) 61 | if (notes) { 62 | notes = JSON.parse(notes) 63 | notes.push(note) 64 | sessionStorage.setItem(STORAGE_NOTE, JSON.stringify(notes)) 65 | 66 | increaseNoteCount(note.book_id) 67 | } 68 | }) 69 | }, 70 | remove: function(params) { 71 | var id = params.id 72 | , bid = params.bid 73 | return $http.delete('/api/v2/book/annotation/' + id).success(function() { 74 | var notes = sessionStorage.getItem(STORAGE_NOTE) 75 | if (notes) { 76 | notes = JSON.parse(notes) 77 | notes = notes.filter(function(note) { 78 | return note.id != id 79 | }) 80 | sessionStorage.setItem(STORAGE_NOTE, JSON.stringify(notes)) 81 | 82 | increaseNoteCount(bid, -1) 83 | } 84 | }) 85 | } 86 | } 87 | }]) -------------------------------------------------------------------------------- /assets/js/services/serializeService.js: -------------------------------------------------------------------------------- 1 | app.factory('SerializeService', function($q, $http, $rootScope, BookService) { 2 | function sync(url, start, count, callback) { 3 | sync.books = sync.books || {} 4 | sync.notes = sync.notes || [] 5 | $http({ 6 | method: 'GET', 7 | url: url, 8 | params: { 9 | start: start, 10 | count: count 11 | } 12 | }).success(function(resp) { 13 | 14 | var annotations = resp.annotations 15 | , books = sync.books 16 | , notes = sync.notes 17 | , book 18 | annotations.forEach(function(note) { 19 | notes.push(_.pick(note, 'id', 'book_id', 'chapter', 'time', 'summary', 'content', 'privacy', 'page_no', 'last_photo', 'photos')) 20 | book = _.pick(note.book, 'id', 'title', 'author', 'images', 'summary') 21 | if (!books[book.id]) { 22 | books[book.id] = _.extend(book, { 23 | notes_count: 1 24 | }) 25 | } else { 26 | books[book.id].notes_count += 1 27 | } 28 | }) 29 | if (annotations.length) sync(url, start+count, count, callback) 30 | else callback() 31 | }) 32 | } 33 | return { 34 | serialize: function(id, callback) { 35 | sync.books = {} 36 | sync.notes = [] 37 | sync('/api/v2/book/user/' + id + '/annotations', 0, 20, function() { 38 | sessionStorage.setItem(id+ '_books', JSON.stringify(sync.books)) 39 | sessionStorage.setItem(id+ '_notes', JSON.stringify(sync.notes)) 40 | callback(sync.books, sync.notes) 41 | }) 42 | }, 43 | fetchAllBooks: function(id) { 44 | var defer = new $q.defer() 45 | , books = sessionStorage.getItem(id+ '_books') 46 | if (books) { 47 | books = JSON.parse(books) 48 | defer.resolve(books) 49 | } else { 50 | this.serialize(id, function(books) { 51 | defer.resolve(books) 52 | }) 53 | } 54 | return defer.promise 55 | }, 56 | fetchBook: function(uid, bid) { 57 | var defer = new $q.defer() 58 | , books = sessionStorage.getItem(uid+ '_books') 59 | , book 60 | , notes 61 | if (books) { 62 | books = JSON.parse(books) 63 | book = books[bid] 64 | } 65 | if (book) { // book exist in sessionStorage 66 | notes = JSON.parse(sessionStorage.getItem(uid+ '_notes')) 67 | notes = notes.filter(function(note) { 68 | return note.book_id == bid 69 | }) 70 | book.notes = notes 71 | defer.resolve(book) 72 | } else { 73 | this.serialize(uid, function(books, notes) { 74 | book = books[bid] 75 | if (book) { // book exist in user's library 76 | book.notes = notes.filter(function(note) { 77 | return note.book_id == book.id 78 | }) 79 | defer.resolve(book) 80 | } else { // new book, never write notes for it 81 | BookService.get(bid).then(function(book) { 82 | book.notes_count = 0 83 | book.notes = [] 84 | defer.resolve(book) 85 | }) 86 | } 87 | }) 88 | } 89 | return defer.promise 90 | }, 91 | fetchAll: function(uid) { 92 | var defer = new $q.defer() 93 | , books = sessionStorage.getItem(uid+ '_books') 94 | , notes = sessionStorage.getItem(uid+ '_notes') 95 | if (books && notes) { 96 | books = JSON.parse(books) 97 | notes = JSON.parse(notes) 98 | defer.resolve([books, notes]) 99 | } else { 100 | this.serialize(uid, function(books, notes) { 101 | defer.resolve([books, notes]) 102 | }) 103 | } 104 | return defer.promise 105 | } 106 | } 107 | 108 | }) -------------------------------------------------------------------------------- /assets/js/services/translateService.js: -------------------------------------------------------------------------------- 1 | app.factory('TranslateService', function() { 2 | var g_html_blocks = [] 3 | var escapeCharacters = function(text, charsToEscape, afterBackslash) { 4 | var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g,"\\$1") + "])" 5 | if (afterBackslash) { 6 | regexString = "\\\\" + regexString 7 | } 8 | var regex = new RegExp(regexString,"g") 9 | text = text.replace(regex,escapeCharacters_callback) 10 | return text 11 | } 12 | var escapeCharacters_callback = function(wholeMatch,m1) { 13 | var charCodeToEscape = m1.charCodeAt(0) 14 | return "~E"+charCodeToEscape+"E" 15 | } 16 | var _EncodeCode = function(text) { 17 | text = text.replace(/&/g,"&") 18 | text = text.replace(//g,">") 20 | text = escapeCharacters(text,"\*_{}[]\\",false) 21 | return text 22 | } 23 | var _Detab = function(text) { 24 | text = text.replace(/\t(?=\t)/g," ") 25 | text = text.replace(/\t/g,"~A~B") 26 | text = text.replace(/~B(.+?)~A/g, function(wholeMatch,m1,m2) { 27 | var leadingText = m1 28 | var numSpaces = 4 - leadingText.length % 4 29 | for (var i=0; i/g, function(_, num) { 47 | return '![图片' + num + '](' + images[num] + ')' 48 | }) 49 | } 50 | 51 | return text 52 | .replace(/\<代码开始 lang=\"(.*)\"\>(\n{0,1})/g, '```$1\n') 53 | .replace(/\<\/代码结束\>/g, '```') 54 | .replace(/\[code:(.*)\]/g, '```$1') 55 | .replace(/\[\/code\]/g, '```') 56 | .replace(/\<原文开始\>([\s\S]*?)\<\/原文结束\>/g, function(wholeMatch, m1) { 57 | var bq = m1 58 | // remove leading and trailing newlines 59 | bq = bq.replace(/^\n/g, "") 60 | //bq = bq.replace(/\n$/g, "") 61 | bq = bq.replace(/^([^\n|^\s])/gm, '> $1') 62 | return bq 63 | }) 64 | }, 65 | markdownToDouban: function(text) { 66 | text += '\n' 67 | return text.replace(/(^|\n)```(.*)\n([\s\S]*?)\n```/g, function(wholeMatch, m0, m1, m2) { 68 | var blank = m0 69 | , language = m1 70 | , codeblock = m2 71 | 72 | // codeblock = _EncodeCode(codeblock) 73 | codeblock = _Detab(codeblock) 74 | codeblock = codeblock.replace(/^\n+/g,"") // trim leading newlines 75 | codeblock = codeblock.replace(/\n+$/g,"") // trim trailing whitespace 76 | 77 | codeblock = blank + "<代码开始" + (language ? " lang=\"" + language + '"' : "") + ">\n" + codeblock + "\n" 78 | 79 | return codeblock 80 | }).replace(/\!\[图片(\d*)\]\((.*)\)/g, '<图片$1>') 81 | .replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, function(wholeMatch, m1) { 82 | var bq = m1 83 | bq = bq.replace(/^[ \t]*>[ \t]?/gm,"~0") 84 | bq = bq.replace(/~0/g, "") 85 | bq = bq.replace(/^[ \t]+$/gm,"") 86 | bq = bq.replace(/\n{1,}$/, '') 87 | 88 | return "<原文开始>\n" + bq + "\n\n" 89 | }).replace(/\n$/g, '') 90 | } 91 | } 92 | }) -------------------------------------------------------------------------------- /assets/js/services/userService.js: -------------------------------------------------------------------------------- 1 | app.factory('UserService', function($rootScope, $http, $q) { 2 | return { 3 | isLoggedIn: function() { 4 | var defer = new $q.defer() 5 | chrome.storage.local.get('logintoken', function(resp) { 6 | if ((!_.isEmpty(resp)) && resp.logintoken) { 7 | defer.resolve() 8 | } else { 9 | defer.reject() 10 | } 11 | }) 12 | return defer.promise 13 | }, 14 | // isLoggedIn: !!chrome.storage.local.get('logintoken'), 15 | getUser: function(uid) { 16 | var defer = new $q.defer() 17 | , self = this 18 | this.login().then(function(user) { 19 | uid = uid || user.uid 20 | if (user.uid == uid) { 21 | user.is_self = true 22 | defer.resolve(user) 23 | } else { 24 | self.getUserInfo(uid).then(function(user) { 25 | user.is_self = false 26 | defer.resolve(_.pick(user, 'id', 'uid', 'name')) 27 | }) 28 | } 29 | }, function() { 30 | defer.reject() 31 | }) 32 | return defer.promise 33 | }, 34 | getUserInfo: function(uid) { 35 | var defer = new $q.defer() 36 | 37 | this.login().then(function(user) { 38 | uid = uid || user.uid 39 | $http({ 40 | method: 'GET', 41 | url: '/api/v2/user/' + uid, 42 | cache: true 43 | }).success(function(userinfo) { 44 | defer.resolve(userinfo) 45 | }) 46 | }, function() { 47 | defer.reject() 48 | }) 49 | return defer.promise 50 | }, 51 | login: function() { 52 | var defer = new $q.defer() 53 | chrome.storage.local.get('logintoken', function(resp) { 54 | if (resp.logintoken) { 55 | $rootScope.user = resp.logintoken 56 | defer.resolve($rootScope.user) 57 | } else { 58 | defer.reject() 59 | } 60 | }) 61 | return defer.promise 62 | }, 63 | logout: function(callback) { 64 | chrome.storage.local.remove('logintoken', function() { 65 | $rootScope.user = null 66 | callback() 67 | }) 68 | } 69 | } 70 | }) -------------------------------------------------------------------------------- /assets/js/ui-directives.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | angular.module('ANNO.ui-directives', []) 4 | .directive('scrollHide', [function() { 5 | return function(scope, elem, attrs) { 6 | var hideFrom = attrs.hideFrom || 10 7 | , target = window 8 | 9 | if (attrs.targetSel) { 10 | target = document.querySelector(attrs.targetSel) 11 | } 12 | 13 | if (attrs.hideFrom) { 14 | scope.$watch(attrs.hideFrom, function(value) { 15 | hideFrom = parseInt(value, 10) 16 | }) 17 | } 18 | 19 | function onScroll() { 20 | elem.toggleClass('hide', target.scrollTop > hideFrom) 21 | } 22 | scope.hide = false 23 | 24 | angular.element(target).on('scroll', _.throttle(onScroll, 200)) 25 | } 26 | }]) 27 | -------------------------------------------------------------------------------- /assets/lib/angular/angular-cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.13 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /** 9 | * @ngdoc overview 10 | * @name ngCookies 11 | * @description 12 | * 13 | * # ngCookies 14 | * 15 | * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies. 16 | * 17 | * {@installModule cookies} 18 | * 19 | *
20 | * 21 | * See {@link ngCookies.$cookies `$cookies`} and 22 | * {@link ngCookies.$cookieStore `$cookieStore`} for usage. 23 | */ 24 | 25 | 26 | angular.module('ngCookies', ['ng']). 27 | /** 28 | * @ngdoc object 29 | * @name ngCookies.$cookies 30 | * @requires $browser 31 | * 32 | * @description 33 | * Provides read/write access to browser's cookies. 34 | * 35 | * Only a simple Object is exposed and by adding or removing properties to/from 36 | * this object, new cookies are created/deleted at the end of current $eval. 37 | * 38 | * Requires the {@link ngCookies `ngCookies`} module to be installed. 39 | * 40 | * @example 41 | 42 | 43 | 51 | 52 | 53 | */ 54 | factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { 55 | var cookies = {}, 56 | lastCookies = {}, 57 | lastBrowserCookies, 58 | runEval = false, 59 | copy = angular.copy, 60 | isUndefined = angular.isUndefined; 61 | 62 | //creates a poller fn that copies all cookies from the $browser to service & inits the service 63 | $browser.addPollFn(function() { 64 | var currentCookies = $browser.cookies(); 65 | if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl 66 | lastBrowserCookies = currentCookies; 67 | copy(currentCookies, lastCookies); 68 | copy(currentCookies, cookies); 69 | if (runEval) $rootScope.$apply(); 70 | } 71 | })(); 72 | 73 | runEval = true; 74 | 75 | //at the end of each eval, push cookies 76 | //TODO: this should happen before the "delayed" watches fire, because if some cookies are not 77 | // strings or browser refuses to store some cookies, we update the model in the push fn. 78 | $rootScope.$watch(push); 79 | 80 | return cookies; 81 | 82 | 83 | /** 84 | * Pushes all the cookies from the service to the browser and verifies if all cookies were 85 | * stored. 86 | */ 87 | function push() { 88 | var name, 89 | value, 90 | browserCookies, 91 | updated; 92 | 93 | //delete any cookies deleted in $cookies 94 | for (name in lastCookies) { 95 | if (isUndefined(cookies[name])) { 96 | $browser.cookies(name, undefined); 97 | } 98 | } 99 | 100 | //update all cookies updated in $cookies 101 | for(name in cookies) { 102 | value = cookies[name]; 103 | if (!angular.isString(value)) { 104 | if (angular.isDefined(lastCookies[name])) { 105 | cookies[name] = lastCookies[name]; 106 | } else { 107 | delete cookies[name]; 108 | } 109 | } else if (value !== lastCookies[name]) { 110 | $browser.cookies(name, value); 111 | updated = true; 112 | } 113 | } 114 | 115 | //verify what was actually stored 116 | if (updated){ 117 | updated = false; 118 | browserCookies = $browser.cookies(); 119 | 120 | for (name in cookies) { 121 | if (cookies[name] !== browserCookies[name]) { 122 | //delete or reset all cookies that the browser dropped from $cookies 123 | if (isUndefined(browserCookies[name])) { 124 | delete cookies[name]; 125 | } else { 126 | cookies[name] = browserCookies[name]; 127 | } 128 | updated = true; 129 | } 130 | } 131 | } 132 | } 133 | }]). 134 | 135 | 136 | /** 137 | * @ngdoc object 138 | * @name ngCookies.$cookieStore 139 | * @requires $cookies 140 | * 141 | * @description 142 | * Provides a key-value (string-object) storage, that is backed by session cookies. 143 | * Objects put or retrieved from this storage are automatically serialized or 144 | * deserialized by angular's toJson/fromJson. 145 | * 146 | * Requires the {@link ngCookies `ngCookies`} module to be installed. 147 | * 148 | * @example 149 | */ 150 | factory('$cookieStore', ['$cookies', function($cookies) { 151 | 152 | return { 153 | /** 154 | * @ngdoc method 155 | * @name ngCookies.$cookieStore#get 156 | * @methodOf ngCookies.$cookieStore 157 | * 158 | * @description 159 | * Returns the value of given cookie key 160 | * 161 | * @param {string} key Id to use for lookup. 162 | * @returns {Object} Deserialized cookie value. 163 | */ 164 | get: function(key) { 165 | var value = $cookies[key]; 166 | return value ? angular.fromJson(value) : value; 167 | }, 168 | 169 | /** 170 | * @ngdoc method 171 | * @name ngCookies.$cookieStore#put 172 | * @methodOf ngCookies.$cookieStore 173 | * 174 | * @description 175 | * Sets a value for given cookie key 176 | * 177 | * @param {string} key Id for the `value`. 178 | * @param {Object} value Value to be stored. 179 | */ 180 | put: function(key, value) { 181 | $cookies[key] = angular.toJson(value); 182 | }, 183 | 184 | /** 185 | * @ngdoc method 186 | * @name ngCookies.$cookieStore#remove 187 | * @methodOf ngCookies.$cookieStore 188 | * 189 | * @description 190 | * Remove given cookie 191 | * 192 | * @param {string} key Id of the key-value pair to delete. 193 | */ 194 | remove: function(key) { 195 | delete $cookies[key]; 196 | } 197 | }; 198 | 199 | }]); 200 | 201 | 202 | })(window, window.angular); 203 | -------------------------------------------------------------------------------- /assets/lib/ng-infinite-scroll/ng-infinite-scroll.js: -------------------------------------------------------------------------------- 1 | /* ng-infinite-scroll - v1.0.0 - 2013-02-23 */ 2 | var mod; 3 | 4 | mod = angular.module('infinite-scroll', []); 5 | 6 | mod.directive('infiniteScroll', [ 7 | '$rootScope', '$timeout', function($rootScope, $timeout) { 8 | return { 9 | link: function(scope, elem, attrs) { 10 | var scrollElemSel = attrs.scrollElemSel || 'body' 11 | , scrollElem = document.querySelector(scrollElemSel) 12 | , $scrollElem = angular.element(scrollElem) 13 | 14 | var checkWhenEnabled, handler, scrollDistance, scrollEnabled; 15 | scrollDistance = 0; 16 | if (attrs.infiniteScrollDistance != null) { 17 | scope.$watch(attrs.infiniteScrollDistance, function(value) { 18 | return scrollDistance = parseInt(value, 10); 19 | }); 20 | } 21 | scrollEnabled = true; 22 | checkWhenEnabled = false; 23 | if (attrs.infiniteScrollDisabled != null) { 24 | scope.$watch(attrs.infiniteScrollDisabled, function(value) { 25 | scrollEnabled = !value; 26 | if (scrollEnabled && checkWhenEnabled) { 27 | checkWhenEnabled = false; 28 | return handler(); 29 | } 30 | }); 31 | } 32 | handler = function() { 33 | var elementBottom, remaining, shouldScroll, windowBottom; 34 | windowBottom = scrollElem.clientHeight + scrollElem.scrollTop; 35 | elementBottom = elem[0].offsetTop + elem[0].clientHeight; 36 | remaining = elementBottom - windowBottom; 37 | shouldScroll = remaining <= scrollElem.clientHeight * scrollDistance; 38 | if (shouldScroll && scrollEnabled) { 39 | if ($rootScope.$$phase) { 40 | return scope.$eval(attrs.infiniteScroll); 41 | } else { 42 | return scope.$apply(attrs.infiniteScroll); 43 | } 44 | } else if (shouldScroll) { 45 | return checkWhenEnabled = true; 46 | } 47 | }; 48 | $scrollElem.on('scroll', _.throttle(handler, 200)); 49 | scope.$on('$destroy', function() { 50 | return $scrollElem.off('scroll', handler); 51 | }); 52 | return $timeout((function() { 53 | if (attrs.infiniteScrollImmediateCheck) { 54 | if (scope.$eval(attrs.infiniteScrollImmediateCheck)) { 55 | return handler(); 56 | } 57 | } else { 58 | return handler(); 59 | } 60 | }), 0); 61 | } 62 | }; 63 | } 64 | ]); 65 | -------------------------------------------------------------------------------- /assets/resource/background.js: -------------------------------------------------------------------------------- 1 | chrome.app.runtime.onLaunched.addListener(function() { 2 | chrome.app.window.create('index.html', { 3 | 'minWidth': 860, 4 | 'minHeight': 580, 5 | 'bounds': { 6 | 'width': 960, 7 | 'height': 600 8 | } 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /assets/resource/css/themes/solarized_dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | padding: 0.5em; 10 | background: #002b36; 11 | color: #839496; 12 | } 13 | 14 | .hljs-comment, 15 | .hljs-template_comment, 16 | .diff .hljs-header, 17 | .hljs-doctype, 18 | .hljs-pi, 19 | .lisp .hljs-string, 20 | .hljs-javadoc { 21 | color: #586e75; 22 | } 23 | 24 | /* Solarized Green */ 25 | .hljs-keyword, 26 | .hljs-winutils, 27 | .method, 28 | .hljs-addition, 29 | .css .hljs-tag, 30 | .hljs-request, 31 | .hljs-status, 32 | .nginx .hljs-title { 33 | color: #859900; 34 | } 35 | 36 | /* Solarized Cyan */ 37 | .hljs-number, 38 | .hljs-command, 39 | .hljs-string, 40 | .hljs-tag .hljs-value, 41 | .hljs-rules .hljs-value, 42 | .hljs-phpdoc, 43 | .tex .hljs-formula, 44 | .hljs-regexp, 45 | .hljs-hexcolor, 46 | .hljs-link_url { 47 | color: #2aa198; 48 | } 49 | 50 | /* Solarized Blue */ 51 | .hljs-title, 52 | .hljs-localvars, 53 | .hljs-chunk, 54 | .hljs-decorator, 55 | .hljs-built_in, 56 | .hljs-identifier, 57 | .vhdl .hljs-literal, 58 | .hljs-id, 59 | .css .hljs-function { 60 | color: #268bd2; 61 | } 62 | 63 | /* Solarized Yellow */ 64 | .hljs-attribute, 65 | .hljs-variable, 66 | .lisp .hljs-body, 67 | .smalltalk .hljs-number, 68 | .hljs-constant, 69 | .hljs-class .hljs-title, 70 | .hljs-parent, 71 | .haskell .hljs-type, 72 | .hljs-link_reference { 73 | color: #b58900; 74 | } 75 | 76 | /* Solarized Orange */ 77 | .hljs-preprocessor, 78 | .hljs-preprocessor .hljs-keyword, 79 | .hljs-pragma, 80 | .hljs-shebang, 81 | .hljs-symbol, 82 | .hljs-symbol .hljs-string, 83 | .diff .hljs-change, 84 | .hljs-special, 85 | .hljs-attr_selector, 86 | .hljs-subst, 87 | .hljs-cdata, 88 | .clojure .hljs-title, 89 | .css .hljs-pseudo, 90 | .hljs-header { 91 | color: #cb4b16; 92 | } 93 | 94 | /* Solarized Red */ 95 | .hljs-deletion, 96 | .hljs-important { 97 | color: #dc322f; 98 | } 99 | 100 | /* Solarized Violet */ 101 | .hljs-link_label { 102 | color: #6c71c4; 103 | } 104 | 105 | .tex .hljs-formula { 106 | background: #073642; 107 | } 108 | -------------------------------------------------------------------------------- /assets/resource/css/themes/tomorrow.css: -------------------------------------------------------------------------------- 1 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 2 | 3 | /* Tomorrow Comment */ 4 | .hljs-comment, 5 | .hljs-title { 6 | color: #8e908c; 7 | } 8 | 9 | /* Tomorrow Red */ 10 | .hljs-variable, 11 | .hljs-attribute, 12 | .hljs-tag, 13 | .hljs-regexp, 14 | .ruby .hljs-constant, 15 | .xml .hljs-tag .hljs-title, 16 | .xml .hljs-pi, 17 | .xml .hljs-doctype, 18 | .html .hljs-doctype, 19 | .css .hljs-id, 20 | .css .hljs-class, 21 | .css .hljs-pseudo { 22 | color: #c82829; 23 | } 24 | 25 | /* Tomorrow Orange */ 26 | .hljs-number, 27 | .hljs-preprocessor, 28 | .hljs-pragma, 29 | .hljs-built_in, 30 | .hljs-literal, 31 | .hljs-params, 32 | .hljs-constant { 33 | color: #f5871f; 34 | } 35 | 36 | /* Tomorrow Yellow */ 37 | .ruby .hljs-class .hljs-title, 38 | .css .hljs-rules .hljs-attribute { 39 | color: #eab700; 40 | } 41 | 42 | /* Tomorrow Green */ 43 | .hljs-string, 44 | .hljs-value, 45 | .hljs-inheritance, 46 | .hljs-header, 47 | .ruby .hljs-symbol, 48 | .xml .hljs-cdata { 49 | color: #718c00; 50 | } 51 | 52 | /* Tomorrow Aqua */ 53 | .css .hljs-hexcolor { 54 | color: #3e999f; 55 | } 56 | 57 | /* Tomorrow Blue */ 58 | .hljs-function, 59 | .python .hljs-decorator, 60 | .python .hljs-title, 61 | .ruby .hljs-function .hljs-title, 62 | .ruby .hljs-title .hljs-keyword, 63 | .perl .hljs-sub, 64 | .javascript .hljs-title, 65 | .coffeescript .hljs-title { 66 | color: #4271ae; 67 | } 68 | 69 | /* Tomorrow Purple */ 70 | .hljs-keyword, 71 | .javascript .hljs-function { 72 | color: #8959a8; 73 | } 74 | 75 | .hljs { 76 | display: block; 77 | background: white; 78 | color: #4d4d4c; 79 | padding: 0.5em; 80 | } 81 | 82 | .coffeescript .javascript, 83 | .javascript .xml, 84 | .tex .hljs-formula, 85 | .xml .javascript, 86 | .xml .vbscript, 87 | .xml .css, 88 | .xml .hljs-cdata { 89 | opacity: 0.5; 90 | } 91 | -------------------------------------------------------------------------------- /assets/resource/css/themes/zenburn.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Zenburn style from voldmar.ru (c) Vladimir Epifanov 4 | based on dark.css by Ivan Sagalaev 5 | 6 | */ 7 | 8 | .hljs { 9 | display: block; padding: 0.5em; 10 | background: #3F3F3F; 11 | color: #DCDCDC; 12 | } 13 | 14 | .hljs-keyword, 15 | .hljs-tag, 16 | .css .hljs-class, 17 | .css .hljs-id, 18 | .lisp .hljs-title, 19 | .nginx .hljs-title, 20 | .hljs-request, 21 | .hljs-status, 22 | .clojure .hljs-attribute { 23 | color: #E3CEAB; 24 | } 25 | 26 | .django .hljs-template_tag, 27 | .django .hljs-variable, 28 | .django .hljs-filter .hljs-argument { 29 | color: #DCDCDC; 30 | } 31 | 32 | .hljs-number, 33 | .hljs-date { 34 | color: #8CD0D3; 35 | } 36 | 37 | .dos .hljs-envvar, 38 | .dos .hljs-stream, 39 | .hljs-variable, 40 | .apache .hljs-sqbracket { 41 | color: #EFDCBC; 42 | } 43 | 44 | .dos .hljs-flow, 45 | .diff .hljs-change, 46 | .python .exception, 47 | .python .hljs-built_in, 48 | .hljs-literal, 49 | .tex .hljs-special { 50 | color: #EFEFAF; 51 | } 52 | 53 | .diff .hljs-chunk, 54 | .hljs-subst { 55 | color: #8F8F8F; 56 | } 57 | 58 | .dos .hljs-keyword, 59 | .python .hljs-decorator, 60 | .hljs-title, 61 | .haskell .hljs-type, 62 | .diff .hljs-header, 63 | .ruby .hljs-class .hljs-parent, 64 | .apache .hljs-tag, 65 | .nginx .hljs-built_in, 66 | .tex .hljs-command, 67 | .hljs-prompt { 68 | color: #efef8f; 69 | } 70 | 71 | .dos .hljs-winutils, 72 | .ruby .hljs-symbol, 73 | .ruby .hljs-symbol .hljs-string, 74 | .ruby .hljs-string { 75 | color: #DCA3A3; 76 | } 77 | 78 | .diff .hljs-deletion, 79 | .hljs-string, 80 | .hljs-tag .hljs-value, 81 | .hljs-preprocessor, 82 | .hljs-pragma, 83 | .hljs-built_in, 84 | .sql .hljs-aggregate, 85 | .hljs-javadoc, 86 | .smalltalk .hljs-class, 87 | .smalltalk .hljs-localvars, 88 | .smalltalk .hljs-array, 89 | .css .hljs-rules .hljs-value, 90 | .hljs-attr_selector, 91 | .hljs-pseudo, 92 | .apache .hljs-cbracket, 93 | .tex .hljs-formula, 94 | .coffeescript .hljs-attribute { 95 | color: #CC9393; 96 | } 97 | 98 | .hljs-shebang, 99 | .diff .hljs-addition, 100 | .hljs-comment, 101 | .java .hljs-annotation, 102 | .hljs-template_comment, 103 | .hljs-pi, 104 | .hljs-doctype { 105 | color: #7F9F7F; 106 | } 107 | 108 | .coffeescript .javascript, 109 | .javascript .xml, 110 | .tex .hljs-formula, 111 | .xml .javascript, 112 | .xml .vbscript, 113 | .xml .css, 114 | .xml .hljs-cdata { 115 | opacity: 0.5; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /assets/resource/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/assets/resource/fonts/icomoon.eot -------------------------------------------------------------------------------- /assets/resource/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/assets/resource/fonts/icomoon.ttf -------------------------------------------------------------------------------- /assets/resource/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/assets/resource/fonts/icomoon.woff -------------------------------------------------------------------------------- /assets/resource/img/books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/assets/resource/img/books.png -------------------------------------------------------------------------------- /assets/resource/img/login_with_douban_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/assets/resource/img/login_with_douban_32.png -------------------------------------------------------------------------------- /assets/resource/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "读书笔记", 3 | "description": "豆瓣读书笔记第三方应用", 4 | "version": "0.2.6", 5 | "manifest_version": 2, 6 | "minimum_chrome_version": "29", 7 | "app": { 8 | "background": { 9 | "scripts": ["background.js"] 10 | } 11 | }, 12 | "icons": { "16": "img/books.png", "128": "img/books.png" }, 13 | "permissions": [ 14 | {"fileSystem": ["write"]} 15 | , "storage" 16 | , "identity" 17 | , "https://www.douban.com/*", "https://api.douban.com/*", "http://*.douban.com/*" 18 | , "https://*.evernote.com/*", "http://*.evernote.com/*" 19 | , "https://*.yinxiang.com/*", "http://*.yinxiang.com/*" 20 | , "https://www.google-analytics.com/" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /assets/stylus/edit.styl: -------------------------------------------------------------------------------- 1 | @import 'lib/vendor' 2 | 3 | tip(font_size = 14px) 4 | [data-title] 5 | position relative 6 | &:after 7 | display none 8 | content attr(data-title) "" 9 | position absolute 10 | top 0 11 | margin-left -100% 12 | background rgba(0, 0, 0, .6) 13 | color #fff 14 | font-size font_size 15 | border-radius 3px 16 | padding (font_size/7) (font_size/2) 17 | line-height font_size * 1.65 18 | white-space pre 19 | //left 80% 20 | transform translate(-100%, 25%) 21 | &:hover 22 | &:after 23 | display inline-block 24 | 25 | modal() 26 | background rgba(0, 0, 0, .2) 27 | width 160px 28 | height 160px 29 | position fixed 30 | top 0 31 | left 0 32 | transform translate(-50%, 50%) 33 | 34 | html.global_editor 35 | height 100% 36 | overflow hidden 37 | body, .ng-view, .container 38 | height 100% 39 | body 40 | overflow hidden 41 | .ng-view 42 | overflow hidden 43 | width calc(100% \- 64px) 44 | margin-top 0 45 | 46 | #editor, #reader 47 | height 100% 48 | overflow-y scroll 49 | 50 | #reader 51 | overflow-x hidden 52 | 53 | #navigation 54 | position fixed 55 | z-index 100 56 | top 0 57 | right 0 58 | bottom 0 59 | padding 10px 12px 60 | margin 0 61 | text-align center 62 | // background #ddd 63 | background #eee 64 | li 65 | list-style none 66 | tip() 67 | a 68 | display inline-block 69 | font-size 1.5em 70 | width 40px 71 | height 40px 72 | line-height 40px 73 | border-radius 3px 74 | background #eee 75 | text-decoration none 76 | color #8d8d8d 77 | margin .3em 0 78 | &:hover 79 | background #e9e9e9 80 | box-shadow inset 0 0 3px #ccc 81 | &.active 82 | background #8d8d8d 83 | color #f1f1f1 84 | box-shadow 0 2px 0 #666 85 | &:hover 86 | box-shadow inset 0 0 3px rgba(0, 0, 0, .3), 0 2px 0 #666 87 | 88 | #editor, #reader 89 | .book-title 90 | font-size .8em 91 | color #ccc 92 | 93 | @import "widgets/editor.styl" 94 | @import "widgets/reader.styl" 95 | 96 | // three kinds of editing mode 97 | .column-mode 98 | #editor, #reader 99 | float left 100 | width 50% 101 | #editor 102 | margin 0 103 | form 104 | margin 40px 60px 0 105 | #reader 106 | box-sizing border-box 107 | padding 40px 40px 0 108 | border-left 1px solid #ccc 109 | 110 | .edit-mode 111 | #reader 112 | display none 113 | #editor 114 | form 115 | max-width 600px 116 | margin 40px auto 0 117 | 118 | .read-mode 119 | #editor 120 | display none 121 | #reader 122 | box-sizing border-box 123 | padding-top 40px 124 | pre 125 | background #eee 126 | 127 | .modal-help 128 | h3, h4 129 | margin-bottom 0 130 | h4 131 | margin-top 1em 132 | .modal-body 133 | line-height 1.65 134 | padding-top 0 -------------------------------------------------------------------------------- /assets/stylus/fonticon.styl: -------------------------------------------------------------------------------- 1 | @font-face 2 | font-family 'icomoon' 3 | src url('/fonts/icomoon.eot?-s5hseh') 4 | src url('/fonts/icomoon.eot?#iefix-s5hseh') format('embedded-opentype'), 5 | url('/fonts/icomoon.woff?-s5hseh') format('woff'), 6 | url('/fonts/icomoon.ttf?-s5hseh') format('truetype'), 7 | url('/fonts/icomoon.svg?-s5hseh#icomoon') format('svg') 8 | font-weight normal 9 | font-style normal 10 | 11 | 12 | [class^="icon-"], [class*=" icon-"] 13 | font-family 'icomoon' 14 | speak none 15 | font-style normal 16 | font-weight normal 17 | font-variant normal 18 | text-transform none 19 | line-height 1 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing antialiased 23 | -moz-osx-font-smoothing grayscale 24 | 25 | 26 | .icon-evernote:before 27 | content "\e600" 28 | 29 | .icon-lock:before 30 | content "\e602" 31 | 32 | .icon-feather:before 33 | content "\e603" 34 | 35 | .icon-leaf:before 36 | content "\e605" 37 | 38 | .icon-delete:before 39 | content "\e604" 40 | 41 | .icon-pen:before 42 | content "\e606" 43 | 44 | .icon-zoom-in:before 45 | content "\e60b" 46 | 47 | .icon-zoom-out:before 48 | content "\e60c" 49 | 50 | .icon-eye:before 51 | content "\e60a" 52 | 53 | .icon-save:before 54 | content "\e601" 55 | 56 | .icon-columns:before 57 | content "\e608" 58 | 59 | .icon-bold:before 60 | content "\e614" 61 | 62 | .icon-italic:before 63 | content "\e615" 64 | 65 | .icon-image:before 66 | content "\e616" 67 | 68 | .icon-left-quote:before 69 | content "\e617" 70 | 71 | .icon-info:before 72 | content "\e618" 73 | 74 | .icon-help:before 75 | content "\e61b" 76 | 77 | .icon-list:before 78 | content "\e619" 79 | 80 | .icon-numbered-list:before 81 | content "\e60f" 82 | 83 | .icon-link:before 84 | content "\e60d" 85 | 86 | .icon-sidebar:before 87 | content "\e61a" 88 | 89 | .icon-more:before 90 | content "\e613" 91 | 92 | .icon-fullscreen:before 93 | content "\e61d" 94 | 95 | .icon-restore:before 96 | content "\e61c" 97 | 98 | .icon-sad:before 99 | content "\e610" 100 | 101 | .icon-star:before 102 | content "\e607" 103 | 104 | .icon-star-empty:before 105 | content "\e61e" 106 | 107 | .icon-plus:before 108 | content "\e61f" 109 | 110 | .icon-export:before 111 | content "\e620" 112 | 113 | .icon-edit:before 114 | content "\e621" 115 | vertical-align middle 116 | -------------------------------------------------------------------------------- /assets/stylus/lib/vendor.styl: -------------------------------------------------------------------------------- 1 | transform() 2 | -webkit-transform arguments 3 | -moz-transform arguments 4 | transform arguments 5 | 6 | transform-origin() 7 | -webkit-transform-origin arguments 8 | -moz-transform-origin arguments 9 | transform-origin arguments 10 | 11 | transform-style() 12 | -webkit-transform-style arguments 13 | -moz-transform-style arguments 14 | transform-style arguments 15 | 16 | perspective() 17 | -webkit-perspective arguments 18 | -moz-perspective arguments 19 | perspective arguments 20 | 21 | perspective-origin() 22 | -webkit-perspective-origin arguments 23 | -moz-perspective-origin arguments 24 | perspective-origin arguments 25 | 26 | transition() 27 | -webkit-transition arguments 28 | -moz-transition arguments 29 | transition arguments 30 | 31 | transition-transform() 32 | -webkit-transition -webkit-transform arguments 33 | -moz-transition -moz-transform arguments 34 | transition transform arguments 35 | 36 | backface-visibility() 37 | -webkit-backface-visibility arguments 38 | -moz-backface-visibility arguments 39 | backface-visibility arguments 40 | -------------------------------------------------------------------------------- /assets/stylus/mod_about.styl: -------------------------------------------------------------------------------- 1 | #about 2 | line-height 1.65 3 | h1 4 | font-size 1.8em 5 | a 6 | color lightseagreen 7 | &:hover 8 | background lightseagreen 9 | color #fff 10 | h2 + ul, h2 ~ div 11 | padding-left 40px 12 | h2 ~ div 13 | line-height 2 14 | a 15 | display inline-block 16 | margin 0 .8em 17 | text-decoration none -------------------------------------------------------------------------------- /assets/stylus/mod_aside.styl: -------------------------------------------------------------------------------- 1 | @import "lib/vendor" 2 | 3 | aside 4 | position fixed 5 | z-index 1000 6 | top 0 7 | left 0 8 | bottom 0 9 | transform translateX(-200px) 10 | transition-transform(.3s) 11 | width 200px 12 | background #363636 13 | padding-top 1em 14 | background-image: url(unquote('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAUVBMVEWFhYWDg4N3d3dtbW17e3t1dXWBgYGHh4d5eXlzc3OLi4ubm5uVlZWPj4+NjY19fX2JiYl/f39ra2uRkZGZmZlpaWmXl5dvb29xcXGTk5NnZ2c8TV1mAAAAG3RSTlNAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAvEOwtAAAFVklEQVR4XpWWB67c2BUFb3g557T/hRo9/WUMZHlgr4Bg8Z4qQgQJlHI4A8SzFVrapvmTF9O7dmYRFZ60YiBhJRCgh1FYhiLAmdvX0CzTOpNE77ME0Zty/nWWzchDtiqrmQDeuv3powQ5ta2eN0FY0InkqDD73lT9c9lEzwUNqgFHs9VQce3TVClFCQrSTfOiYkVJQBmpbq2L6iZavPnAPcoU0dSw0SUTqz/GtrGuXfbyyBniKykOWQWGqwwMA7QiYAxi+IlPdqo+hYHnUt5ZPfnsHJyNiDtnpJyayNBkF6cWoYGAMY92U2hXHF/C1M8uP/ZtYdiuj26UdAdQQSXQErwSOMzt/XWRWAz5GuSBIkwG1H3FabJ2OsUOUhGC6tK4EMtJO0ttC6IBD3kM0ve0tJwMdSfjZo+EEISaeTr9P3wYrGjXqyC1krcKdhMpxEnt5JetoulscpyzhXN5FRpuPHvbeQaKxFAEB6EN+cYN6xD7RYGpXpNndMmZgM5Dcs3YSNFDHUo2LGfZuukSWyUYirJAdYbF3MfqEKmjM+I2EfhA94iG3L7uKrR+GdWD73ydlIB+6hgref1QTlmgmbM3/LeX5GI1Ux1RWpgxpLuZ2+I+IjzZ8wqE4nilvQdkUdfhzI5QDWy+kw5Wgg2pGpeEVeCCA7b85BO3F9DzxB3cdqvBzWcmzbyMiqhzuYqtHRVG2y4x+KOlnyqla8AoWWpuBoYRxzXrfKuILl6SfiWCbjxoZJUaCBj1CjH7GIaDbc9kqBY3W/Rgjda1iqQcOJu2WW+76pZC9QG7M00dffe9hNnseupFL53r8F7YHSwJWUKP2q+k7RdsxyOB11n0xtOvnW4irMMFNV4H0uqwS5ExsmP9AxbDTc9JwgneAT5vTiUSm1E7BSflSt3bfa1tv8Di3R8n3Af7MNWzs49hmauE2wP+ttrq+AsWpFG2awvsuOqbipWHgtuvuaAE+A1Z/7gC9hesnr+7wqCwG8c5yAg3AL1fm8T9AZtp/bbJGwl1pNrE7RuOX7PeMRUERVaPpEs+yqeoSmuOlokqw49pgomjLeh7icHNlG19yjs6XXOMedYm5xH2YxpV2tc0Ro2jJfxC50ApuxGob7lMsxfTbeUv07TyYxpeLucEH1gNd4IKH2LAg5TdVhlCafZvpskfncCfx8pOhJzd76bJWeYFnFciwcYfubRc12Ip/ppIhA1/mSZ/RxjFDrJC5xifFjJpY2Xl5zXdguFqYyTR1zSp1Y9p+tktDYYSNflcxI0iyO4TPBdlRcpeqjK/piF5bklq77VSEaA+z8qmJTFzIWiitbnzR794USKBUaT0NTEsVjZqLaFVqJoPN9ODG70IPbfBHKK+/q/AWR0tJzYHRULOa4MP+W/HfGadZUbfw177G7j/OGbIs8TahLyynl4X4RinF793Oz+BU0saXtUHrVBFT/DnA3ctNPoGbs4hRIjTok8i+algT1lTHi4SxFvONKNrgQFAq2/gFnWMXgwffgYMJpiKYkmW3tTg3ZQ9Jq+f8XN+A5eeUKHWvJWJ2sgJ1Sop+wwhqFVijqWaJhwtD8MNlSBeWNNWTa5Z5kPZw5+LbVT99wqTdx29lMUH4OIG/D86ruKEauBjvH5xy6um/Sfj7ei6UUVk4AIl3MyD4MSSTOFgSwsH/QJWaQ5as7ZcmgBZkzjjU1UrQ74ci1gWBCSGHtuV1H2mhSnO3Wp/3fEV5a+4wz//6qy8JxjZsmxxy5+4w9CDNJY09T072iKG0EnOS0arEYgXqYnXcYHwjTtUNAcMelOd4xpkoqiTYICWFq0JSiPfPDQdnt+4/wuqcXY47QILbgAAAABJRU5ErkJggg==')) 15 | box-shadow inset -3px 0 8px #444 16 | &.show 17 | transform(translateX(0)) 18 | ul 19 | margin 0 20 | padding 0 21 | li 22 | border-bottom 1px solid rgba(255,255,255,.1) 23 | a 24 | display block 25 | color #fefefe 26 | text-decoration none 27 | padding .8em 1.2em 28 | &:hover 29 | color lightseagreen 30 | .info 31 | position absolute 32 | left 0 33 | right 0 34 | bottom 0 35 | .banner 36 | height 120px 37 | .avatar 38 | display block 39 | width 100px 40 | height 100px 41 | border-radius 50px 42 | margin 0 auto 43 | background-size cover 44 | background-position center 45 | // .logo 46 | // // background rgba(32, 178, 170, .8) 47 | // color #fff 48 | // font-size 1.5em 49 | // width 100px 50 | // height 100px 51 | // margin 0 auto 52 | // line-height 100px 53 | // text-align center 54 | // position relative 55 | // &:before, &:after 56 | // content "" 57 | // position absolute 58 | // width 100% 59 | // height 100% 60 | // top 0 61 | // left 0 62 | // z-index -1 63 | // // background rgba(32, 178, 170, .5) 64 | // &:before 65 | // transform rotate(8deg) 66 | // &:after 67 | // transform rotate(16deg) 68 | .ng-view 69 | &.pushed 70 | position relative 71 | width calc(100% \- 200px) 72 | 73 | .global_editor 74 | .ng-view.pushed 75 | width calc(100% \- 200px \- 64px) 76 | 77 | .header, .ng-view 78 | transition left .3s 79 | &.pushed 80 | left 200px -------------------------------------------------------------------------------- /assets/stylus/mod_auth.styl: -------------------------------------------------------------------------------- 1 | @import "lib/vendor" 2 | 3 | .auth 4 | margin-top 10% 5 | // transform(translateY(-50%)) 6 | text-align center 7 | -------------------------------------------------------------------------------- /assets/stylus/mod_friends.styl: -------------------------------------------------------------------------------- 1 | #friends 2 | .friends 3 | padding 0 4 | margin 0 5 | .friend 6 | display inline-block 7 | margin 1em 8 | width 60px 9 | text-align center 10 | vertical-align top 11 | a 12 | color #999 13 | text-decoration none 14 | &:hover 15 | opacity .8 16 | span 17 | display block 18 | margin-top .3em 19 | img 20 | border-radius 24px 21 | box-shadow 0 1px 3px #aaa 22 | &.more 23 | font-size 1.5em 24 | line-height 2em 25 | a 26 | color lightseagreen 27 | &:hover 28 | opacity .8 -------------------------------------------------------------------------------- /assets/stylus/mod_info.styl: -------------------------------------------------------------------------------- 1 | @require 'lib/vendor' 2 | 3 | #info 4 | margin-bottom 3em 5 | .summary 6 | white-space pre-wrap 7 | line-height 1.8 8 | a 9 | color lightseagreen 10 | text-decoration none 11 | margin 0 .3em 12 | &:hover 13 | background lightseagreen 14 | color #fff 15 | .books 16 | .add-note 17 | font-size 1em 18 | & > section 19 | h1 20 | font-size 1.32em 21 | &.statistic 22 | h1 23 | margin-top 0 24 | &.account 25 | .ui-card 26 | text-align center 27 | margin-right 1em 28 | a 29 | font-size 3em 30 | // color #4B4D4E 31 | color #757778 32 | &:hover, &.active 33 | // color #5FB336 34 | color lightseagreen 35 | &.active 36 | cursor default 37 | span 38 | font-size .4em 39 | display block 40 | .bind-choice 41 | .icon-evernote 42 | color lightgray 43 | float left 44 | font-size 3em 45 | margin-right .3em 46 | a 47 | &:first-of-type 48 | display inline-block 49 | padding-bottom .1em 50 | margin-bottom .2em 51 | border-bottom 1px solid #eee 52 | &:last-of-type 53 | display block 54 | .unbind 55 | position relative 56 | &:before, &:after 57 | content "" 58 | position absolute 59 | &:before 60 | transition background .3s 61 | &:after 62 | content "解绑" 63 | color #e74c3c 64 | top 50% 65 | left 50% 66 | transform(translate(-50%, -50%)) 67 | opacity 0 68 | transition opacity .3s 69 | &:hover 70 | cursor pointer 71 | &:before 72 | top 0 73 | left 0 74 | right 0 75 | bottom 0 76 | background rgba(255, 255, 255, .7) 77 | &:after 78 | display initial 79 | opacity 1 80 | .options 81 | .tip 82 | font-size 12px 83 | color #bbb -------------------------------------------------------------------------------- /assets/stylus/note.styl: -------------------------------------------------------------------------------- 1 | @require "fonticon" 2 | @require 'ui/typography' 3 | 4 | body 5 | & > .meta 6 | @extends $center 7 | 8 | .global_note 9 | .ng-view 10 | margin-top 0 11 | padding-top 50px 12 | 13 | #one-note 14 | margin 3em 0 15 | h1 16 | @extends $center 17 | pre 18 | background #eee 19 | white-space pre-wrap 20 | .content 21 | margin-bottom 40px 22 | -webkit-user-select text 23 | user-select text 24 | .share 25 | margin-bottom 60px 26 | 27 | .meta 28 | color #aaa 29 | @extends $center 30 | span 31 | margin-left .5em 32 | &:first-child 33 | margin-left 0 34 | a 35 | text-decoration none 36 | color lightseagreen 37 | margin-right .5em 38 | &:hover 39 | background lightseagreen 40 | color #fff 41 | 42 | .operate 43 | position: absolute 44 | top 0 45 | right 0 46 | padding 1em 47 | margin 0 auto 48 | padding-left calc(50% \- 300px) 49 | margin .3em 0 1.2em 50 | transition opacity .3s, top .5s cubic-bezier(0.55, 0.085, 0.68, 0.53) 51 | opacity 1 52 | a 53 | color #808080 54 | margin-right 1em 55 | text-decoration none 56 | vertical-align: middle 57 | &:before 58 | margin-right: 0.3em 59 | &:hover 60 | color #111 61 | &.icon-evernote 62 | &:hover 63 | color #48b449 64 | &.icon-pen 65 | &:before 66 | vertical-align: middle 67 | &:hover 68 | color #0E90D2 69 | &.icon-star, &.icon-star-empty 70 | &:hover 71 | color #f39c12 72 | &.icon-delete 73 | &:before 74 | vertical-align middle 75 | &:hover 76 | color #DD514C 77 | &.hide 78 | top -2em 79 | opacity 0 80 | -------------------------------------------------------------------------------- /assets/stylus/style.styl: -------------------------------------------------------------------------------- 1 | @require "lib/vendor" 2 | $page_width = 600px 3 | 4 | html, body 5 | height 100% 6 | 7 | .ng-view 8 | height calc(100% \- 50px) 9 | margin-top 50px 10 | overflow-y auto 11 | 12 | .ng-hide 13 | display: none !important 14 | 15 | body 16 | padding: 50px 17 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif 18 | background #f9f9f9 19 | color #111 20 | a 21 | color #00B7FF 22 | cursor pointer 23 | 24 | button 25 | cursor pointer 26 | 27 | // [link] 28 | 29 | pre 30 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif 31 | 32 | @import "ui/reset.styl" 33 | @import "ui/button.styl" 34 | @import "ui/form.styl" 35 | @import "ui/modal.styl" 36 | @import "ui/bar.styl" 37 | @import "ui/loading.styl" 38 | @import "ui/alert.styl" 39 | @import "ui/card.styl" 40 | 41 | header 42 | &.header 43 | bar() 44 | span.icon-sidebar 45 | float left 46 | &.vanished 47 | right auto 48 | box-shadow none 49 | background none 50 | color #aaa 51 | .title 52 | font-size .8em 53 | line-height 1.25 54 | margin-left 1em 55 | font-family Hiragino Sans GB, STHeiti, Microsoft Yahei, sans-serif 56 | a 57 | color #fff 58 | 59 | @import "mod_aside.styl" 60 | @import "mod_auth.styl" 61 | 62 | //#all-books, #one-book, #about, #info, #friends 63 | .ng-view > section 64 | margin 3em 3em 0 65 | 66 | // all books 67 | 68 | ul.books 69 | padding 0 70 | margin 0 -1em 71 | 72 | li.book 73 | margin 1em 74 | 75 | @import "widgets/book.styl" 76 | 77 | .modal-footer 78 | div:first-child 79 | float left 80 | text-align left 81 | color #999 82 | 83 | #new-book-name 84 | border 1px solid #eee 85 | padding .6em 1em 86 | box-sizing border-box 87 | outline 0 88 | width 100% 89 | &:focus 90 | border-color #999 91 | .tip 92 | color #999 93 | text-align right 94 | margin-top 1em 95 | & + .tip 96 | margin-top 0 97 | margin-bottom 1em 98 | span 99 | color rgb(237, 20, 61) 100 | // search result 101 | .search-results 102 | margin-top 1em 103 | padding 0 104 | margin-right -20px 105 | padding-right 20px 106 | max-height 260px 107 | overflow-y scroll 108 | li 109 | list-style none 110 | overflow hidden 111 | padding 8px 112 | position relative 113 | &:before, &.active &:before 114 | content "" 115 | position absolute 116 | top 0 117 | right 0 118 | bottom 0 119 | left 0 120 | background rgba(0, 0, 0, .15) 121 | border-radius 3px 122 | transform(translateZ(0)) 123 | &.active 124 | &:before 125 | border 3px solid rgb(32, 178, 170) 126 | background rgba(0, 0, 0, .05) 127 | &:before 128 | display none 129 | img 130 | width 120px 131 | float left 132 | margin-right 8px 133 | .result-info 134 | // display inline-block 135 | h4 136 | word-break break-word 137 | margin 0 138 | p 139 | color #666 140 | font-size .8em 141 | &:hover, &.active 142 | &:before 143 | display block 144 | 145 | .selected-book-name 146 | color #e74c3c 147 | 148 | 149 | // one book 150 | .ng-view.pushed 151 | .main 152 | top 60px 153 | 154 | #one-book 155 | h1 156 | a 157 | color #000 158 | text-decoration none 159 | 160 | .cubic-view 161 | float left 162 | 163 | .main 164 | margin-left 310px 165 | transform(translateZ(0)) 166 | 167 | header 168 | padding-bottom 1em 169 | border-bottom 1px solid #eee 170 | margin-bottom 1em 171 | span 172 | color #999 173 | margin-right 1em 174 | font-size 12px 175 | .button-default 176 | color #000 177 | border-radius 3px 178 | margin-right 1em 179 | .order 180 | display inline-block 181 | .button-primary 182 | display inline-block 183 | border-radius 2px 184 | margin-right .5em 185 | background #aaa 186 | border none 187 | border-bottom 3px solid #777 188 | transition all .3s 189 | &:before 190 | vertical-align middle 191 | &:hover 192 | background lightseagreen 193 | border-radius 2px 194 | border-color #12837D 195 | [class*="icon-"]:before 196 | margin-right .5em 197 | .right-actions 198 | float right 199 | margin-top .2em 200 | margin-bottom 1em 201 | 202 | @import "widgets/book-3d.styl" 203 | 204 | .cubic-view 205 | .book-back 206 | width 308px 207 | 208 | ul.notes 209 | padding 0 210 | @import "widgets/note-card.styl" 211 | 212 | .modal-export 213 | h3 214 | margin-bottom 0 215 | button.icon-evernote 216 | background #c0c0c0 217 | border 0 218 | color #FFF 219 | padding .5em 1em 220 | font-size 1em 221 | outline 0 222 | border-radius .3em 223 | transition background-color .2s 224 | &:before 225 | margin-right .3em 226 | &:hover 227 | background #119E4D 228 | box-shadow inset 0 1px 3px rgba(0, 0, 0, 0.3) 229 | &:before 230 | display inline-block 231 | transform(rotate(-10deg)) 232 | .mask-evernote 233 | background rgba(0, 0, 0, .3) 234 | &.modal .modal-dialog 235 | min-width 70% 236 | 237 | 238 | .export-preview 239 | font-size 90% 240 | white-space pre-wrap 241 | overflow-y scroll 242 | max-height 300px 243 | margin-top 1em 244 | // box-shadow inset 0 0 2px #ccc 245 | border 2px solid #eee 246 | padding 1em 247 | box-sizing border-box 248 | -webkit-user-select text 249 | user-select text 250 | pre 251 | white-space pre-wrap 252 | 253 | 254 | // one note 255 | @import "note.styl" 256 | .modal-evernote 257 | h3 258 | margin-bottom 0 259 | .row 260 | height 2em 261 | label 262 | width 10em 263 | display inline-block 264 | vertical-align top 265 | select, input[type="text"] 266 | margin 0 267 | select 268 | font-size 1.2em 269 | input[type="text"] 270 | border 1px solid #ccc 271 | padding .4em 1em 272 | outline 0 273 | &:focus 274 | border-color #999 275 | 276 | // editor 277 | @import "edit.styl" 278 | 279 | // about 280 | @import "mod_about.styl" 281 | 282 | // info 283 | @import "mod_info.styl" 284 | 285 | // friends 286 | @import "mod_friends.styl" 287 | 288 | // mod_error 289 | .not_found 290 | font-size 2em 291 | position absolute 292 | top 30% 293 | left 50% 294 | transform(translate(-50%, -50%)) 295 | color #aaa 296 | text-align center 297 | a 298 | display block 299 | font-size .8em 300 | margin-top 1em 301 | 302 | // mod_fav 303 | .footnote 304 | margin-top 1em 305 | -------------------------------------------------------------------------------- /assets/stylus/ui/alert.styl: -------------------------------------------------------------------------------- 1 | // get colors from 2 | // http://flatuicolors.com/ 3 | @import '../lib/vendor' 4 | 5 | .alert 6 | position fixed 7 | top 4em 8 | // z-index 1111 9 | width 60% 10 | min-width 500px 11 | left 20% 12 | opacity .96 13 | box-shadow 0px 2px 4px rgba(0, 0, 0, 0.2) 14 | z-index 9999 15 | // transform(rotateX(90deg)) 16 | // transform-origin(top center) 17 | // transition-transform(1s) 18 | top -4em 19 | transition(top 1s) 20 | cursor pointer 21 | &:after 22 | content "x" 23 | position absolute 24 | right 1em 25 | top .5em 26 | &.show 27 | top 4em 28 | //transform(rotateX(0)) 29 | 30 | 31 | .alert 32 | padding .6em 1.2em 33 | border 1px solid 34 | border-radius 2px 35 | text-align center 36 | &.success 37 | background-color #1abc9c 38 | border-color #16a085 39 | color #fff 40 | a 41 | color #fff 42 | &:hover 43 | background-color #fff 44 | color #1abc9c 45 | &.error 46 | background-color #F17288 47 | border-color #e74c3c 48 | color #fff 49 | a 50 | color #fff 51 | &:hover 52 | color #F17288 53 | background-color #fff 54 | &.warn 55 | background-color #F9EDBE 56 | border-color #F0C36D 57 | color #333 58 | a 59 | color #f39c12 60 | &:hover 61 | background-color #f39c12 62 | color #fff -------------------------------------------------------------------------------- /assets/stylus/ui/bar.styl: -------------------------------------------------------------------------------- 1 | bar() 2 | // background linear-gradient(to right,rgba(243, 123, 124, 0.96) 0,rgba(245, 154, 133, 0.96) 100%) 3 | padding .5em 4 | font-size 1.8em 5 | position fixed 6 | top 0 7 | left 0 8 | right 0 9 | z-index 999 10 | box-shadow 0 1px 1px rgba(0, 0, 0, .1) 11 | // background linear-gradient(to left, rgba(95, 158, 160, .96) 0, rgba(126, 180, 142, .96) 100%) 12 | background rgba(95, 158, 160, .96) 13 | color rgba(255, 255, 255, .9) 14 | line-height 0 -------------------------------------------------------------------------------- /assets/stylus/ui/button.styl: -------------------------------------------------------------------------------- 1 | button 2 | &[disabled] 3 | cursor not-allowed 4 | 5 | .button-plain 6 | background #eee 7 | color #777 8 | padding .4em 1.2em 9 | border 0 10 | box-shadow 0 0 1px #ccc 11 | border-radius 3px 12 | &.active, &:active, &:focus 13 | outline 0 14 | box-shadow 0 0 1px #ccc, inset 0 0 1px #ccc 15 | background #fcfcfc 16 | color #333 17 | 18 | $basic_button 19 | padding .4em 1.2em 20 | border 0 21 | outline 0 22 | border-radius 0 23 | box-shadow none 24 | cursor pointer 25 | &:active, &:focus 26 | box-shadow inset 0 1px 2px rgba(0, 0, 0, .3) 27 | &:hover 28 | opacity .9 29 | 30 | .button-default 31 | @extends $basic_button 32 | background #fff 33 | border 1px solid #eee 34 | 35 | .button-primary 36 | @extends $basic_button 37 | background lightseagreen 38 | border 1px solid lightseagreen 39 | color #fff 40 | 41 | .button-warning 42 | @extends $basic_button 43 | background #e74c3c 44 | color #fff 45 | border 1px solid #e74c3c 46 | 47 | .button-radio 48 | background #eee 49 | @extends $basic_button 50 | border-radius 3px 51 | &.active, &:active, &:focus 52 | box-shadow inset 0 1px 3px darken(lightseagreen, 30%) 53 | color #fff 54 | background lightseagreen 55 | 56 | 57 | .button-group 58 | button 59 | &:first-child 60 | border-radius 3px 0 0 3px 61 | &.active, &:active, &:focus 62 | box-shadow 0 0 1px #ccc, inset 0 0 1px #ccc, inset 1px 0 #ddd 63 | &:last-child 64 | border-radius 0 3px 3px 0 65 | &.active, &:active, &:focus 66 | box-shadow 0 0 1px #ccc, inset 0 0 1px #ccc, inset -1px 0 #ddd 67 | 68 | .buttons-group 69 | button 70 | &:first-child 71 | margin-right 1em -------------------------------------------------------------------------------- /assets/stylus/ui/card.styl: -------------------------------------------------------------------------------- 1 | .ui-card 2 | padding 1.5em 2em 3 | background #fff 4 | box-shadow 0 0 3px #ccc 5 | color #333 6 | display inline-block -------------------------------------------------------------------------------- /assets/stylus/ui/form.styl: -------------------------------------------------------------------------------- 1 | input 2 | &.ng-invalid 3 | color crimson !important -------------------------------------------------------------------------------- /assets/stylus/ui/loading.styl: -------------------------------------------------------------------------------- 1 | @import "../lib/vendor" 2 | 3 | .loading 4 | width 50px 5 | height 30px 6 | text-align center 7 | font-size 10px 8 | position fixed 9 | // top 5px 10 | // right 5px 11 | top 50% 12 | left 50% 13 | transform(translate(-50%, -50%)) 14 | background rgba(0,0,0,.4) 15 | padding 80px 16 | border-radius 10px 17 | z-index 1099 18 | opacity .8 19 | transition(opacity 1s) 20 | &.fadeOut 21 | opacity 0 22 | 23 | 24 | .loading > div 25 | background-color #fff 26 | height 100% 27 | width 6px 28 | margin-right 2px 29 | display inline-block 30 | -webkit-animation stretchdelay 1.2s infinite ease-in-out 31 | animation stretchdelay 1.2s infinite ease-in-out 32 | transform translateZ(0) 33 | 34 | 35 | .loading .rect2 36 | -webkit-animation-delay -1.1s 37 | animation-delay -1.1s 38 | 39 | 40 | .loading .rect3 41 | -webkit-animation-delay -1.0s 42 | animation-delay -1.0s 43 | 44 | 45 | .loading .rect4 46 | -webkit-animation-delay -0.9s 47 | animation-delay -0.9s 48 | 49 | 50 | .loading .rect5 51 | -webkit-animation-delay -0.8s 52 | animation-delay -0.8s 53 | 54 | @keyframes stretchdelay 55 | 0%, 40%, 100% 56 | transform(scaleY(.4)) 57 | 20% 58 | transform(scaleY(1.0)) -------------------------------------------------------------------------------- /assets/stylus/ui/modal.styl: -------------------------------------------------------------------------------- 1 | @import "../lib/vendor"; 2 | 3 | $zindex-modal-background ?= 1040; 4 | // Modals 5 | // ------------------------- 6 | $modal-inner-padding ?= 20px; 7 | 8 | $modal-title-padding ?= 15px; 9 | $modal-title-line-height ?= $line-height-base; 10 | 11 | $modal-content-bg ?= #fff; 12 | $modal-content-border-color ?= rgba(0,0,0,.2); 13 | $modal-content-fallback-border-color ?= #999; 14 | 15 | $modal-backdrop-bg ?= rgba(0, 0, 0, .3); 16 | $modal-header-border-color ?= #e5e5e5; 17 | $modal-footer-border-color ?= $modal-header-border-color; 18 | // 19 | // Modals 20 | // -------------------------------------------------- 21 | 22 | // .modal-open - body class for killing the scroll 23 | // .modal - container to scroll within 24 | // .modal-dialog - positioning shell for the actual modal 25 | // .modal-content - actual modal w/ bg and corners and shit 26 | 27 | // Kill the scroll on the body 28 | .modal-open { 29 | overflow: hidden; 30 | 31 | 32 | // Account for hiding of scrollbar 33 | body&, 34 | .navbar-fixed-top, 35 | .navbar-fixed-bottom { 36 | // margin-right: 15px 37 | } 38 | } 39 | 40 | // Container that the modal scrolls within 41 | .modal 42 | display: none; 43 | overflow: auto; 44 | overflow-y: scroll; 45 | position: fixed; 46 | top: 0; 47 | right: 0; 48 | bottom: 0; 49 | left: 0; 50 | z-index: $zindex-modal-background; 51 | 52 | // When fading in the modal, animate it to slide down 53 | &.fade .modal-dialog 54 | // transform(translate(0, -25%)); 55 | position absolute 56 | // top 35% 57 | top 50% 58 | left 50% 59 | // transform(translate(-50%, -50%)); 60 | transform(translate(-50%, -50%)) 61 | transition-transform(0.3s ease-out); 62 | min-width 60% 63 | 64 | &.in 65 | .modal-dialog 66 | translate(0, 0) 67 | 68 | // Shell div to position the modal with bottom padding 69 | .modal-dialog { 70 | margin-left: auto; 71 | margin-right: auto; 72 | width: auto; 73 | padding: 10px; 74 | z-index: ($zindex-modal-background + 10); 75 | } 76 | 77 | // Actual modal 78 | .modal-content { 79 | position: relative; 80 | background-color: $modal-content-bg; 81 | border-radius: 5px; 82 | box-shadow: 0 0px 40px rgba(0,0,0,.6); 83 | overflow: hidden 84 | background-clip: padding-box; 85 | // Remove focus outline from opened modal 86 | outline: none; 87 | } 88 | 89 | // Modal background 90 | .modal-backdrop 91 | position: fixed; 92 | top: 0; 93 | right: 0; 94 | bottom: 0; 95 | left: 0; 96 | z-index: ($zindex-modal-background - 10); 97 | background-color: $modal-backdrop-bg; 98 | // Fade for backdrop 99 | &.fade 100 | opacity(0); 101 | &.in 102 | opacity(.5); 103 | 104 | // Modal header 105 | // Top section of the modal w/ title and dismiss 106 | .modal-header { 107 | padding: $modal-title-padding; 108 | // border-bottom: 1px solid $modal-header-border-color; 109 | // min-height: ($modal-title-padding + $modal-title-line-height); 110 | } 111 | // Close icon 112 | .modal-header .close { 113 | margin-top: -2px; 114 | } 115 | 116 | // Title text within header 117 | .modal-title { 118 | margin: 0; 119 | line-height: $modal-title-line-height; 120 | } 121 | 122 | // Modal body 123 | // Where all modal content resides (sibling of .modal-header and .modal-footer) 124 | .modal-body { 125 | position: relative; 126 | padding: $modal-inner-padding; 127 | } 128 | 129 | // Footer (for actions) 130 | .modal-footer { 131 | margin-top: 15px; 132 | padding: ($modal-inner-padding - 1) $modal-inner-padding $modal-inner-padding; 133 | text-align: right; // right align buttons 134 | border-top: 1px solid $modal-footer-border-color; 135 | background: #f9f9f9; 136 | // clearfix(); // clear it in case folks use .pull-* classes on buttons 137 | 138 | // Properly space out buttons 139 | .btn + .btn { 140 | margin-left: 5px; 141 | margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs 142 | } 143 | // but override that for button groups 144 | .btn-group .btn + .btn { 145 | margin-left: -1px; 146 | } 147 | // and override it for block buttons as well 148 | .btn-block + .btn-block { 149 | margin-left: 0; 150 | } 151 | } 152 | 153 | // Scale up the modal 154 | @media $media-screen-min-tablet { 155 | 156 | .modal-dialog { 157 | left: 50%; 158 | right: auto; 159 | width: 600px; 160 | padding-top: 30px; 161 | padding-bottom: 30px; 162 | } 163 | .modal-content { 164 | box-shadow 0 5px 15px rgba(0,0,0,.5); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /assets/stylus/ui/reset.styl: -------------------------------------------------------------------------------- 1 | html, body 2 | padding 0 3 | margin 0 4 | 5 | button 6 | margin 0 7 | 8 | input::-webkit-outer-spin-button, 9 | input::-webkit-inner-spin-button 10 | -webkit-appearance none 11 | margin 0 -------------------------------------------------------------------------------- /assets/stylus/ui/typography.styl: -------------------------------------------------------------------------------- 1 | $center 2 | max-width 600px 3 | margin-left auto 4 | margin-right auto 5 | 6 | div, ul, li, p, article, h1, h2, h3, h4, h5, h6 7 | header, nav, form 8 | word-wrap break-word 9 | 10 | .typo-center 11 | @extends $center 12 | 13 | .content 14 | & > h2, & > h3, & > h4, & > h5, & > h6, 15 | & > p, & > ul, & > ol, 16 | & > pre code, & > blockquote, 17 | & > hr 18 | @extends $center 19 | 20 | .content 21 | p, & > ol, & > ul, blockquote 22 | font-size 15px 23 | line-height 1.65 24 | hr 25 | border 0 26 | border-top 1px dashed #aaa 27 | margin-top 3em 28 | margin-bottom 3em 29 | a 30 | text-decoration none 31 | color #00B7FF 32 | &:hover 33 | background-color #00B7FF 34 | color #fff 35 | img 36 | max-width 100% 37 | blockquote 38 | //background #fdfdfd 39 | font-size 16px 40 | // padding 1.6em 1.8em 1.6em 3.3em 41 | padding 0.8em 0 .8em 3em 42 | //box-shadow 0 0 2px #ccc 43 | // border-left 3px solid #ccc 44 | // border-top 1px solid #eee 45 | // border-bottom 1px solid #eee 46 | // border 1px solid #eee 47 | box-sizing border-box 48 | position relative 49 | // background #fcfcfc 50 | // background #f1f1f1 51 | // border-radius 3px 52 | // box-shadow 1px 1px 0 #ccc 53 | border-left 3px solid #ccc 54 | overflow hidden 55 | font-style italic 56 | &:before 57 | // content "“" 58 | position absolute 59 | top .2em 60 | left -.1em 61 | font-size 3em 62 | width 1em 63 | height 1em 64 | border-radius 1em 65 | line-height 1.5em 66 | text-align center 67 | color #FFF 68 | background-color #dedede 69 | font-style normal 70 | // background-color #cadab8 71 | pre 72 | padding 1em calc(50% \- 300px) 73 | code 74 | background #eee 75 | border 1px solid #ccc 76 | border-radius 3px 77 | padding 0 .3em 78 | 79 | pre 80 | background transparent 81 | code.hljs 82 | background transparent 83 | -------------------------------------------------------------------------------- /assets/stylus/widgets/book-3d.styl: -------------------------------------------------------------------------------- 1 | @import '../lib/vendor' 2 | 3 | .book-large 4 | display inline-block 5 | box-shadow 10px 4px 5px rgba(0, 0, 0, .08), -10px 20px 12px rgba(0, 0, 0, .15) //, inset 0 0 1px #000 6 | position relative 7 | font-size 0 8 | &:before 9 | content "" 10 | position absolute 11 | top 1px 12 | bottom 1px 13 | left 1% 14 | width 1px 15 | box-shadow 10px 0 6px #111 16 | img 17 | position relative 18 | z-index -1 19 | 20 | cube_l = 300px 21 | cube_w = 40px 22 | cube_h = 400px 23 | 24 | .cubic-view 25 | perspective 1800px 26 | 27 | .book-cube 28 | transform-style preserve-3d 29 | position relative 30 | transform-origin top left 31 | transition -webkit-transform .5s 32 | transition -moz-transform .5s 33 | transition transform .5s 34 | min-width cube_l 35 | min-height cube_h 36 | display inline-block 37 | transform rotateY(0) scale(.9) 38 | &.sided 39 | transform rotateY(35deg) scale(.9) 40 | &:hover 41 | transform rotateY(0) scale(.9) 42 | div 43 | position absolute 44 | // background-color rgba(0, 0, 0, .3) 45 | .book-front, .book-back 46 | width cube_l 47 | height cube_h 48 | .book-left, .book-right 49 | width cube_w 50 | height cube_h 51 | left -(cube_w/2) 52 | .book-top, .book-bottom 53 | width cube_l - 5px 54 | height cube_w 55 | top -15px 56 | .book-back 57 | transform rotateY(-180deg) translate3d(0,0,20px) 58 | .book-right 59 | transform rotateY(90deg) translate3d(0, 0, 295px) 60 | .book-left 61 | transform rotateY(-90deg) 62 | .book-top 63 | transform rotateX(90deg) 64 | .book-bottom 65 | transform rotateX(-90deg) translate3d(0,0,390px) 66 | .book-front 67 | transform translate3d(0,0,20px) 68 | .book-front, .book-left 69 | background-size cover 70 | .book-front 71 | box-shadow inset 4px 0 10px rgba(0, 0, 0, 0.1) 72 | -webkit-transform-style preserve-3d 73 | -webkit-backface-visibility hidden 74 | &:after 75 | content "" 76 | position absolute 77 | top 0 78 | left 10px 79 | bottom 0 80 | width 3px 81 | background rgba(0, 0, 0, 0.06) 82 | box-shadow 1px 0 3px rgba(255, 255, 255, 0.1) 83 | & > div 84 | position absolute 85 | width 100% 86 | height 100% 87 | -webkit-transform-style preserve-3d 88 | -webkit-backface-visibility hidden 89 | .book-cover-frontface 90 | background-size cover 91 | background-position center 92 | .book-cover-backface 93 | transform(rotateY(180deg)) 94 | .book-back 95 | box-shadow 10px 10px 30px rgba(0, 0, 0, 0.3) 96 | .book-left 97 | background-color #fff 98 | overflow hidden 99 | &:before 100 | content "" 101 | position absolute 102 | width cube_w 103 | height cube_h 104 | top 0 105 | left 0 106 | background-color rgba(0, 0, 0, .2) 107 | backface-visibility hidden 108 | .book-title 109 | -webkit-writing-mode vertical-rl 110 | width cube_w 111 | line-height cube_w 112 | font-size 1.8em 113 | //white-space pre 114 | // padding 1em 0 115 | height "calc(%s - 2em)" % cube_h 116 | overflow hidden 117 | position relative 118 | top 1em 119 | .book-page 120 | width cube_l 121 | height cube_h 122 | background #fff 123 | // line-height cube_h 124 | text-align center 125 | transform translate3d(0,0,19px) 126 | background #f1f1f1 127 | .page-content 128 | opacity 0 129 | position relative 130 | top 50% 131 | left 50% 132 | width 50% 133 | transform translate(-50%, -50%) 134 | transition opacity 1s 135 | .summary 136 | color #666 137 | margin 0 -2em 1em 138 | text-align left 139 | overflow hidden 140 | text-overflow ellipsis 141 | display -webkit-box 142 | -webkit-line-clamp 5 143 | -webkit-box-orient vertical 144 | a 145 | display block 146 | color #666 147 | // color lightseagreen 148 | padding .8em 1.2em 149 | // border 2px solid lightseagreen 150 | text-decoration none 151 | margin-bottom 1em 152 | transition all .5s 153 | border-radius 3px 154 | &:hover 155 | // background lightseagreen 156 | color #fff 157 | background #222 158 | .book-left 159 | .book-title 160 | opacity 1 161 | transition opacity 1s 162 | .book-front 163 | transition-transform 1s 164 | transform-origin top left 165 | cursor pointer 166 | transform-style preserve-3d 167 | &.opened 168 | transform translate3d(0,0,20px) rotateY(-100deg) 169 | & + .book-page 170 | .page-content 171 | opacity 1 172 | -------------------------------------------------------------------------------- /assets/stylus/widgets/book.styl: -------------------------------------------------------------------------------- 1 | @import '../lib/vendor' 2 | 3 | book_w = 120px 4 | book_h = 160px 5 | 6 | .book 7 | display inline-block 8 | vertical-align top 9 | .cover 10 | position relative 11 | min-width book_w 12 | height book_h 13 | overflow hidden 14 | background-size cover 15 | background-position center 16 | box-shadow 0 2px 3px #ddd 17 | position relative 18 | border-radius: 2px 19 | // &:before 20 | // content "" 21 | // position absolute 22 | // top 0 23 | // bottom 0 24 | // left 0 25 | // width 10px 26 | // background linear-gradient(to right,rgb(255, 255, 255) 0,rgba(255, 255, 255, 0.1) 20%,rgba(0, 0, 0, 0.3) 100%) 27 | // opacity .3 28 | a 29 | text-decoration initial 30 | .book-link 31 | min-width book_w 32 | height book_h 33 | display inline-block 34 | // overflow hidden 35 | // border-radius 0 .3em .3em 0 36 | // box-shadow 2px 2px 2px #DDD, inset 4px 0 0 rgba(0, 0, 0, .1) 37 | box-shadow inset 0 0 1px rgba(#32373A, 0.5), inset 2px 0 5px rgba(#fff, 0.5) 38 | border-radius: 2px 39 | position relative 40 | background-color transparent 41 | transition background-color .3s 42 | &:hover 43 | a 44 | box-shadow initial 45 | .book-link 46 | background-color rgba(0, 0, 0, .65) 47 | .add-note 48 | display block 49 | bottom .7em 50 | opacity 1 51 | .notes-count 52 | top 0 53 | opacity 1 54 | .title, .author 55 | display block 56 | width book_w 57 | margin-top .3em 58 | .title 59 | font-weight bold 60 | .author 61 | font-size .9em 62 | color #ccc 63 | .add-note 64 | border 2px solid #fff 65 | color #fff 66 | font-size 1.5em 67 | text-align center 68 | line-height 1.8em 69 | border-radius 999px 70 | position absolute 71 | z-index 2 72 | bottom -3em 73 | opacity 0 74 | transition bottom .5s, opacity .2s 75 | padding 0 0.5em 76 | left 50% 77 | transform translateZ(0) translateX(-50%) 78 | white-space nowrap 79 | &:hover 80 | background #fff 81 | color #111 82 | .icon-plus 83 | font-size 0.7em 84 | line-height 2.7em 85 | margin-left 0.07em 86 | vertical-align middle 87 | span 88 | font-size 0.6em 89 | margin-left .4em 90 | vertical-align middle 91 | .notes-count 92 | text-shadow 0px 0px 18px #000 93 | position absolute 94 | z-index 2 95 | color #fff 96 | display block 97 | width 100% 98 | text-align center 99 | top -6em 100 | opacity 0 101 | transition top .5s, opacity .2s 102 | transform translateZ(0) 103 | pointer-events none 104 | .count 105 | display block 106 | font-size: 3.6em 107 | font-family: Georgia 108 | .count-text 109 | font-size: 12px 110 | 111 | 112 | .book.empty 113 | .cover 114 | box-shadow initial 115 | border 3px dashed #ccc 116 | box-sizing border-box 117 | cursor pointer 118 | &:before 119 | background transparent 120 | &:after 121 | content "+" 122 | color #bbb 123 | font-size 3em 124 | position absolute 125 | top 0 126 | right 0 127 | bottom 0 128 | left 0 129 | text-align center 130 | line-height book_h - 0.5*14px 131 | // line-height "calc(%s - .5em)" % book_h 132 | &:hover 133 | border-color #aaa 134 | &:after 135 | color #999 136 | -------------------------------------------------------------------------------- /assets/stylus/widgets/editor.styl: -------------------------------------------------------------------------------- 1 | #editor 2 | position relative 3 | form 4 | margin-bottom 100px 5 | .guide 6 | opacity 0 7 | position fixed 8 | top 0 9 | bottom 0 10 | width 20px 11 | border-left 1px solid lightpink 12 | left calc(50% \- 340px) 13 | transition opacity .3s 14 | &:before 15 | content "" 16 | position absolute 17 | top 0 18 | right 0 19 | bottom 0 20 | left -20px 21 | width 20px 22 | &:hover 23 | opacity 1 24 | cursor col-resize 25 | input 26 | border 0 27 | background none 28 | &:focus 29 | outline 0 30 | 31 | .CodeMirror pre.CodeMirror-placeholder 32 | color #999 33 | 34 | 35 | // specific form controls 36 | #title, #page 37 | display block 38 | 39 | #title 40 | font-size 2em 41 | font-weight bold 42 | margin-bottom 1em 43 | width 100% 44 | box-sizing border-box 45 | 46 | #page 47 | font-size 1.1em 48 | margin-bottom 2em 49 | color gray 50 | 51 | #privacy 52 | color gray 53 | input 54 | margin-right .5em 55 | label 56 | margin-right 1em 57 | 58 | #page_no 59 | line-height 1.65 60 | color gray 61 | vertical-align baseline 62 | min-width 12em 63 | 64 | // warning 65 | error_tip() 66 | background rgba(240, 72, 72, 0.8) 67 | color #fff 68 | padding .2em .8em 69 | border-radius 3px 70 | display inline-block 71 | 72 | .warning 73 | error_tip() 74 | opacity 1 75 | transition opacity .2s 76 | &.right 77 | transform translateX(-105%) 78 | &.hidden 79 | transform translateX(-105%) 80 | opacity 0 81 | 82 | .toolbar 83 | background #EEE 84 | padding .6em 1em 85 | border-radius .3em 86 | li 87 | display inline-block 88 | background #fcfcfc 89 | padding .4em 90 | border-radius 2px 91 | cursor pointer 92 | margin-right .6em 93 | font-size .9em 94 | position relative 95 | &:active, &.active 96 | background #ccc 97 | box-shadow inset 0 0 3px #999 98 | #upload 99 | opacity 0 100 | position absolute 101 | top 0 102 | right 0 103 | bottom 0 104 | left 0 105 | -------------------------------------------------------------------------------- /assets/stylus/widgets/note-card.styl: -------------------------------------------------------------------------------- 1 | @require '../fonticon' 2 | 3 | paper(font_size = 1em, line_height = 1.2em, background_color = #fff) 4 | background -webkit-gradient(linear, 0 0, 0 100%, from(#d9eaf3), color-stop(4%, background_color)) 0 (line_height/4) 5 | background -webkit-linear-gradient(top, #d9eaf3 0%, background_color 8%) 0 (line_height/4) 6 | background -moz-linear-gradient(top, #d9eaf3 0%, background_color 8%) 0 (line_height/4) 7 | background -ms-linear-gradient(top, #d9eaf3 0%, background_color 8%) 0 (line_height/4) 8 | background -o-linear-gradient(top, #d9eaf3 0%, background_color 8%) 0 (line_height/4) 9 | background linear-gradient(top, #d9eaf3 0%, background_color 8%) 0 (line_height/4) 10 | background-size 100% 20px 11 | position relative 12 | &:before 13 | content '' 14 | position absolute 15 | width 4px 16 | top 0 17 | left 1.1em 18 | bottom 0 19 | border 1px solid 20 | border-color transparent #efe4e4 21 | h1, h2, h3, h4, h5 22 | line-height line_height // line_height * 2 (stylus bug?) 23 | margin (line_height/2) 0 24 | h1, h2, h3 25 | font-size 1.2em 26 | h4, h5 27 | font-size 1em 28 | font-weight normal 29 | p 30 | line-height 1.44em // line_height * 1.2 31 | 32 | 33 | ul.notes 34 | margin 0 -.8em 35 | 36 | .note 37 | display inline-block 38 | vertical-align top 39 | border-radius .2em 40 | width calc(33% \- 1.6em*4/3) 41 | // width 28% // (100% - 1.6em)/3 42 | min-width 260px 43 | margin .8em 44 | //box-sizing padding-box 45 | //background #f3f3f3 46 | box-shadow 0 0 3px #aaa 47 | paper(1em, 1.2em, #fcfcfc) 48 | // &:before 49 | // content "" 50 | // position absolute 51 | // top 0 52 | // bottom 0 53 | // width 2px 54 | // left 1em 55 | // background-color #FFB6C1 // lightpink 56 | // opacity .3 57 | &:hover 58 | .note-mask 59 | background-color rgba(0, 0, 0, .03) 60 | .note-mask 61 | display block 62 | text-decoration none 63 | color #333 64 | padding .6em 1.3em 1em 2.3em 65 | time 66 | color #aaa 67 | h3 68 | word-wrap break-word 69 | .note-summary 70 | word-wrap break-word 71 | margin 0 0 1em 72 | // margin 0 73 | color #444 74 | .note-page 75 | color #ccc 76 | font-size .8em 77 | position absolute 78 | bottom .2em 79 | right .4em 80 | -------------------------------------------------------------------------------- /assets/stylus/widgets/reader.styl: -------------------------------------------------------------------------------- 1 | @require '../ui/typography' 2 | 3 | #reader 4 | min-height 100% 5 | z-index 2 6 | transition left 1s 7 | background #f7f7f7 8 | &.hidden 9 | left 100% 10 | h1 11 | margin-top 0 12 | @extend $center 13 | 14 | pre 15 | overflow-x scroll 16 | .content 17 | margin-bottom 100px 18 | word-break break-word -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anno", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "simonday@foxmail.com" 6 | ], 7 | "description": "Annotation", 8 | "main": "app.js", 9 | "keywords": [ 10 | "annotation" 11 | ], 12 | "license": "MIT", 13 | "homepage": "http://daix.me", 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests" 20 | ], 21 | "dependencies": { 22 | "angular-route": "~1.2.15", 23 | "angular-cookies": "~1.2.15", 24 | "angular-bootstrap": "*", 25 | "marked": "*", 26 | "codemirror": "*", 27 | "color-thief": "*", 28 | "underscore": "*", 29 | "evernote": "~1.25.2", 30 | "angular-sanitize": "~1.2.15" 31 | }, 32 | "resolutions": { 33 | "angular": "~1.2.15" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anno", 3 | "version": "0.1.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "devDependencies": { 9 | "grunt": "~0.4.2", 10 | "grunt-contrib-stylus": "~0.13.0", 11 | "grunt-contrib-jade": "~0.10.0", 12 | "grunt-contrib-watch": "~0.5.3", 13 | "grunt-contrib-concat": "~0.3.0", 14 | "grunt-contrib-uglify": "~0.4.0", 15 | "grunt-contrib-compress": "~0.7.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/background.js: -------------------------------------------------------------------------------- 1 | chrome.app.runtime.onLaunched.addListener(function() { 2 | chrome.app.window.create('index.html', { 3 | 'minWidth': 860, 4 | 'minHeight': 580, 5 | 'bounds': { 6 | 'width': 960, 7 | 'height': 600 8 | } 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /public/css/codemirror.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------ 2 | CodeMirror 3 | ---------------------------------------------*/ 4 | .CodeMirror { 5 | font-family:Menlo, monospace; 6 | font-size:13px; 7 | line-height:1.4em; 8 | -webkit-font-smoothing:subpixel-antialiased; 9 | } 10 | .code .CodeMirror-scroll { 11 | min-height:360px; 12 | } 13 | .markdown .CodeMirror { 14 | font-size:14px; 15 | line-height:1.75em 16 | } 17 | 18 | .CodeMirror-scroll { 19 | /* Set scrolling behaviour here */ 20 | overflow: auto; 21 | } 22 | 23 | /* PADDING */ 24 | .CodeMirror-scrollbar-filler { 25 | background-color: white; /* The little square between H and V scrollbars */ 26 | } 27 | 28 | /* GUTTER */ 29 | .CodeMirror-linenumbers {} 30 | .CodeMirror-linenumber { 31 | padding:0 3px 0 5px; 32 | min-width:20px; 33 | text-align:right; 34 | color:#d4d7d9; 35 | } 36 | .cm-s-prose-bright .CodeMirror-gutter { 37 | border-right:1px solid #eff3f5; 38 | padding-right:5px; 39 | margin-right:15px; 40 | min-width:2.5em; 41 | } 42 | 43 | /* CURSOR */ 44 | .CodeMirror div.CodeMirror-cursor { 45 | border-left: 1px solid #3D494E; 46 | z-index: 3; 47 | } 48 | .cm-s-prose-bright .CodeMirror-cursor { border-left: 2px solid #667880; } 49 | .cm-s-prose-bright .CodeMirror-lines { margin:0; } 50 | 51 | /* Shown when moving in bi-directional text */ 52 | .CodeMirror div.CodeMirror-secondarycursor { 53 | border-left: 1px solid #e0e7eb; 54 | } 55 | .CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor { 56 | width: auto; 57 | border: 0; 58 | background: #90bb74; 59 | z-index: 1; 60 | } 61 | /* Can style cursor different in overwrite (non-insert) mode */ 62 | .CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {} 63 | .cm-tab { display: inline-block; } 64 | 65 | /* Prose Bright Theme 66 | -------------------- */ 67 | /* Dark Grey */ 68 | .cm-s-prose-bright { color:#3D494E; } 69 | .cm-s-prose-bright span.cm-header { color:#3D494E; font-weight:bold;} 70 | .cm-s-prose-bright span.cm-variable-2 { color:#3D494E; } 71 | 72 | /* Medium Grey */ 73 | .cm-s-prose-bright span.cm-meta { color:#516066; } 74 | .cm-s-prose-bright span.cm-hr { color:#516066;} 75 | 76 | /* Lighter Grey */ 77 | .cm-s-prose-bright span.cm-comment { color:#868f93; } 78 | .cm-s-prose-bright span.cm-qualifier { color:#868f93; } 79 | 80 | /* Dark Blue/Green */ 81 | .cm-s-prose-bright span.cm-number { color:#197987; } 82 | .cm-s-prose-bright span.cm-variable { color:#197987; } 83 | .cm-s-prose-bright span.cm-builtin { color:#197987; } 84 | .cm-s-prose-bright span.cm-link { color:#197987; text-decoration:underline;} 85 | .cm-s-prose-bright span.cm-tag { color:#197987; } 86 | 87 | /* Medium Blue/Green */ 88 | .cm-s-prose-bright span.cm-string { color:#48abb9; } 89 | .cm-s-prose-bright span.cm-string-2 { color:#48abb9; } 90 | .cm-s-prose-bright span.cm-quote { color:#48abb9;} 91 | .cm-s-prose-bright span.cm-atom { color:#48abb9; } 92 | 93 | /* Light Blue/Green */ 94 | /*.cm-s-prose-bright .CodeMirror-selected { background:#90d5df; }*/ 95 | 96 | /* Turtle Green */ 97 | .cm-s-prose-bright span.cm-property { color:#82a367; } 98 | .cm-s-prose-bright span.cm-operator { color:#82a367; } 99 | .cm-s-prose-bright span.cm-variable-3 { color:#82a367; } 100 | 101 | /* Light Turtle Green */ 102 | .cm-s-prose-bright span.cm-attribute { color:#90bb74; } 103 | .cm-s-prose-bright span.cm-def { color:#90bb74; } 104 | 105 | /* Brick */ 106 | .cm-s-prose-bright span.cm-keyword { color:#ec6c45; } 107 | .cm-s-prose-bright span.cm-bracket { color:#ec6c45; } 108 | 109 | /* Darker Brick */ 110 | .cm-s-prose-bright span.cm-error { color:#e45346; } 111 | 112 | .cm-s-prose-bright span.cm-em { font-style:italic; } 113 | .cm-s-prose-bright span.cm-strong { font-weight:bold; } 114 | 115 | /* STOP */ 116 | /* The rest of this file contains styles related to the mechanics of 117 | the editor. You probably shouldn't touch them. */ 118 | 119 | .CodeMirror { 120 | /*line-height: 1;*/ 121 | position:relative; 122 | overflow:hidden; 123 | /* background:white; */ 124 | color:#516066; 125 | } 126 | 127 | .CodeMirror-scroll { 128 | /* 30px is the magic margin used to hide the element's real scrollbars */ 129 | /* See overflow: hidden in .CodeMirror */ 130 | margin-bottom: -30px; margin-right: -30px; 131 | padding-bottom: 30px; padding-right: 30px; 132 | height: 100%; 133 | outline: none; /* Prevent dragging from highlighting the element */ 134 | position: relative; 135 | } 136 | .CodeMirror-sizer { 137 | position: relative; 138 | } 139 | 140 | /* The fake, visible scrollbars. Used to force redraw during scrolling 141 | before actuall scrolling happens, thus preventing shaking and 142 | flickering artifacts. */ 143 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler { 144 | position: absolute; 145 | z-index: 6; 146 | display: none; 147 | } 148 | .CodeMirror-vscrollbar { 149 | right: 0; top: 0; 150 | overflow-x: hidden; 151 | overflow-y: scroll; 152 | } 153 | .CodeMirror-hscrollbar { 154 | bottom: 0; left: 0; 155 | overflow-y: hidden; 156 | overflow-x: scroll; 157 | } 158 | .CodeMirror-scrollbar-filler { 159 | right: 0; bottom: 0; 160 | z-index: 6; 161 | } 162 | 163 | .CodeMirror-gutters { 164 | position: absolute; left: 0; top: 0; 165 | height: 100%; 166 | padding-bottom: 30px; 167 | z-index: 3; 168 | } 169 | .CodeMirror-gutter { 170 | height: 100%; 171 | padding-bottom: 30px; 172 | margin-bottom: -32px; 173 | display: inline-block; 174 | /* Hack to make IE7 behave */ 175 | *zoom:1; 176 | *display:inline; 177 | } 178 | .CodeMirror-gutter-elt { 179 | position: absolute; 180 | cursor: default; 181 | z-index: 4; 182 | } 183 | 184 | .CodeMirror-lines { 185 | cursor: text; 186 | } 187 | .CodeMirror pre { 188 | margin: 0; 189 | white-space: pre; 190 | word-wrap: normal; 191 | z-index: 2; 192 | position: relative; 193 | overflow: visible; 194 | } 195 | .CodeMirror-wrap pre { 196 | word-wrap: break-word; 197 | white-space: pre-wrap; 198 | word-break: normal; 199 | } 200 | .CodeMirror-linebackground { 201 | position: absolute; 202 | left: 0; right: 0; top: 0; bottom: 0; 203 | z-index: 0; 204 | } 205 | 206 | .CodeMirror-linewidget { 207 | position: relative; 208 | z-index: 2; 209 | overflow: auto; 210 | } 211 | 212 | .CodeMirror-widget { 213 | display: inline-block; 214 | } 215 | 216 | .CodeMirror-wrap .CodeMirror-scroll { 217 | overflow-x: hidden; 218 | } 219 | 220 | .CodeMirror-measure { 221 | position: absolute; 222 | width: 100%; height: 0px; 223 | overflow: hidden; 224 | visibility: hidden; 225 | } 226 | .CodeMirror-measure pre { position: static; } 227 | 228 | .CodeMirror div.CodeMirror-cursor { 229 | position: absolute; 230 | visibility: hidden; 231 | border-right: none; 232 | width: 0; 233 | } 234 | .CodeMirror-focused div.CodeMirror-cursor { 235 | visibility: visible; 236 | } 237 | 238 | .CodeMirror-selected { background: #d9d9d9; } 239 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 240 | 241 | .cm-searching { 242 | background: #ffa; 243 | background: rgba(255, 255, 0, .4); 244 | } 245 | 246 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 247 | .CodeMirror span { *vertical-align: text-bottom; } 248 | 249 | @media print { 250 | /* Hide the cursor when printing */ 251 | .CodeMirror div.CodeMirror-cursor { 252 | visibility: hidden; 253 | } 254 | } 255 | 256 | -------------------------------------------------------------------------------- /public/css/themes/solarized_dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | padding: 0.5em; 10 | background: #002b36; 11 | color: #839496; 12 | } 13 | 14 | .hljs-comment, 15 | .hljs-template_comment, 16 | .diff .hljs-header, 17 | .hljs-doctype, 18 | .hljs-pi, 19 | .lisp .hljs-string, 20 | .hljs-javadoc { 21 | color: #586e75; 22 | } 23 | 24 | /* Solarized Green */ 25 | .hljs-keyword, 26 | .hljs-winutils, 27 | .method, 28 | .hljs-addition, 29 | .css .hljs-tag, 30 | .hljs-request, 31 | .hljs-status, 32 | .nginx .hljs-title { 33 | color: #859900; 34 | } 35 | 36 | /* Solarized Cyan */ 37 | .hljs-number, 38 | .hljs-command, 39 | .hljs-string, 40 | .hljs-tag .hljs-value, 41 | .hljs-rules .hljs-value, 42 | .hljs-phpdoc, 43 | .tex .hljs-formula, 44 | .hljs-regexp, 45 | .hljs-hexcolor, 46 | .hljs-link_url { 47 | color: #2aa198; 48 | } 49 | 50 | /* Solarized Blue */ 51 | .hljs-title, 52 | .hljs-localvars, 53 | .hljs-chunk, 54 | .hljs-decorator, 55 | .hljs-built_in, 56 | .hljs-identifier, 57 | .vhdl .hljs-literal, 58 | .hljs-id, 59 | .css .hljs-function { 60 | color: #268bd2; 61 | } 62 | 63 | /* Solarized Yellow */ 64 | .hljs-attribute, 65 | .hljs-variable, 66 | .lisp .hljs-body, 67 | .smalltalk .hljs-number, 68 | .hljs-constant, 69 | .hljs-class .hljs-title, 70 | .hljs-parent, 71 | .haskell .hljs-type, 72 | .hljs-link_reference { 73 | color: #b58900; 74 | } 75 | 76 | /* Solarized Orange */ 77 | .hljs-preprocessor, 78 | .hljs-preprocessor .hljs-keyword, 79 | .hljs-pragma, 80 | .hljs-shebang, 81 | .hljs-symbol, 82 | .hljs-symbol .hljs-string, 83 | .diff .hljs-change, 84 | .hljs-special, 85 | .hljs-attr_selector, 86 | .hljs-subst, 87 | .hljs-cdata, 88 | .clojure .hljs-title, 89 | .css .hljs-pseudo, 90 | .hljs-header { 91 | color: #cb4b16; 92 | } 93 | 94 | /* Solarized Red */ 95 | .hljs-deletion, 96 | .hljs-important { 97 | color: #dc322f; 98 | } 99 | 100 | /* Solarized Violet */ 101 | .hljs-link_label { 102 | color: #6c71c4; 103 | } 104 | 105 | .tex .hljs-formula { 106 | background: #073642; 107 | } 108 | -------------------------------------------------------------------------------- /public/css/themes/tomorrow.css: -------------------------------------------------------------------------------- 1 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 2 | 3 | /* Tomorrow Comment */ 4 | .hljs-comment, 5 | .hljs-title { 6 | color: #8e908c; 7 | } 8 | 9 | /* Tomorrow Red */ 10 | .hljs-variable, 11 | .hljs-attribute, 12 | .hljs-tag, 13 | .hljs-regexp, 14 | .ruby .hljs-constant, 15 | .xml .hljs-tag .hljs-title, 16 | .xml .hljs-pi, 17 | .xml .hljs-doctype, 18 | .html .hljs-doctype, 19 | .css .hljs-id, 20 | .css .hljs-class, 21 | .css .hljs-pseudo { 22 | color: #c82829; 23 | } 24 | 25 | /* Tomorrow Orange */ 26 | .hljs-number, 27 | .hljs-preprocessor, 28 | .hljs-pragma, 29 | .hljs-built_in, 30 | .hljs-literal, 31 | .hljs-params, 32 | .hljs-constant { 33 | color: #f5871f; 34 | } 35 | 36 | /* Tomorrow Yellow */ 37 | .ruby .hljs-class .hljs-title, 38 | .css .hljs-rules .hljs-attribute { 39 | color: #eab700; 40 | } 41 | 42 | /* Tomorrow Green */ 43 | .hljs-string, 44 | .hljs-value, 45 | .hljs-inheritance, 46 | .hljs-header, 47 | .ruby .hljs-symbol, 48 | .xml .hljs-cdata { 49 | color: #718c00; 50 | } 51 | 52 | /* Tomorrow Aqua */ 53 | .css .hljs-hexcolor { 54 | color: #3e999f; 55 | } 56 | 57 | /* Tomorrow Blue */ 58 | .hljs-function, 59 | .python .hljs-decorator, 60 | .python .hljs-title, 61 | .ruby .hljs-function .hljs-title, 62 | .ruby .hljs-title .hljs-keyword, 63 | .perl .hljs-sub, 64 | .javascript .hljs-title, 65 | .coffeescript .hljs-title { 66 | color: #4271ae; 67 | } 68 | 69 | /* Tomorrow Purple */ 70 | .hljs-keyword, 71 | .javascript .hljs-function { 72 | color: #8959a8; 73 | } 74 | 75 | .hljs { 76 | display: block; 77 | background: white; 78 | color: #4d4d4c; 79 | padding: 0.5em; 80 | } 81 | 82 | .coffeescript .javascript, 83 | .javascript .xml, 84 | .tex .hljs-formula, 85 | .xml .javascript, 86 | .xml .vbscript, 87 | .xml .css, 88 | .xml .hljs-cdata { 89 | opacity: 0.5; 90 | } 91 | -------------------------------------------------------------------------------- /public/css/themes/zenburn.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Zenburn style from voldmar.ru (c) Vladimir Epifanov 4 | based on dark.css by Ivan Sagalaev 5 | 6 | */ 7 | 8 | .hljs { 9 | display: block; padding: 0.5em; 10 | background: #3F3F3F; 11 | color: #DCDCDC; 12 | } 13 | 14 | .hljs-keyword, 15 | .hljs-tag, 16 | .css .hljs-class, 17 | .css .hljs-id, 18 | .lisp .hljs-title, 19 | .nginx .hljs-title, 20 | .hljs-request, 21 | .hljs-status, 22 | .clojure .hljs-attribute { 23 | color: #E3CEAB; 24 | } 25 | 26 | .django .hljs-template_tag, 27 | .django .hljs-variable, 28 | .django .hljs-filter .hljs-argument { 29 | color: #DCDCDC; 30 | } 31 | 32 | .hljs-number, 33 | .hljs-date { 34 | color: #8CD0D3; 35 | } 36 | 37 | .dos .hljs-envvar, 38 | .dos .hljs-stream, 39 | .hljs-variable, 40 | .apache .hljs-sqbracket { 41 | color: #EFDCBC; 42 | } 43 | 44 | .dos .hljs-flow, 45 | .diff .hljs-change, 46 | .python .exception, 47 | .python .hljs-built_in, 48 | .hljs-literal, 49 | .tex .hljs-special { 50 | color: #EFEFAF; 51 | } 52 | 53 | .diff .hljs-chunk, 54 | .hljs-subst { 55 | color: #8F8F8F; 56 | } 57 | 58 | .dos .hljs-keyword, 59 | .python .hljs-decorator, 60 | .hljs-title, 61 | .haskell .hljs-type, 62 | .diff .hljs-header, 63 | .ruby .hljs-class .hljs-parent, 64 | .apache .hljs-tag, 65 | .nginx .hljs-built_in, 66 | .tex .hljs-command, 67 | .hljs-prompt { 68 | color: #efef8f; 69 | } 70 | 71 | .dos .hljs-winutils, 72 | .ruby .hljs-symbol, 73 | .ruby .hljs-symbol .hljs-string, 74 | .ruby .hljs-string { 75 | color: #DCA3A3; 76 | } 77 | 78 | .diff .hljs-deletion, 79 | .hljs-string, 80 | .hljs-tag .hljs-value, 81 | .hljs-preprocessor, 82 | .hljs-pragma, 83 | .hljs-built_in, 84 | .sql .hljs-aggregate, 85 | .hljs-javadoc, 86 | .smalltalk .hljs-class, 87 | .smalltalk .hljs-localvars, 88 | .smalltalk .hljs-array, 89 | .css .hljs-rules .hljs-value, 90 | .hljs-attr_selector, 91 | .hljs-pseudo, 92 | .apache .hljs-cbracket, 93 | .tex .hljs-formula, 94 | .coffeescript .hljs-attribute { 95 | color: #CC9393; 96 | } 97 | 98 | .hljs-shebang, 99 | .diff .hljs-addition, 100 | .hljs-comment, 101 | .java .hljs-annotation, 102 | .hljs-template_comment, 103 | .hljs-pi, 104 | .hljs-doctype { 105 | color: #7F9F7F; 106 | } 107 | 108 | .coffeescript .javascript, 109 | .javascript .xml, 110 | .tex .hljs-formula, 111 | .xml .javascript, 112 | .xml .vbscript, 113 | .xml .css, 114 | .xml .hljs-cdata { 115 | opacity: 0.5; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /public/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/public/fonts/icomoon.eot -------------------------------------------------------------------------------- /public/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/public/fonts/icomoon.ttf -------------------------------------------------------------------------------- /public/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/public/fonts/icomoon.woff -------------------------------------------------------------------------------- /public/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/public/img/bg.png -------------------------------------------------------------------------------- /public/img/books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/public/img/books.png -------------------------------------------------------------------------------- /public/img/fabric_of_squares_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/public/img/fabric_of_squares_gray.png -------------------------------------------------------------------------------- /public/img/login_with_douban_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/public/img/login_with_douban_32.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 读书笔记
-------------------------------------------------------------------------------- /public/js/app.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var $=function(selector){return"string"==typeof selector?angular.element(document.querySelectorAll(selector)):1===selector.nodeType?angular.element(selector):null},app=angular.module("ANNO",["ngRoute","ANNO.controllers","ANNO.directives","ANNO.ui-directives","ui.bootstrap"]).config(["$routeProvider",function($routeProvider){$routeProvider.when("/login",{templateUrl:"/partials/login.html",controller:"LoginCtrl"}).when("/",{templateUrl:"/partials/books.html",controller:"BooksCtrl"}).when("/:uid/book/:bid",{templateUrl:"/partials/book.html",controller:"BookCtrl"}).when("/:uid/book/:bid/new",{templateUrl:"/partials/edit.html",controller:"EditorCtrl"}).when("/:uid/info",{templateUrl:"/partials/info.html",controller:"InfoCtrl"}).when("/note/:nid",{templateUrl:"/partials/note.html",controller:"NoteCtrl"}).when("/note/:nid/edit",{templateUrl:"/partials/edit.html",controller:"EditorCtrl"}).when("/friends",{templateUrl:"/partials/friends.html",controller:"FriendsCtrl"}).when("/fav",{templateUrl:"/partials/fav.html",controller:"FavCtrl"}).when("/about",{templateUrl:"/partials/about.html"}).when("/:uid/",{templateUrl:"/partials/books.html",controller:"BooksCtrl"}).otherwise({templateUrl:"/partials/error.html"})}]).config(["$httpProvider",function($httpProvider){$httpProvider.interceptors.push("HttpLoadingIntercepter"),$httpProvider.interceptors.push("HttpOAuthIntercepter")}]).config(["$compileProvider",function($compileProvider){$compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|blob):|data:image\//)}]).run(function($route,$rootScope,$location,UserService){$rootScope.$on("$routeChangeStart",function(event,next){"/partials/about.html"!=next.templateUrl&&"/partials/login.html"!=next.templateUrl&&UserService.isLoggedIn().catch(function(){$location.path("/login")})})}); -------------------------------------------------------------------------------- /public/js/directives.min.js: -------------------------------------------------------------------------------- 1 | "use strict";angular.module("ANNO.directives",[]).directive("inputEnter",function(){var ENTER_KEY=13;return function(scope,elem,attrs){elem.bind("keydown",function(event){event.keyCode===ENTER_KEY&&scope.$apply(attrs.inputEnter)})}}).directive("editor",["$modal",function($modal){return{restrict:"E",replace:!0,templateUrl:"/partials/widgets/editor.html",scope:{article:"=",images:"="},controller:function($scope){var utils={bold:function(s,editor){editor.replaceSelection("*"===s.charAt(0)&&s.charAt(s.length-1==="*")?s.replace(/\*/g,""):"**"+s.replace(/\*/g,"")+"**")},italic:function(s,editor){editor.replaceSelection("_"===s.charAt(0)&&s.charAt(s.length-1==="_")?s.replace(/_/g,""):"_"+s.replace(/_/g,"")+"_")},quote:function(s,editor){editor.replaceSelection(">"===s.charAt(0)?util.lTrim(s.replace(/\>/g,"")):"> "+s.replace(/\>/g,""))}},self=this;this.codeMirrorLoad=function(editor){$scope.bold=function(){""!==editor.getSelection()&&utils.bold(editor.getSelection(),editor)},$scope.italic=function(){""!==editor.getSelection()&&utils.italic(editor.getSelection(),editor)},$scope.quote=function(){""!==editor.getSelection()&&utils.quote(editor.getSelection(),editor)},self.uploadPhoto=function(dataURL,file){$scope.article.last_photo=$scope.article.last_photo||0,++$scope.article.last_photo,$scope.images.push({name:"pic"+$scope.article.last_photo,src:dataURL,obj:file}),editor.replaceSelection("<图片"+$scope.article.last_photo+">")},$scope.toggleEditorHelp=function(){$modal.open({templateUrl:"modalEditorHelp.html",controller:function($scope,$modalInstance){$scope.ok=function(){$modalInstance.close()}}})}}}}}]).directive("codemirror",function(){return{restrict:"E",require:["^editor","ngModel"],replace:!0,link:function(scope,elem,attrs,ctrls){var editorCtrl=ctrls[0],ngModel=ctrls[1],editor=new CodeMirror(elem[0],{mode:"gfm",lineWrapping:!0,lineNumbers:!1,matchBrackets:!0,autofocus:!0,theme:"prose-bright",placeholder:"开始写笔记"});editor.refresh(),editor.on("change",function(cm){var phase=scope.$root.$$phase;"$apply"==phase||"$digest"==phase?ngModel.$setViewValue(cm.getValue()):scope.$apply(function(){ngModel.$setViewValue(cm.getValue())})}),ngModel.$render=function(){ngModel.$viewValue&&editor.setValue(ngModel.$viewValue)},editorCtrl.codeMirrorLoad(editor)}}}).directive("filereader",function(){return{restrict:"A",require:"^editor",link:function(scope,elem,attrs,editorCtrl){var reader=new FileReader;reader.onload=function(e){editorCtrl.uploadPhoto(e.target.result,elem[0].files[0])},elem.on("change",function(){reader.readAsDataURL(elem[0].files[0])})}}}).directive("reader",function(){return{restrict:"E",replace:!0,templateUrl:"/partials/widgets/reader.html",scope:{article:"=",images:"="},controller:function($scope){this.images=$scope.images}}}).directive("previewer",["$timeout","$http",function($timeout,$http){var imageCache={};return{restrict:"E",scope:{markdown:"=",images:"="},template:'
{{markdown}}
',replace:!0,require:"^?reader",link:function(scope,elem){var renderer=new marked.Renderer;renderer.image=function(href,title,text){var out=''+text+'":">"},renderer.link=function(href,title,text){if(this.options.sanitize){try{var prot=decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return""}if(0===prot.indexOf("javascript:"))return""}var out='"},marked.setOptions({renderer:renderer,gfm:!0,tables:!0,breaks:!0,pedantic:!1,sanitize:!0,smartLists:!0,smartypants:!1}),scope.$watch("markdown",function(text){if(!_.isUndefined(text)){elem.html(marked(text));var MIME={c:"text/x-csrc",cpp:"text/x-c++src",csharp:"text/x-csharp",java:"text/x-java",lisp:"text/x-common-lisp"};Array.prototype.slice.call(elem.find("pre"),0).forEach(function(block){var $block=$(block),$code=$block.find("code");if($code.attr("class")){var mode=$code.attr("class").slice(5);mode=MIME[mode]||mode,CodeMirror.runMode($block.text(),mode,block),$block.addClass("cm-s-prose-bright")}}),Array.prototype.slice.call(elem.find("img"),0).forEach(function(img){var $img=$(img),source=$img.attr("data-src");imageCache[source]?$img.attr("src",imageCache[source]):$http.get($img.attr("data-src"),{responseType:"blob",cache:!0}).success(function(blob){imageCache[source]=window.URL.createObjectURL(blob),Array.prototype.slice.call(elem.find("img"),0).forEach(function(new_img){$(new_img).attr("src",imageCache[source])})})}),elem.html(elem.html().replace(/\<图片(\d+)\>/g,function(wholeMatch,m1){var img=_.findWhere(scope.images,{name:"pic"+m1});return''}))}})}}}]).directive("loader",["$rootScope","$timeout",function($rootScope,$timeout){return function(scope,elem){scope.$on("loading:show",function(){elem.css("display","block").removeClass("fadeOut")}),scope.$on("loading:hide",function(){elem.addClass("fadeOut"),$timeout(function(){elem.css("display","none")},1e3)})}}]).directive("alert",["$rootScope","$timeout","$compile","AnalyticsService",function($rootScope,$timeout,$compile,AnalyticsService){return function(scope,elem){var ERROR={103:'访问令牌出错,需要重新授权登录',106:'访问令牌过期,需要重新登录',111:"访问太频繁,超出第三方应用限额",999:"未知错误",1000:'这篇笔记是私密的,只能阅读到简介部分,不能显示全文。点我返回书架',1001:'内容不存在,回到我的书架',1002:"必要的信息还没填完,请检查标题和内容是否填写",1003:"上传图片太大,不能大于3M",1004:"有违禁词",1005:"输入的正文太短,要求超过15个字",1008:"不支持上传图片的格式"};scope.$on("alert:error",function(e,data){var msg=ERROR[data.code];msg||(msg=data.code?"你遇到了编号为"+data.code+'的错误"'+data.msg:'发生了奇怪的错误,可能是服务器不稳定,稍后再试试吧。告诉管理员让他修复错误吧。'),elem.removeClass("success").addClass("error").addClass("show").html(msg);var link=elem.find("a");link.length&&link.replaceWith($compile(link[0].outerHTML)(scope)),AnalyticsService.reportError({id:data.code,msg:data.msg})}),scope.$on("alert:success",function(e,msg){elem.removeClass("error").addClass("success").addClass("show").html(msg),$timeout(function(){scope.$emit("alert:dismiss")},2e3)}),scope.$on("alert:dismiss",function(){elem.removeClass("show")}),elem.on("click",function(){elem.removeClass("show")}),scope.$on("alert:error:hide",elem.removeClass.bind(elem,"show")),$rootScope.$on("$routeChangeStart",scope.$emit.bind(scope,"alert:error:hide"))}}]).directive("colorThief",["$http",function($http){return{restrict:"A",require:"ngModel",link:function(scope,elem,attrs,ngModel){var colorThief=new ColorThief,sourceImage=new Image;scope.$watch(attrs.image,function(url){url&&($http.get(url,{responseType:"blob",cache:"true"}).success(function(blob){sourceImage.src=window.URL.createObjectURL(blob)}),sourceImage.onload=function(){var rgb=colorThief.getColor(sourceImage),luma=.2126*rgb[0]+.7152*rgb[1]+.0722*rgb[2];ngModel.$setViewValue({background:"rgb("+rgb.join(",")+")",foreground:luma>192?"#000":"#fff"})})})}}}]).directive("link",function($location){return function(scope,element,attrs){element.bind("click",function(){scope.$apply($location.path(attrs.link))})}}).directive("compile",["$compile",function($compile){return function(scope,element,attrs){scope.$watch(function(scope){return scope.$eval(attrs.compile)},function(value){element.html(value),$compile(element.contents())(scope)})}}]).directive("remoteImage",["$http",function($http){function fetchImage(scope,elem,url){$http.get(url,{responseType:"blob",cache:!0}).success(function(blob){"IMG"===elem[0].nodeName?elem.attr("src",window.URL.createObjectURL(blob)):elem.css({"background-image":"url("+window.URL.createObjectURL(blob)+")"})})}return{link:function(scope,elem,attrs){scope.$watch(attrs.remoteImage,function(url){url&&fetchImage(scope,elem,url)})}}}]); -------------------------------------------------------------------------------- /public/js/douban-auth.min.js: -------------------------------------------------------------------------------- 1 | var DoubanAuth=function(){"use strict";var conf=DoubanAuthConf,redirectURL="https://"+chrome.runtime.id+".chromiumapp.org/auth/callback",redirectRe=new RegExp(redirectURL+"[#?](.*)"),access_token=null;return{getToken:function(callback){function parseRedirectFragment(fragment){var pairs=fragment.split(/&/),values={};return pairs.forEach(function(pair){var nameval=pair.split(/=/);values[nameval[0]]=nameval[1]}),values}function handleProviderResponse(values){console.log("providerResponse",values),values.hasOwnProperty("access_token")?setAccessToken(values.access_token):values.hasOwnProperty("code")?exchangeCodeForToken(values.code):callback(new Error("Neither access_token nor code avialable."))}function exchangeCodeForToken(code){var xhr=new XMLHttpRequest;xhr.open("POST","https://www.douban.com/service/auth2/token?client_id="+conf.api_key+"&client_secret="+conf.api_secret+"&redirect_uri="+redirectURL+"&grant_type=authorization_code&code="+code),xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),xhr.setRequestHeader("Accept","application/json"),xhr.onload=function(){if(200===this.status){var response=JSON.parse(this.responseText);console.log(response),response.hasOwnProperty("access_token")?setAccessToken(response.access_token):callback(new Error("Cannot obtain access_token from code."))}else console.log("code exchange status:",this.status),callback(new Error("Code exchange failed"))},xhr.send()}function setAccessToken(token){access_token=token,console.log("Setting access_token: ",access_token),callback(null,access_token)}if(access_token)return void callback(null,access_token);var options={interactive:!0,url:"https://www.douban.com/service/auth2/auth?client_id="+conf.api_key+"&response_type=code&redirect_uri="+encodeURIComponent(redirectURL)+"&scope="+conf.scopes};chrome.identity.launchWebAuthFlow(options,function(redirectUri){if(console.log("launchWebAuthFlow completed",chrome.runtime.lastError,redirectUri),chrome.runtime.lastError)return void callback(new Error(chrome.runtime.lastError));var matches=redirectUri.match(redirectRe);matches&&matches.length>1?handleProviderResponse(parseRedirectFragment(matches[1])):callback(new Error("Invalid redirect URI"))})}}}(); -------------------------------------------------------------------------------- /public/js/enml.min.js: -------------------------------------------------------------------------------- 1 | var enml=function(){function html2enml(node){var result,$$=angular.element,clone=node.cloneNode(!0),process=$$('
'),nodeClassName=$(node).attr("class").split(" ")[0];return $("body").append(process),process=$("#process"),process.append($(clone)),nodewalk(clone.childNodes,process_node),result=$("#process ."+nodeClassName).html().replace(/
/g,"
").replace(/
/g,"
").replace(//g,''),process.remove(),result}function nodewalk(node,fn){for(var i=0;ii;i++)rule=rules[i],str_style+=rule+":"+getComputedStyle(node).getPropertyValue(rule)+";";node.setAttribute("style",str_style)}}var EXTRACT_RULE={blockquote:["border-left"],span:["color"],code:["border","background-color"],pre:["background-color","margin"]};return{html2enml:html2enml}}(); -------------------------------------------------------------------------------- /public/js/evernote-auth.min.js: -------------------------------------------------------------------------------- 1 | var EvernoteAuth=function(){"use strict";var conf=EvernoteAuthConf,redirectURL="https://"+chrome.runtime.id+".chromiumapp.org/auth/callback",redirectRe=new RegExp(redirectURL+"[#?](.*)"),access_token=null;return{getToken:function(type,callback){function parseRedirectFragment(fragment,options){var pairs=fragment.split(/&/),values={};return pairs.forEach(function(pair){var nameval=pair.split(/=/);values[nameval[0]]=options&&options.decode?decodeURIComponent(nameval[1]):nameval[1]}),values}function handleProviderResponse(values,values2){console.log("providerResponse",values),values.hasOwnProperty("access_token")?setAccessToken(values.access_token):values.hasOwnProperty("oauth_token")?exchangeCodeForToken(values.oauth_token,values2.oauth_verifier):callback(new Error("Neither access_token nor code avialable."))}function exchangeCodeForToken(token,verifier){var xhr=new XMLHttpRequest;xhr.open("POST",conf.hostname[type]+"/oauth?oauth_consumer_key="+conf.consumer_key+"&oauth_signature="+conf.consumer_secret+"%26&oauth_signature_method=PLAINTEXT&oauth_timestamp="+(new Date).valueOf()+"&oauth_nonce="+(new Date).valueOf()+"&oauth_token="+token+"&oauth_verifier="+verifier),xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),xhr.onload=function(){if(200===this.status){var response=parseRedirectFragment(this.responseText,{decode:!0});console.log(response),response.hasOwnProperty("oauth_token")?callback(null,response):callback(new Error("Cannot obtain access_token from code."))}else console.log("code exchange status:",this.status),callback(new Error("Code exchange failed"))},xhr.send()}if(access_token)return void callback(null,access_token);var options={interactive:!0,url:conf.hostname[type]+"/oauth?oauth_consumer_key="+conf.consumer_key+"&oauth_signature="+conf.consumer_secret+"%26&oauth_signature_method=PLAINTEXT&oauth_timestamp="+(new Date).valueOf()+"&oauth_nonce"+(new Date).valueOf()+"&oauth_callback="+encodeURIComponent(redirectURL)},xhr=new XMLHttpRequest;xhr.open("GET",options.url),xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),xhr.onload=function(){if(200===this.status){var resp=parseRedirectFragment(this.responseText);if(console.log(resp),!resp.oauth_callback_confirmed)return;chrome.identity.launchWebAuthFlow({interactive:!0,url:conf.hostname[type]+"/OAuth.action?oauth_token="+resp.oauth_token},function(redirectUri){if(console.log("launchWebAuthFlow completed",chrome.runtime.lastError,redirectUri),chrome.runtime.lastError)return void callback(new Error(chrome.runtime.lastError));var matches=redirectUri.match(redirectRe);matches&&matches.length>1?handleProviderResponse(resp,parseRedirectFragment(redirectUri)):callback(new Error("Invalid redirect URI"))})}},xhr.send()}}}(); -------------------------------------------------------------------------------- /public/js/filters.min.js: -------------------------------------------------------------------------------- 1 | "use strict";angular.module("ANNO").filter("characters",function(){return function(input,chars){if(input&&input.length>=chars){input=input.substring(0,chars);var lastspace=input.lastIndexOf(" ");return-1!==lastspace&&(input=input.substr(0,lastspace)),input+"..."}return input}}); -------------------------------------------------------------------------------- /public/js/ui-directives.min.js: -------------------------------------------------------------------------------- 1 | "use strict";angular.module("ANNO.ui-directives",[]).directive("scrollHide",[function(){return function(scope,elem,attrs){function onScroll(){elem.toggleClass("hide",target.scrollTop>hideFrom)}var hideFrom=attrs.hideFrom||10,target=window;attrs.targetSel&&(target=document.querySelector(attrs.targetSel)),attrs.hideFrom&&scope.$watch(attrs.hideFrom,function(value){hideFrom=parseInt(value,10)}),scope.hide=!1,angular.element(target).on("scroll",_.throttle(onScroll,200))}}]); -------------------------------------------------------------------------------- /public/lib/angular/angular-cookies.min.js: -------------------------------------------------------------------------------- 1 | !function(window,angular,undefined){"use strict";angular.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function($rootScope,$browser){function push(){var name,value,browserCookies,updated;for(name in lastCookies)isUndefined(cookies[name])&&$browser.cookies(name,undefined);for(name in cookies)value=cookies[name],angular.isString(value)?value!==lastCookies[name]&&($browser.cookies(name,value),updated=!0):angular.isDefined(lastCookies[name])?cookies[name]=lastCookies[name]:delete cookies[name];if(updated){updated=!1,browserCookies=$browser.cookies();for(name in cookies)cookies[name]!==browserCookies[name]&&(isUndefined(browserCookies[name])?delete cookies[name]:cookies[name]=browserCookies[name],updated=!0)}}var lastBrowserCookies,cookies={},lastCookies={},runEval=!1,copy=angular.copy,isUndefined=angular.isUndefined;return $browser.addPollFn(function(){var currentCookies=$browser.cookies();lastBrowserCookies!=currentCookies&&(lastBrowserCookies=currentCookies,copy(currentCookies,lastCookies),copy(currentCookies,cookies),runEval&&$rootScope.$apply())})(),runEval=!0,$rootScope.$watch(push),cookies}]).factory("$cookieStore",["$cookies",function($cookies){return{get:function(key){var value=$cookies[key];return value?angular.fromJson(value):value},put:function(key,value){$cookies[key]=angular.toJson(value)},remove:function(key){delete $cookies[key]}}}])}(window,window.angular); -------------------------------------------------------------------------------- /public/lib/angular/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | !function(window,angular,undefined){"use strict";function isValidDottedPath(path){return null!=path&&""!==path&&"hasOwnProperty"!==path&&MEMBER_NAME_REGEX.test("."+path)}function lookupDottedPath(obj,path){if(!isValidDottedPath(path))throw $resourceMinErr("badmember",'Dotted member path "@{0}" is invalid.',path);for(var keys=path.split("."),i=0,ii=keys.length;ii>i&&obj!==undefined;i++){var key=keys[i];obj=null!==obj?obj[key]:undefined}return obj}function shallowClearAndCopy(src,dst){dst=dst||{},angular.forEach(dst,function(value,key){delete dst[key]});for(var key in src)!src.hasOwnProperty(key)||"$"===key.charAt(0)&&"$"===key.charAt(1)||(dst[key]=src[key]);return dst}var $resourceMinErr=angular.$$minErr("$resource"),MEMBER_NAME_REGEX=/^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;angular.module("ngResource",["ng"]).factory("$resource",["$http","$q",function($http,$q){function encodeUriSegment(val){return encodeUriQuery(val,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function encodeUriQuery(val,pctEncodeSpaces){return encodeURIComponent(val).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,pctEncodeSpaces?"%20":"+")}function Route(template,defaults){this.template=template,this.defaults=defaults||{},this.urlParams={}}function resourceFactory(url,paramDefaults,actions){function extractParams(data,actionParams){var ids={};return actionParams=extend({},paramDefaults,actionParams),forEach(actionParams,function(value,key){isFunction(value)&&(value=value()),ids[key]=value&&value.charAt&&"@"==value.charAt(0)?lookupDottedPath(data,value.substr(1)):value}),ids}function defaultResponseInterceptor(response){return response.resource}function Resource(value){shallowClearAndCopy(value||{},this)}var route=new Route(url);return actions=extend({},DEFAULT_ACTIONS,actions),forEach(actions,function(action,name){var hasBody=/^(POST|PUT|PATCH)$/i.test(action.method);Resource[name]=function(a1,a2,a3,a4){var data,success,error,params={};switch(arguments.length){case 4:error=a4,success=a3;case 3:case 2:if(!isFunction(a2)){params=a1,data=a2,success=a3;break}if(isFunction(a1)){success=a1,error=a2;break}success=a2,error=a3;case 1:isFunction(a1)?success=a1:hasBody?data=a1:params=a1;break;case 0:break;default:throw $resourceMinErr("badargs","Expected up to 4 arguments [params, data, success, error], got {0} arguments",arguments.length)}var isInstanceCall=this instanceof Resource,value=isInstanceCall?data:action.isArray?[]:new Resource(data),httpConfig={},responseInterceptor=action.interceptor&&action.interceptor.response||defaultResponseInterceptor,responseErrorInterceptor=action.interceptor&&action.interceptor.responseError||undefined;forEach(action,function(value,key){"params"!=key&&"isArray"!=key&&"interceptor"!=key&&(httpConfig[key]=copy(value))}),hasBody&&(httpConfig.data=data),route.setUrlParams(httpConfig,extend({},extractParams(data,action.params||{}),params),action.url);var promise=$http(httpConfig).then(function(response){var data=response.data,promise=value.$promise;if(data){if(angular.isArray(data)!==!!action.isArray)throw $resourceMinErr("badcfg","Error in resource configuration. Expected response to contain an {0} but got an {1}",action.isArray?"array":"object",angular.isArray(data)?"array":"object");action.isArray?(value.length=0,forEach(data,function(item){value.push(new Resource(item))})):(shallowClearAndCopy(data,value),value.$promise=promise)}return value.$resolved=!0,response.resource=value,response},function(response){return value.$resolved=!0,(error||noop)(response),$q.reject(response)});return promise=promise.then(function(response){var value=responseInterceptor(response);return(success||noop)(value,response.headers),value},responseErrorInterceptor),isInstanceCall?promise:(value.$promise=promise,value.$resolved=!1,value)},Resource.prototype["$"+name]=function(params,success,error){isFunction(params)&&(error=success,success=params,params={});var result=Resource[name].call(this,params,this,success,error);return result.$promise||result}}),Resource.bind=function(additionalParamDefaults){return resourceFactory(url,extend({},paramDefaults,additionalParamDefaults),actions)},Resource}var DEFAULT_ACTIONS={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},noop=angular.noop,forEach=angular.forEach,extend=angular.extend,copy=angular.copy,isFunction=angular.isFunction;return Route.prototype={setUrlParams:function(config,params,actionUrl){var val,encodedVal,self=this,url=actionUrl||self.template,urlParams=self.urlParams={};forEach(url.split(/\W/),function(param){if("hasOwnProperty"===param)throw $resourceMinErr("badname","hasOwnProperty is not a valid parameter name.");!new RegExp("^\\d+$").test(param)&¶m&&new RegExp("(^|[^\\\\]):"+param+"(\\W|$)").test(url)&&(urlParams[param]=!0)}),url=url.replace(/\\:/g,":"),params=params||{},forEach(self.urlParams,function(_,urlParam){val=params.hasOwnProperty(urlParam)?params[urlParam]:self.defaults[urlParam],angular.isDefined(val)&&null!==val?(encodedVal=encodeUriSegment(val),url=url.replace(new RegExp(":"+urlParam+"(\\W|$)","g"),function(match,p1){return encodedVal+p1})):url=url.replace(new RegExp("(/?):"+urlParam+"(\\W|$)","g"),function(match,leadingSlashes,tail){return"/"==tail.charAt(0)?tail:leadingSlashes+tail})}),url=url.replace(/\/+$/,"")||"/",url=url.replace(/\/\.(?=\w+($|\?))/,"."),config.url=url.replace(/\/\\\./,"/."),forEach(params,function(value,key){self.urlParams[key]||(config.params=config.params||{},config.params[key]=value)})}},resourceFactory}])}(window,window.angular); -------------------------------------------------------------------------------- /public/lib/angular/angular-route.min.js: -------------------------------------------------------------------------------- 1 | !function(window,angular){"use strict";function $RouteProvider(){function inherit(parent,extra){return angular.extend(new(angular.extend(function(){},{prototype:parent})),extra)}function pathRegExp(path,opts){var insensitive=opts.caseInsensitiveMatch,ret={originalPath:path,regexp:path},keys=ret.keys=[];return path=path.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(_,slash,key,option){var optional="?"===option?option:null,star="*"===option?option:null;return keys.push({name:key,optional:!!optional}),slash=slash||"",""+(optional?"":slash)+"(?:"+(optional?slash:"")+(star&&"(.+?)"||"([^/]+)")+(optional||"")+")"+(optional||"")}).replace(/([\/$\*])/g,"\\$1"),ret.regexp=new RegExp("^"+path+"$",insensitive?"i":""),ret}var routes={};this.when=function(path,route){if(routes[path]=angular.extend({reloadOnSearch:!0},route,path&&pathRegExp(path,route)),path){var redirectPath="/"==path[path.length-1]?path.substr(0,path.length-1):path+"/";routes[redirectPath]=angular.extend({redirectTo:path},pathRegExp(redirectPath,route))}return this},this.otherwise=function(params){return this.when(null,params),this},this.$get=["$rootScope","$location","$routeParams","$q","$injector","$http","$templateCache","$sce",function($rootScope,$location,$routeParams,$q,$injector,$http,$templateCache,$sce){function switchRouteMatcher(on,route){var keys=route.keys,params={};if(!route.regexp)return null;var m=route.regexp.exec(on);if(!m)return null;for(var i=1,len=m.length;len>i;++i){var key=keys[i-1],val="string"==typeof m[i]?decodeURIComponent(m[i]):m[i];key&&val&&(params[key.name]=val)}return params}function updateRoute(){var next=parseRoute(),last=$route.current;next&&last&&next.$$route===last.$$route&&angular.equals(next.pathParams,last.pathParams)&&!next.reloadOnSearch&&!forceReload?(last.params=next.params,angular.copy(last.params,$routeParams),$rootScope.$broadcast("$routeUpdate",last)):(next||last)&&(forceReload=!1,$rootScope.$broadcast("$routeChangeStart",next,last),$route.current=next,next&&next.redirectTo&&(angular.isString(next.redirectTo)?$location.path(interpolate(next.redirectTo,next.params)).search(next.params).replace():$location.url(next.redirectTo(next.pathParams,$location.path(),$location.search())).replace()),$q.when(next).then(function(){if(next){var template,templateUrl,locals=angular.extend({},next.resolve);return angular.forEach(locals,function(value,key){locals[key]=angular.isString(value)?$injector.get(value):$injector.invoke(value)}),angular.isDefined(template=next.template)?angular.isFunction(template)&&(template=template(next.params)):angular.isDefined(templateUrl=next.templateUrl)&&(angular.isFunction(templateUrl)&&(templateUrl=templateUrl(next.params)),templateUrl=$sce.getTrustedResourceUrl(templateUrl),angular.isDefined(templateUrl)&&(next.loadedTemplateUrl=templateUrl,template=$http.get(templateUrl,{cache:$templateCache}).then(function(response){return response.data}))),angular.isDefined(template)&&(locals.$template=template),$q.all(locals)}}).then(function(locals){next==$route.current&&(next&&(next.locals=locals,angular.copy(next.params,$routeParams)),$rootScope.$broadcast("$routeChangeSuccess",next,last))},function(error){next==$route.current&&$rootScope.$broadcast("$routeChangeError",next,last,error)}))}function parseRoute(){var params,match;return angular.forEach(routes,function(route){!match&&(params=switchRouteMatcher($location.path(),route))&&(match=inherit(route,{params:angular.extend({},$location.search(),params),pathParams:params}),match.$$route=route)}),match||routes[null]&&inherit(routes[null],{params:{},pathParams:{}})}function interpolate(string,params){var result=[];return angular.forEach((string||"").split(":"),function(segment,i){if(0===i)result.push(segment);else{var segmentMatch=segment.match(/(\w+)(.*)/),key=segmentMatch[1];result.push(params[key]),result.push(segmentMatch[2]||""),delete params[key]}}),result.join("")}var forceReload=!1,$route={routes:routes,reload:function(){forceReload=!0,$rootScope.$evalAsync(updateRoute)}};return $rootScope.$on("$locationChangeSuccess",updateRoute),$route}]}function $RouteParamsProvider(){this.$get=function(){return{}}}function ngViewFactory($route,$anchorScroll,$animate){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(scope,$element,attr,ctrl,$transclude){function cleanupLastView(){previousElement&&(previousElement.remove(),previousElement=null),currentScope&&(currentScope.$destroy(),currentScope=null),currentElement&&($animate.leave(currentElement,function(){previousElement=null}),previousElement=currentElement,currentElement=null)}function update(){var locals=$route.current&&$route.current.locals,template=locals&&locals.$template;if(angular.isDefined(template)){var newScope=scope.$new(),current=$route.current,clone=$transclude(newScope,function(clone){$animate.enter(clone,null,currentElement||$element,function(){!angular.isDefined(autoScrollExp)||autoScrollExp&&!scope.$eval(autoScrollExp)||$anchorScroll()}),cleanupLastView()});currentElement=clone,currentScope=current.scope=newScope,currentScope.$emit("$viewContentLoaded"),currentScope.$eval(onloadExp)}else cleanupLastView()}var currentScope,currentElement,previousElement,autoScrollExp=attr.autoscroll,onloadExp=attr.onload||"";scope.$on("$routeChangeSuccess",update),update()}}}function ngViewFillContentFactory($compile,$controller,$route){return{restrict:"ECA",priority:-400,link:function(scope,$element){var current=$route.current,locals=current.locals;$element.html(locals.$template);var link=$compile($element.contents());if(current.controller){locals.$scope=scope;var controller=$controller(current.controller,locals);current.controllerAs&&(scope[current.controllerAs]=controller),$element.data("$ngControllerController",controller),$element.children().data("$ngControllerController",controller)}link(scope)}}}var ngRouteModule=angular.module("ngRoute",["ng"]).provider("$route",$RouteProvider);ngRouteModule.provider("$routeParams",$RouteParamsProvider),ngRouteModule.directive("ngView",ngViewFactory),ngRouteModule.directive("ngView",ngViewFillContentFactory),ngViewFactory.$inject=["$route","$anchorScroll","$animate"],ngViewFillContentFactory.$inject=["$compile","$controller","$route"]}(window,window.angular); -------------------------------------------------------------------------------- /public/lib/angular/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | !function(window,angular){"use strict";function $SanitizeProvider(){this.$get=["$$sanitizeUri",function($$sanitizeUri){return function(html){var buf=[];return htmlParser(html,htmlSanitizeWriter(buf,function(uri,isImage){return!/^unsafe/.test($$sanitizeUri(uri,isImage))})),buf.join("")}}]}function sanitizeText(chars){var buf=[],writer=htmlSanitizeWriter(buf,angular.noop);return writer.chars(chars),buf.join("")}function makeMap(str){var i,obj={},items=str.split(",");for(i=0;i=0&&stack[pos]!=tagName;pos--);if(pos>=0){for(i=stack.length-1;i>=pos;i--)handler.end&&handler.end(stack[i]);stack.length=pos}}var index,chars,match,stack=[],last=html;for(stack.last=function(){return stack[stack.length-1]};html;){if(chars=!0,stack.last()&&specialElements[stack.last()])html=html.replace(new RegExp("(.*)<\\s*\\/\\s*"+stack.last()+"[^>]*>","i"),function(all,text){return text=text.replace(COMMENT_REGEXP,"$1").replace(CDATA_REGEXP,"$1"),handler.chars&&handler.chars(decodeEntities(text)),""}),parseEndTag("",stack.last());else if(0===html.indexOf("",index)===index&&(handler.comment&&handler.comment(html.substring(4,index)),html=html.substring(index+3),chars=!1)):DOCTYPE_REGEXP.test(html)?(match=html.match(DOCTYPE_REGEXP),match&&(html=html.replace(match[0],""),chars=!1)):BEGING_END_TAGE_REGEXP.test(html)?(match=html.match(END_TAG_REGEXP),match&&(html=html.substring(match[0].length),match[0].replace(END_TAG_REGEXP,parseEndTag),chars=!1)):BEGIN_TAG_REGEXP.test(html)&&(match=html.match(START_TAG_REGEXP),match&&(html=html.substring(match[0].length),match[0].replace(START_TAG_REGEXP,parseStartTag),chars=!1)),chars){index=html.indexOf("<");var text=0>index?html:html.substring(0,index);html=0>index?"":html.substring(index),handler.chars&&handler.chars(decodeEntities(text))}if(html==last)throw $sanitizeMinErr("badparse","The sanitizer was unable to parse the following block of html: {0}",html);last=html}parseEndTag()}function decodeEntities(value){if(!value)return"";var parts=spaceRe.exec(value),spaceBefore=parts[1],spaceAfter=parts[3],content=parts[2];return content&&(hiddenPre.innerHTML=content.replace(//g,">")}function htmlSanitizeWriter(buf,uriValidator){var ignore=!1,out=angular.bind(buf,buf.push);return{start:function(tag,attrs,unary){tag=angular.lowercase(tag),!ignore&&specialElements[tag]&&(ignore=tag),ignore||validElements[tag]!==!0||(out("<"),out(tag),angular.forEach(attrs,function(value,key){var lkey=angular.lowercase(key),isImage="img"===tag&&"src"===lkey||"background"===lkey;validAttrs[lkey]!==!0||uriAttrs[lkey]===!0&&!uriValidator(value,isImage)||(out(" "),out(key),out('="'),out(encodeEntities(value)),out('"'))}),out(unary?"/>":">"))},end:function(tag){tag=angular.lowercase(tag),ignore||validElements[tag]!==!0||(out("")),tag==ignore&&(ignore=!1)},chars:function(chars){ignore||out(encodeEntities(chars))}}}var $sanitizeMinErr=angular.$$minErr("$sanitize"),START_TAG_REGEXP=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,END_TAG_REGEXP=/^<\s*\/\s*([\w:-]+)[^>]*>/,ATTR_REGEXP=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,BEGIN_TAG_REGEXP=/^/g,DOCTYPE_REGEXP=/]*?)>/i,CDATA_REGEXP=//g,NON_ALPHANUMERIC_REGEXP=/([^\#-~| |!])/g,voidElements=makeMap("area,br,col,hr,img,wbr"),optionalEndTagBlockElements=makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),optionalEndTagInlineElements=makeMap("rp,rt"),optionalEndTagElements=angular.extend({},optionalEndTagInlineElements,optionalEndTagBlockElements),blockElements=angular.extend({},optionalEndTagBlockElements,makeMap("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")),inlineElements=angular.extend({},optionalEndTagInlineElements,makeMap("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")),specialElements=makeMap("script,style"),validElements=angular.extend({},voidElements,blockElements,inlineElements,optionalEndTagElements),uriAttrs=makeMap("background,cite,href,longdesc,src,usemap"),validAttrs=angular.extend({},uriAttrs,makeMap("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,size,span,start,summary,target,title,type,valign,value,vspace,width")),hiddenPre=document.createElement("pre"),spaceRe=/^(\s*)([\s\S]*?)(\s*)$/;angular.module("ngSanitize",[]).provider("$sanitize",$SanitizeProvider),angular.module("ngSanitize").filter("linky",["$sanitize",function($sanitize){var LINKY_URL_REGEXP=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,MAILTO_REGEXP=/^mailto:/;return function(text,target){function addText(text){text&&html.push(sanitizeText(text))}function addLink(url,text){html.push("'),addText(text),html.push("")}if(!text)return text;for(var match,url,i,raw=text,html=[];match=raw.match(LINKY_URL_REGEXP);)url=match[0],match[2]==match[3]&&(url="mailto:"+url),i=match.index,addText(raw.substr(0,i)),addLink(url,match[0].replace(MAILTO_REGEXP,"")),raw=raw.substring(i+match[0].length);return addText(raw),$sanitize(html.join(""))}}])}(window,window.angular); -------------------------------------------------------------------------------- /public/lib/codemirror/codemirror.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdYAG/ANNO/8bdde81ec10e63a2814fa1cab700986df751e53e/public/lib/codemirror/codemirror.min.js -------------------------------------------------------------------------------- /public/lib/ng-infinite-scroll/ng-infinite-scroll.min.js: -------------------------------------------------------------------------------- 1 | var mod;mod=angular.module("infinite-scroll",[]),mod.directive("infiniteScroll",["$rootScope","$timeout",function($rootScope,$timeout){return{link:function(scope,elem,attrs){var checkWhenEnabled,handler,scrollDistance,scrollEnabled,scrollElemSel=attrs.scrollElemSel||"body",scrollElem=document.querySelector(scrollElemSel),$scrollElem=angular.element(scrollElem);return scrollDistance=0,null!=attrs.infiniteScrollDistance&&scope.$watch(attrs.infiniteScrollDistance,function(value){return scrollDistance=parseInt(value,10)}),scrollEnabled=!0,checkWhenEnabled=!1,null!=attrs.infiniteScrollDisabled&&scope.$watch(attrs.infiniteScrollDisabled,function(value){return scrollEnabled=!value,scrollEnabled&&checkWhenEnabled?(checkWhenEnabled=!1,handler()):void 0}),handler=function(){var elementBottom,remaining,shouldScroll,windowBottom;return windowBottom=scrollElem.clientHeight+scrollElem.scrollTop,elementBottom=elem[0].offsetTop+elem[0].clientHeight,remaining=elementBottom-windowBottom,shouldScroll=remaining<=scrollElem.clientHeight*scrollDistance,shouldScroll&&scrollEnabled?$rootScope.$$phase?scope.$eval(attrs.infiniteScroll):scope.$apply(attrs.infiniteScroll):shouldScroll?checkWhenEnabled=!0:void 0},$scrollElem.on("scroll",_.throttle(handler,200)),scope.$on("$destroy",function(){return $scrollElem.off("scroll",handler)}),$timeout(function(){return attrs.infiniteScrollImmediateCheck?scope.$eval(attrs.infiniteScrollImmediateCheck)?handler():void 0:handler()},0)}}}]); -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "读书笔记", 3 | "description": "豆瓣读书笔记第三方应用", 4 | "version": "0.2.5", 5 | "manifest_version": 2, 6 | "minimum_chrome_version": "29", 7 | "app": { 8 | "background": { 9 | "scripts": ["background.js"] 10 | } 11 | }, 12 | "icons": { "16": "img/books.png", "128": "img/books.png" }, 13 | "permissions": [ 14 | {"fileSystem": ["write"]} 15 | , "storage" 16 | , "identity" 17 | , "https://www.douban.com/*", "https://api.douban.com/*", "http://*.douban.com/*" 18 | , "https://*.evernote.com/*", "http://*.evernote.com/*" 19 | , "https://*.yinxiang.com/*", "http://*.yinxiang.com/*" 20 | , "https://www.google-analytics.com/" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /public/partials/about.html: -------------------------------------------------------------------------------- 1 |

读书笔记

读书笔记是基于豆瓣读书笔记开放接口开发的应用
2 | 支持 Github Flavor Markdown 书写笔记
3 | 如果遇到 BUG、体验不好的地方,或者希望增加功能
4 | 欢迎通过以下方式联系反馈 5 |

反馈

已知问题

  • 第三方应用无法读取私密笔记的图片。建议设置带图笔记权限为公开。
  • 豆瓣读书尚未开放收藏接口,因此收藏的内容存储在Chrome云端。在豆瓣开放接口后,应用将更新,统一你的收藏数据。

使用的开源项目

-------------------------------------------------------------------------------- /public/partials/book.html: -------------------------------------------------------------------------------- 1 |

{{ book.summary }}

去豆瓣看这本书
{{ book.title }}
共{{ book.notes_count }}篇笔记
写新笔记
-------------------------------------------------------------------------------- /public/partials/books.html: -------------------------------------------------------------------------------- 1 |
  • {{ book.notes_count }}篇笔记
    写新笔记
    {{ book.title }}{{ book.author[0] }}

还没有写过笔记

-------------------------------------------------------------------------------- /public/partials/edit.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /public/partials/error.html: -------------------------------------------------------------------------------- 1 |
哎哟,发生了错误返回书架
-------------------------------------------------------------------------------- /public/partials/fav.html: -------------------------------------------------------------------------------- 1 |
*收藏的笔记存储在Chrome云端,并未与豆瓣收藏同步。为什么?
-------------------------------------------------------------------------------- /public/partials/friends.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /public/partials/info.html: -------------------------------------------------------------------------------- 1 |

{{ user.name }}

从{{ start }}到{{ end }}我一共给{{ total_books }}本书写了{{ total_notes }}篇笔记 2 | 第一篇笔记{{ start_note.chapter }}来自{{ start_book.title }} 3 | 其中给{{ max_book.title }}写了最多笔记,一共{{ max_book.notes_count }}篇

最近写过笔记的书

发送错误报告

* 帮助读书笔记变得更健壮,报告完全匿名,不带有你的任何用户信息
-------------------------------------------------------------------------------- /public/partials/login.html: -------------------------------------------------------------------------------- 1 |

登录

登录

-------------------------------------------------------------------------------- /public/partials/note.html: -------------------------------------------------------------------------------- 1 |

{{ note.chapter || ("第" + note.page_no + "页") }}

{{ author.name }}《{{ book.title }}》的笔记
-------------------------------------------------------------------------------- /public/partials/widgets/editor.html: -------------------------------------------------------------------------------- 1 |
{{ article.book.title }}:
-------------------------------------------------------------------------------- /public/partials/widgets/reader.html: -------------------------------------------------------------------------------- 1 |
{{ article.book.title }}:

{{ article.chapter }}

-------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | header.header(ng-class="{ 'pushed' : asideVisible, 'vanished' : headerInvisible }") 5 | span.icon-sidebar(ng-click="toggleSidebar()") 6 | //-span.title(ng-bind-html="globalTitle") 7 | span.title(compile="globalTitle") 8 | aside.hidden(ng-class="{ 'show': asideVisible }") 9 | div.banner(ng-show="user") 10 | a.avatar(ng-show="user", remote-image="user.avatar", link="/", ng-click="toggleSidebar()") 11 | ul(ng-show="user") 12 | li 13 | a(link="/", ng-click="toggleSidebar()") 我的书架 14 | li(ng-show="lastBook && user") 15 | a(link="/{{ author.uid }}/book/{{ lastBook.id }}") 返回 {{ lastBook.title }} 16 | li 17 | a(link="/{{ user.uid }}/info") 个人 18 | li 19 | a(link="/friends") 友邻笔记 20 | li 21 | a(link="/fav") 我的收藏 22 | li 23 | a(ng-click="logout()") 注销 24 | ul(ng-hide="user") 25 | li 26 | a(link="/login", ng-click="toggleSidebar()") 登录 27 | ul.info 28 | li 29 | a(link="/about") 关于 · 反馈 30 | div.ng-view(ng-class="{ 'pushed' : asideVisible }") 31 | div.loading(loader) 32 | div.rect1 33 | div.rect2 34 | div.rect3 35 | div.rect4 36 | div.rect5 37 | div.alert(alert) 38 | script(type="text/ng-template", id="modalLogout.html") 39 | div.modal-header 40 | h3 确定注销?下次登陆将需要重新前往豆瓣授权。 41 | div.modal-footer.buttons-group 42 | button.button-default(ng-click="cancel()") 取消 43 | button.button-warning(ng-click="ok()") 注销 44 | script(type="text/ng-template", id="modalEvernote.html") 45 | div.modal-evernote 46 | div.modal-header 47 | h3 保存到Evernote/印象笔记 48 | div.modal-body 49 | div.row 50 | label 51 | input(type="radio", ng-model="choice_notebook", value="exist") 52 | 保存已有笔记本 53 | select(ng-show="choice_notebook == 'exist'", ng-model="selectedNotebook", ng-options="nb.name for nb in notebooks") 54 | div.row 55 | label 56 | input(type="radio", ng-model="choice_notebook", value="new") 57 | 创建新笔记本 58 | input(ng-show="choice_notebook == 'new'", type="text", placeholder="输入新笔记本名", ng-model="new_notebook") 59 | div.modal-footer 60 | div.status {{ status }} 61 | div.buttons-group 62 | button.button-default(ng-click="cancel()") 取消 63 | button.button-primary(ng-click="ok(choice_notebook, selectedNotebook, new_notebook)") 保存 64 | 65 | block scripts 66 | script(src="lib/underscore/underscore.min.js") 67 | script(src="lib/angular/angular.min.js") 68 | script(src="lib/angular/angular-route.min.js") 69 | //- script(src="lib/angular/angular-sanitize.min.js") 70 | //- script(src="lib/angular/angular-cookies.min.js") 71 | //- script(src="lib/angular/angular-resource.min.js") 72 | //- script(src="/lib/underscore/underscore.js") 73 | 74 | //- script(src="/lib/angular/angular.js") 75 | //- script(src="/lib/angular/angular-route.js") 76 | //- script(src="/lib/angular/angular-cookies.js") 77 | 78 | script(src="/lib/angular-bootstrap/ui-bootstrap.min.js") 79 | script(src="/lib/angular-bootstrap/ui-bootstrap-tpls.min.js") 80 | 81 | script(src="lib/ng-infinite-scroll/ng-infinite-scroll.min.js") 82 | 83 | script(src="/lib/codemirror/codemirror.min.js") 84 | script(src="/lib/marked/marked.min.js") 85 | script(src="/lib/color-thief/color-thief.min.js") 86 | script(src="/lib/evernote/evernote-sdk.min.js") 87 | script(src="/lib/analytics/ga.bundle.js") 88 | 89 | script(src="/js/config.min.js") 90 | script(src="/js/douban-auth.min.js") 91 | script(src="/js/evernote-auth.min.js") 92 | script(src="/js/app.min.js") 93 | script(src="/js/enml.min.js") 94 | script(src="/js/controllers.min.js") 95 | script(src="/js/services.min.js") 96 | script(src="/js/directives.min.js") 97 | script(src="/js/ui-directives.min.js") 98 | script(src="/js/filters.min.js") 99 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype 2 | html(ng-app="ANNO", ng-controller="appFrameCtrl", ng-class="globalCSSClass", ng-csp) 3 | meta(charset="utf-8") 4 | head 5 | title 读书笔记 6 | link(rel='stylesheet', href='/css/style.css') 7 | link(rel='stylesheet', href='/css/codemirror.css') 8 | link(rel='stylesheet', href='/css/themes/tomorrow.css') 9 | body 10 | block content 11 | block scripts -------------------------------------------------------------------------------- /views/partials/about.jade: -------------------------------------------------------------------------------- 1 | section#about 2 | h1 读书笔记 3 | p. 4 | 读书笔记是基于豆瓣读书笔记开放接口开发的应用
5 | 支持 Github Flavor Markdown 书写笔记
6 | 如果遇到 BUG、体验不好的地方,或者希望增加功能
7 | 欢迎通过以下方式联系反馈 8 | 9 | h2 反馈 10 | div 阿弟: 11 | a(href="http://www.douban.com/people/sensitive", target="_blank") 豆瓣 12 | a(href="mailto:simonday@foxmail.com", target="_blank") 邮件 13 | 14 | h2 已知问题 15 | ul 16 | li 第三方应用无法读取私密笔记的图片。建议设置带图笔记权限为公开。 17 | li 豆瓣读书尚未开放收藏接口,因此收藏的内容存储在Chrome云端。在豆瓣开放接口后,应用将更新,统一你的收藏数据。 18 | 19 | h2 使用的开源项目 20 | ul 21 | li 22 | a(href="http://angularjs.org", target="_blank") AngularJS 23 | li 24 | a(href="http://codemirror.net", target="_blank") CodeMirror 25 | li 26 | a(href="https://github.com/chjj/marked", target="_blank") Marked 27 | li 28 | a(href="http://lokeshdhakar.com/projects/color-thief/", target="_blank") Color-thief -------------------------------------------------------------------------------- /views/partials/book.jade: -------------------------------------------------------------------------------- 1 | section#one-book 2 | //- h1 3 | //- a(link="/{{ author.uid }}", ng-show="!author.is_self") < {{ author.name }}的书架 4 | //- {{ book.title }} 5 | div.cubic-view 6 | div.book-cube(color-thief, image="book_cover", ng-model="dominateColor", ng-class="{ 'sided': book_sided }", ng-click="openBook()", ng-mouseleave="closeBook()") 7 | div.book-front(ng-class="{ 'opened': book_opened }") 8 | .book-cover-backface(ng-style="{ 'background-color': dominateColor.background }") 9 | .book-cover-frontface(remote-image="book_cover") 10 | div.book-page 11 | div.page-content 12 | //- a.icon-feather(link="/{{ user.uid }}/book/{{ book.id }}/new")写新笔记 13 | //- a(ng-click="exportNotes()")导出所有笔记 14 | p.summary {{ book.summary }} 15 | a(href="http://book.douban.com/subject/{{ book.id }}/", target="_blank")去豆瓣看这本书 16 | div.book-back(ng-style="{ 'background-color': dominateColor.background }") 17 | div.book-top 18 | div.book-bottom 19 | div.book-left(ng-style="{ 'background-color': dominateColor.background, 'color': dominateColor.foreground }") 20 | span.book-title {{ book.title }} 21 | div.book-right 22 | section.main 23 | // p {{ book.summary }} 24 | header 25 | .right-actions 26 | span 共{{ book.notes_count }}篇笔记 27 | button.button-default.icon-export(ng-click="exportNotes()") 导出 28 | nav.order.button-group() 29 | button.button-plain(ng-click="order='time'; reverse=true", ng-class="{'active': (order=='time')}") 最新 30 | button.button-plain(ng-click="order='page_no'; reverse=false", ng-class="{'active': (order=='page_no')}") 页码 31 | a.button-primary.icon-feather(link="/{{ user.uid }}/book/{{ book.id }}/new") 32 | 写新笔记 33 | ul.notes 34 | li.note(ng-repeat="note in book.notes | orderBy: order : reverse") 35 | a.note-mask(link="/note/{{ note.id }}") 36 | time(datetime="{{ note.time }}") {{ note.time | date: note.time : 'longDate'}} 37 | h3 {{ note.chapter || ("第" + note.page_no + "页") }} 38 | p.note-summary {{ note.summary }} 39 | span.note-page(ng-class="{ 'icon-lock': note.privacy == '1' }") P{{ note.page_no }} 40 | 41 | script(type="text/ng-template", id="modalExport.html") 42 | div.modal-export 43 | div.modal-header 44 | h3 选择导出格式 45 | div.modal-body 46 | div.buttons-group 47 | button.button-radio(ng-click="changeExportFormat('markdown')", ng-class="{ 'active': exportFormat == 'markdown' }") Markdown 48 | button.button-radio(ng-click="changeExportFormat('html')", ng-class="{ 'active': exportFormat == 'html' }") HTML 49 | div.export-preview 50 | div.preview-markdown(ng-show="exportFormat == 'markdown'") {{ exports }} 51 | div.preview-html(ng-show="exportFormat == 'html'") 52 | previewer(markdown="exports") 53 | div.modal-footer 54 | div 55 | button.icon-evernote(ng-show="user.evernote || user.yinxiang", ng-click="popupEvernote()") 保存到Evernote 56 | div.buttons-group 57 | button.button-default(ng-click="cancel()") 取消 58 | button.button-primary(ng-click="download()") 导出 59 | -------------------------------------------------------------------------------- /views/partials/books.jade: -------------------------------------------------------------------------------- 1 | section#all-books 2 | //- h1(ng-show="user.is_self") 所有书 3 | //- h1(ng-show="!user.is_self") {{ user.name }}的书架 4 | ul.books 5 | li.book(ng-repeat="book in books") 6 | div.cover(remote-image='book.images.large') 7 | a.book-link(link="/{{ user.uid }}/book/{{ book.id }}") 8 | div.notes-count 9 | span.count {{ book.notes_count }} 10 | span.count-text 篇笔记 11 | a.add-note(ng-show='user.is_self', link="/{{ user.uid }}/book/{{ book.id }}/new") 12 | i.icon-plus 13 | span 写新笔记 14 | span.title {{ book.title }} 15 | span.author {{ book.author[0] }} 16 | li.book.empty(ng-show="user.is_self", ng-click="searchBook()") 17 | div.cover 18 | p(ng-show="tipVisible") 还没有写过笔记 19 | 20 | script(type="text/ng-template", id="modalSearchBook.html") 21 | div.modal-search 22 | div.modal-header 23 | h3 给一本书写笔记 24 | div.modal-body 25 | input#new-book-name(type="text", placeholder="输入书名,回车查找", input-enter="searchBook(query)", ng-model="query") 26 | input#new-book-id(type="hidden", value="{{ selectedBook.id }}") 27 | ul.search-results 28 | li.result(ng-repeat="book in books", ng-click="selectBook($index, book)", ng-class="{'active': $index == selectedIndex}") 29 | img(remote-image="book.images.large") 30 | div.result-info 31 | h4 {{ book.title }} 32 | p {{ book.summary | characters: 120 }} 33 | div.tip(ng-show="has_searched") 搜索不到? 直接输入想读的书对应的豆瓣Subject ID (下例红色部分) 34 | div.tip(ng-show="has_searched") 35 | | http://book.douban.com/subject/25746578/ 36 | div.modal-footer 37 | div.selected-bookname {{ selectedBook.title }} 38 | div.buttons-group 39 | button.button-default(ng-click="cancel()") 取消 40 | button.button-primary(ng-click="ok(selectedBook.id)", ng-disabled="!selectedBook") 开始写 41 | -------------------------------------------------------------------------------- /views/partials/edit.jade: -------------------------------------------------------------------------------- 1 | ul#navigation 2 | li 3 | a.js-edit.icon-feather(data-title="编辑", ng-click="changeMode('edit-mode')", ng-class="{ 'active': mode == 'edit-mode' }") 4 | li 5 | a.js-preview.icon-eye(data-title="预览", ng-click="changeMode('read-mode')", ng-class="{ 'active': mode == 'read-mode' }") 6 | li 7 | a.js-columns.icon-columns(data-title="编辑+预览", ng-click="changeMode('column-mode')", ng-class="{ 'active': mode == 'column-mode' }") 8 | li 9 | a.js-fullscreen(data-title="全屏", ng-click="fullscreen()", ng-class="{ 'icon-restore': is_fullscreen, 'icon-fullscreen': !is_fullscreen }") 10 | li 11 | a.js-save.icon-save(data-title="保存", data-ajax="{{ action }}", ng-click="save()") 12 | 13 | 14 | div.container(ng-class="mode") 15 | editor(article="note", images="images") 16 | reader(article="note", images="images") -------------------------------------------------------------------------------- /views/partials/error.jade: -------------------------------------------------------------------------------- 1 | section#error 2 | div.icon-sad.not_found 哎哟,发生了错误 3 | a.button-warning(link="/") 返回书架 -------------------------------------------------------------------------------- /views/partials/fav.jade: -------------------------------------------------------------------------------- 1 | section#fav 2 | //-h1 我收藏的笔记 3 | //- sup * 4 | ul.notes 5 | li.note(ng-repeat="note in notes | orderBy: order : reverse") 6 | a.note-mask(link="/note/{{ note.id }}") 7 | time(datetime="{{ note.time }}") {{ note.time | date: note.time : 'longDate'}} 8 | h3 {{ note.chapter || ("第" + note.page_no + "页") }} 9 | p.note-summary {{ note.summary }} 10 | span.note-page(ng-class="{ 'icon-lock': note.privacy == '1' }") P{{ note.page_no }} 11 | div.footnote 12 | small *收藏的笔记存储在Chrome云端,并未与豆瓣收藏同步。 13 | a(link='/about') 为什么? -------------------------------------------------------------------------------- /views/partials/friends.jade: -------------------------------------------------------------------------------- 1 | section#friends 2 | //- h1 友邻的笔记 3 | ul.friends(infinite-scroll="showMoreFriends()", scroll-elem-sel=".ng-view") 4 | li.friend(ng-repeat="friend in friends") 5 | a(link="/{{ friend.uid }}") 6 | img(remote-image="friend.small_avatar") 7 | span {{ friend.screen_name }} 8 | li.friend.more 9 | a.icon-more(ng-click="showMoreFriends()", ng-show="and_more") 10 | -------------------------------------------------------------------------------- /views/partials/info.jade: -------------------------------------------------------------------------------- 1 | section#info 2 | section.statistic 3 | h1 {{ user.name }} 4 | div.summary.ui-card. 5 | 从{{ start }}到{{ end }}我一共给{{ total_books }}本书写了{{ total_notes }}篇笔记 6 | 第一篇笔记{{ start_note.chapter }}来自{{ start_book.title }} 7 | 其中给{{ max_book.title }}写了最多笔记,一共{{ max_book.notes_count }}篇 8 | section.account 9 | h1 绑定Evernote 10 | div.ui-card.bind-choice(ng-show="(!evernote_bound) && (!yinxiang_bound)") 11 | span.icon-evernote 12 | a(ng-click=" authEvernote('evernote') ") 13 | span Evernote International 14 | a(ng-click=" authEvernote('yinxiang') ") 15 | span 印象笔记 16 | div.ui-card.unbind(ng-show="evernote_bound", ng-click=" unbind('evernote') ") 17 | a.icon-evernote.active 18 | span Evernote International 19 | div.ui-card.unbind(ng-show="yinxiang_bound", ng-click=" unbind('yinxiang') ") 20 | a.icon-evernote.active(ng-show="yinxiang_bound") 21 | span 印象笔记 22 | section.recent 23 | h1 最近写过笔记的书 24 | ul.books 25 | li.book(ng-repeat="book in recent_books") 26 | div.cover(remote-image="book.images.large") 27 | a.book-link(link="/{{ user.uid }}/book/{{ book.id }}") 28 | a.add-note.icon-pen(link="/{{ user.uid }}/book/{{ book.id }}/new", title="继续写") 29 | section.analytics 30 | h1 发送错误报告 31 | div.options.ui-card 32 | label 33 | input(type="checkbox", ng-model="analytics_allowed", ng-change=" alter_analytics() ") 34 | | 允许发生错误时发送匿名数据报告 35 | div.tip * 帮助读书笔记变得更健壮,报告完全匿名,不带有你的任何用户信息 36 | -------------------------------------------------------------------------------- /views/partials/login.jade: -------------------------------------------------------------------------------- 1 | div.auth 2 | h1= "登录" 3 | p 4 | a(title="用豆瓣帐号登录", ng-click="getToken()") 5 | img(alt="登录", src="/img/login_with_douban_32.png") -------------------------------------------------------------------------------- /views/partials/note.jade: -------------------------------------------------------------------------------- 1 | section#one-note(ng-hide="error") 2 | h1 {{ note.chapter || ("第" + note.page_no + "页") }} 3 | div.meta 4 | a(link="/{{ author.uid }}/book/{{ book.id }}") {{ author.name }}《{{ book.title }}》的笔记 5 | time(datetime="{{ note.time }}") {{ note.time | date: note.time : 'longDate'}} 6 | span.note-privacy(ng-class="{'icon-lock': note.privacy === 1}") 7 | div.operate(scroll-hide, hide-from=60, target-sel=".ng-view") 8 | a.icon-edit(ng-show="can_operate", link="/note/{{ note.id }}/edit") 9 | span 编辑 10 | a.icon-star-empty(ng-show="!has_starred", ng-click="toggleStar(true)") 11 | span 收藏 12 | a.icon-star(ng-show="has_starred", ng-click="toggleStar(false)") 13 | span 已收藏 14 | a.icon-evernote(ng-show="user.evernote || user.yinxiang", ng-click="popupEvernote()") 保存到Evernote 15 | a.icon-delete(ng-show="can_operate", ng-click="open()") 16 | span 删除 17 | previewer(markdown="note.content") 18 | //- div.share.typo-center 19 | //- a(href="javascript:void(function(){var d=document,url='http://book.douban.com/annotation/',splits=d.location.href.split('/'),nid=splits[splits.length-1],title=document.querySelector('.meta a').textContent,e=encodeURIComponent,s1=window.getSelection,s2=d.getSelection,s3=d.selection,s=s1?s1():s2?s2():s3?s3.createRange().text:'',r='http://www.douban.com/recommend/?url='+e(url+nid)+'&title='+e(title)+'&sel='+e(s)+'&v=1',w=450,h=330,x=function(){if(!window.open(r,'douban','toolbar=0,resizable=1,scrollbars=yes,status=1,width='+w+',height='+h+',left='+(screen.width-w)/2+',top='+(screen.height-h)/2))location.href=r+'&r=1'};if(/Firefox/.test(navigator.userAgent)){setTimeout(x,0)}else{x()}})()") 20 | //- img(src="http://img2.douban.com/pics/fw2douban1.png") 21 | 22 | script(type="text/ng-template", id="modalDeleteNote.html") 23 | div.modal-header 24 | h3 确定删除这篇笔记? 25 | div.modal-footer.buttons-group 26 | button.button-default(ng-click="cancel()") 取消 27 | button.button-warning(ng-click="ok()") 删除 28 | -------------------------------------------------------------------------------- /views/partials/widgets/editor.jade: -------------------------------------------------------------------------------- 1 | div#editor 2 | form(name="editorForm", novalidate) 3 | span.book-title(ng-show="article.book") {{ article.book.title }}: 4 | input#title(type="text", placeholder="笔记标题", ng-model="article.chapter") 5 | span#page 6 | label 页码: 7 | input#page_no(type="number", name="page_no", placeholder="填写 页码 或 章节最后一页", ng-model="article.page_no", min="0", max="50000") 8 | span#privacy 9 | label 10 | input(type="radio", name="privacy", value="public", ng-model="article.privacy") 11 | 公开 12 | label 13 | input(type="radio", name="privacy", value="private", ng-model="article.privacy") 14 | 隐藏 15 | ul.toolbar 16 | li.icon-bold(ng-click="bold()") 17 | li.icon-italic(ng-click="italic()") 18 | li.icon-image.js-insert-image 19 | input#upload(type="file", multiple, filereader) 20 | input#last_photo(type="hidden", value="{{ article.last_photo || 0 }}") 21 | li.icon-left-quote(ng-click="quote()") 22 | // li.icon-link 23 | li.icon-help(ng-click="toggleEditorHelp()") 24 | codemirror(ng-model="article.content", onload="codeMirrorLoad") 25 | script(type="text/ng-template", id="modalEditorHelp.html") 26 | div.modal-help 27 | div.modal-header 28 | h3 用 Markdown 写笔记 29 | div.modal-body. 30 |

Markdown

31 | Markdown 的语法很简单,可以让你快速地写下内容,不用拘泥于格式。
32 | 这里有一张 Github Flavor Markdown 的 cheat sheet,帮助你快速熟悉语法。 https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
33 | 编辑器的工具栏上目前有加粗、倾斜、图片、原文,可以简单上手。 34 |

编辑模式与日记保存

35 | 在右边导航栏上,可以切换编辑、预览、即时编辑模式,以及切换全屏和保存。 36 | div.modal-footer 37 | button.button-primary(ng-click="ok()") 知道啦 38 | -------------------------------------------------------------------------------- /views/partials/widgets/reader.jade: -------------------------------------------------------------------------------- 1 | div#reader 2 | div.book-title.typo-center(ng-show="article.book") {{ article.book.title }}: 3 | h1.title {{ article.chapter }} 4 | previewer(markdown="article.content", images="images") --------------------------------------------------------------------------------