├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── app ├── app_delegate.rb ├── controller │ ├── about_controller.rb │ ├── history_controller.rb │ ├── list_controller.rb │ ├── menu_controller.rb │ ├── read_controller.rb │ ├── setting_controller.rb │ ├── source_controller.rb │ └── start_controller.rb ├── data │ ├── data_source.rb │ ├── one_store.rb │ ├── source.rb │ └── source_manager.rb ├── lib │ ├── common.rb │ ├── config.rb │ ├── define.rb.example │ ├── http.rb │ ├── pure_layout_motion.rb │ └── router.rb ├── model │ └── item.rb └── view │ └── basic_cell.rb ├── resources ├── Default-568h@2x.png ├── Default-667h@2x.png ├── Default-736h@3x.png ├── Icon-60.png ├── Icon-60@2x.png ├── Icon@3x.png ├── back@2x.png ├── menu@2x.png ├── next@2x.png └── refresh@2x.png └── spec ├── daily_source_spec.rb ├── helpers └── extension.rb ├── main_spec.rb ├── net_spec.rb ├── router_list_spec.rb ├── rss_source_spec.rb ├── tenread_load_spec.rb └── tr_source_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .repl_history 2 | build 3 | tags 4 | app/pixate_code.rb 5 | resources/*.nib 6 | resources/*.momd 7 | resources/*.storyboardc 8 | .DS_Store 9 | nbproject 10 | .redcar 11 | #*# 12 | *~ 13 | *.sw[po] 14 | .eprj 15 | .sass-cache 16 | .idea 17 | .ruby-version 18 | .bundle/* 19 | samples/ios/*/vendor 20 | vendor/* 21 | debugger_cmds 22 | config.yaml 23 | # app/lib/config.rb 24 | # app/config/config.rb 25 | define.rb 26 | app/lib/define.rb 27 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://ruby.taobao.org' 2 | 3 | gem 'rake' 4 | gem 'afmotion' 5 | gem 'bubble-wrap', require: ['bubble-wrap/core', 'bubble-wrap/rss_parser'] 6 | gem 'sugarcube', :require => [ 7 | 'sugarcube', 8 | 'sugarcube-repl', 9 | 'sugarcube-factories', 10 | 'sugarcube-ui', 11 | 'sugarcube-color', 12 | 'sugarcube-foundation', 13 | 'sugarcube-constants', 14 | 'sugarcube-timer', 15 | 'sugarcube-notifications', 16 | 'sugarcube-nsuserdefaults', 17 | 'sugarcube-nsdata' 18 | ] 19 | gem 'motion-imager' 20 | 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://ruby.taobao.org/ 3 | specs: 4 | activesupport (4.2.4) 5 | i18n (~> 0.7) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | afmotion (2.5) 11 | motion-cocoapods (>= 1.4.1) 12 | motion-require (>= 0.1) 13 | bubble-wrap (1.9.2) 14 | claide (0.9.1) 15 | cocoapods (0.38.2) 16 | activesupport (>= 3.2.15) 17 | claide (~> 0.9.1) 18 | cocoapods-core (= 0.38.2) 19 | cocoapods-downloader (~> 0.9.1) 20 | cocoapods-plugins (~> 0.4.2) 21 | cocoapods-stats (~> 0.5.3) 22 | cocoapods-trunk (~> 0.6.1) 23 | cocoapods-try (~> 0.4.5) 24 | colored (~> 1.2) 25 | escape (~> 0.0.4) 26 | molinillo (~> 0.3.1) 27 | nap (~> 0.8) 28 | xcodeproj (~> 0.26.3) 29 | cocoapods-core (0.38.2) 30 | activesupport (>= 3.2.15) 31 | fuzzy_match (~> 2.0.4) 32 | nap (~> 0.8.0) 33 | cocoapods-downloader (0.9.3) 34 | cocoapods-plugins (0.4.2) 35 | nap 36 | cocoapods-stats (0.5.3) 37 | nap (~> 0.8) 38 | cocoapods-trunk (0.6.4) 39 | nap (>= 0.8, < 2.0) 40 | netrc (= 0.7.8) 41 | cocoapods-try (0.4.5) 42 | colored (1.2) 43 | escape (0.0.4) 44 | fuzzy_match (2.0.4) 45 | i18n (0.7.0) 46 | json (1.8.3) 47 | minitest (5.8.1) 48 | molinillo (0.3.1) 49 | motion-cocoapods (1.7.4) 50 | cocoapods (>= 0.34) 51 | motion-imager (0.2.1) 52 | motion-cocoapods (>= 1.4.1) 53 | motion-require (0.2.0) 54 | nap (0.8.0) 55 | netrc (0.7.8) 56 | rake (10.4.2) 57 | sugarcube (3.3.6) 58 | thread_safe (0.3.5) 59 | tzinfo (1.2.2) 60 | thread_safe (~> 0.1) 61 | xcodeproj (0.26.3) 62 | activesupport (>= 3) 63 | claide (~> 0.9.1) 64 | colored (~> 1.2) 65 | 66 | PLATFORMS 67 | ruby 68 | 69 | DEPENDENCIES 70 | afmotion 71 | bubble-wrap 72 | motion-imager 73 | rake 74 | sugarcube 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 shiweifu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 这是啥? 2 | 3 | 一个阅读工具,它聚合了主流的信息来源,提供「简单直接」的阅读体验。同时它也是一个完整的 RubyMotion 项目,在学习使用 RubyMotion 的过程中,它给我留下了不一样的开发体验。 4 | 5 | # 为啥会创造出它? 6 | 7 | - 我不想一觉醒来收到一大堆莫名其妙的推送和通知 8 | - 我不想注册登录点赞留言,我只想看看 9 | - 我不想装一大堆仅仅是在极为零散碎片的时间比如吃饭等车上厕所睡觉前才偶尔想起来看一下的App 10 | - 我想起了以前主流的网站都提供RSS,而现在大家都变得封闭新的网站不再提供RSS,旧的网站不再维护RSS 11 | 12 | # 它是如何工作的? 13 | 14 | ## 三种信息源 15 | 16 | ### 通过selector过滤出的源 17 | 18 | 受到 @WTSER的 Chrome 插件[十阅](https://coding.net/u/wtser/p/ten-read/git)启发,意识到可以通过CSS选择器过滤出网站首页的内容,往往这一部分也是可读性较高的。 19 | 20 | 地址:https://coding.net/u/shiweifu/p/OneReadRouter/git/blob/master/ten_read_list 21 | 22 | 23 | ### 抓取第三方应用的接口 24 | 25 | 基本上,主流的信息类应用结构都一样。都是有列表、有翻页、详情页。 26 | 27 | 地址:https://coding.net/u/shiweifu/p/OneReadRouter/git/blob/master/list 28 | 29 | 30 | ### RSS 接口 31 | 32 | 对仍旧提供RSS接口的网站提供了RSS支持。 33 | 34 | 地址: 35 | https://coding.net/u/shiweifu/p/OneReadRouter/git/blob/master/rss_list 36 | 37 | 需要说明的是,不管是哪种源,「一读」本身都不会抓取任何内容,它严重依赖目标网站对手机浏览器的支持。分享出去的链接也是目标网站的链接。 38 | 39 | # 说了这么多,你倒是上个图啊! 40 | 41 | ## 列表 42 | 43 | ![](http://i4.tietuku.com/93465f6d7e264249.png) 44 | 45 | 46 | ## 源 47 | ![](http://i4.tietuku.com/a207142be194caac.png) 48 | 49 | ## 选择源 50 | 51 | ![](http://i4.tietuku.com/47ac74573f762002.jpg) 52 | 53 | ![](http://i4.tietuku.com/20ebd0b0b7d74999.jpg) 54 | 55 | ![](http://i4.tietuku.com/7bd0b3be0d43d881.jpg) 56 | 57 | 58 | # 那么,哪里可以用? 59 | 60 | https://itunes.apple.com/us/app/yi-du/id1032943622?ls=1&mt=8 61 | 62 | 63 | # 如何贡献? 64 | 65 | - 给[仓库](https://coding.net/u/shiweifu/p/OneReadRouter/)提交新的源,一起来丰富阅读内容 66 | - 反馈你的使用情况,任何途径都可以 67 | - 给这个项目加个star 68 | - 去AppStore给予真实的评价 69 | 70 | # 如何联系: 71 | 72 | 邮箱:shiweifu@gmail.com 73 | 微信:kernel32 74 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $:.unshift("/Library/RubyMotion/lib") 3 | require 'motion/project/template/ios' 4 | require 'motion-cocoapods' 5 | require 'bundler' 6 | Bundler.require 7 | 8 | Motion::Project::App.setup do |app| 9 | app.name = '一读' 10 | 11 | app.version = '1.1.201512081724' 12 | app.short_version = '1.1' 13 | 14 | app.identifier = 'us.dollop.oneread' 15 | app.deployment_target = '7.0' 16 | app.sdk_version = '9.1' 17 | 18 | app.frameworks += %w(UIKit) 19 | app.icons = ['Icon@3x.png', 'Icon-60@2x.png', 'Icon-60.png'] 20 | 21 | 22 | app.archs['iPhoneOS'] << 'arm64' 23 | app.archs['iPhoneSimulator'] << 'x86_64' 24 | 25 | app.info_plist['CFBundleDevelopmentRegion'] = 'zh_CN' 26 | app.info_plist['CFBundleIdentifierKey'] = 'us.dollop.oneread' 27 | app.info_plist['NSAppTransportSecurity'] = {'NSAllowsArbitraryLoads' => true} 28 | app.info_plist['LSApplicationQueriesSchemes'] = ['wechat', 'weixin'] 29 | app.info_plist['CFBundleURLTypes'] = [ 30 | { 31 | 'CFBundleURLName' => 'pocket', 32 | 'CFBundleURLSchemes' => ['pocketapp5678', 'wxbfffd0ddce3278c7'] 33 | }, 34 | { 35 | 'CFBundleURLName' => 'weixin', 36 | 'CFBundleURLSchemes' => ['wxb89d2a6ca7643c4f'] 37 | } 38 | ] 39 | 40 | app.pods do 41 | pod 'PureLayout' 42 | pod 'SDWebImage' 43 | pod 'SSDataSources' 44 | pod 'SWRevealViewController' 45 | pod 'HHRouter' 46 | pod 'SVProgressHUD' 47 | pod 'OpenShare' 48 | pod 'SVWebViewController' 49 | pod 'EGOCache' 50 | pod 'MWFeedParser' 51 | pod 'NSHash', '~> 1.0.1' 52 | pod 'MCSwipeTableViewCell' 53 | pod 'HTMLReader' 54 | pod 'AVOSCloud' 55 | pod 'LeanCloudFeedback' 56 | end 57 | 58 | app.release do 59 | app.codesign_certificate = 'iPhone Distribution: shi weifu (9KL9R4225X)' 60 | app.provisioning_profile = '/Users/shiweifu/Downloads/oneread_appstore_dist.mobileprovision' 61 | # app.provisioning_profile = '/Users/shiweifu/Downloads/oneread_adhoc_dist.mobileprovision' 62 | # app.entitlements['beta-reports-active'] = true 63 | end 64 | 65 | 66 | app.development do 67 | app.codesign_certificate = 'iPhone Developer: shi weifu (W2W2V39367)' 68 | app.provisioning_profile = '/Users/shiweifu/Downloads/oneread_development.mobileprovision' 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | 3 | include Common 4 | include Define 5 | 6 | attr_accessor :revealController 7 | 8 | def application(app, openURL:url, sourceApplication:sa, annotation:ann) 9 | if (OpenShare.handleOpenURL(url)) 10 | p "handle open share with url: #{url}" 11 | return true 12 | end 13 | 14 | if url.absoluteString == "pocketapp5678:authorizationFinished" 15 | puts("准备获取access_token") 16 | pocket_code = NSUserDefaults[:pocket_code] 17 | params = {"consumer_key" => Define::POCKET_CONSUMER_KEY, "code" => pocket_code} 18 | puts("auth params:#{params}") 19 | Http.post_form("https://getpocket.com/v3/oauth/authorize", params) do | result | 20 | p "----auth result: #{result}" 21 | result = result.nsstring 22 | access_token = result.split("=")[1].split("&")[0] 23 | NSUserDefaults[:pocket_access_token] = access_token 24 | end 25 | end 26 | 27 | end 28 | 29 | 30 | def application(application, didFinishLaunchingWithOptions:launchOptions) 31 | startController = StartController.new 32 | startController.view.backgroundColor = UIColor.whiteColor 33 | 34 | nav = UINavigationController.alloc.initWithRootViewController(startController) 35 | 36 | nav.navigationBar.alpha = 0 37 | 38 | menu_controller = MenuController.alloc.init 39 | 40 | @revealController = SWRevealViewController.alloc.initWithRearViewController(menu_controller,frontViewController:nav) 41 | 42 | @revealController.rearViewRevealWidth = 150 43 | @revealController.rearViewRevealOverdraw = 0 44 | @revealController.frontViewShadowRadius = 0.5 45 | @revealController.frontViewShadowColor = :gray.uicolor 46 | 47 | c = EGOCache.globalCache 48 | i = 86400 * 30 * 12 49 | c.setDefaultTimeoutInterval(i) 50 | 51 | @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds) 52 | @window.rootViewController = @revealController 53 | @window.makeKeyAndVisible 54 | 55 | setup_cloud 56 | setup_router 57 | true 58 | 59 | end 60 | 61 | def setup_cloud 62 | AVOSCloud.setApplicationId(AVCLOUD_ID, clientKey:AVCLOUD_KEY) 63 | OpenShare.connectWeixinWithAppId(WEIXIN_ID) 64 | end 65 | 66 | 67 | def setup_router 68 | register_router("/list") do | params | 69 | list = ListController.alloc.init 70 | list.params = params 71 | list 72 | end 73 | 74 | register_router("/about") do | params | 75 | list = AboutController.alloc.init 76 | list.params = params 77 | list 78 | end 79 | 80 | 81 | register_router("/history") do | params | 82 | list = HistoryController.alloc.init 83 | list.params = params 84 | list 85 | end 86 | 87 | register_router("/source") do | params | 88 | 89 | sc = SourceController.alloc.init 90 | sc.params = params 91 | sc 92 | end 93 | 94 | register_router("/read") do | params | 95 | rc = ReadController.alloc.init 96 | rc.params = params 97 | rc 98 | end 99 | 100 | register_router("/setting") do | params | 101 | rc = SettingController.alloc.initWithStyle(UITableViewStyleGrouped) 102 | rc.params = params 103 | rc 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /app/controller/about_controller.rb: -------------------------------------------------------------------------------- 1 | class AboutController < UIViewController 2 | def viewDidLoad 3 | self.edgesForExtendedLayout = UIRectEdgeNone 4 | self.view.backgroundColor = "#e5e5e5".uicolor 5 | 6 | 7 | logo_view = UIImageView.nw 8 | self.view << logo_view 9 | logo_view.pin_size [60, 60] 10 | logo_view.image = 'Icon'.uiimage 11 | 12 | 13 | if is_iphone6? 14 | logo_view_top_offset = 65 15 | logoLabelTop = 25; 16 | versionLabelTop = 20; 17 | infoLabelBottom = 20; 18 | elsif is_iphone6p? 19 | logo_view_top_offset = 80 20 | logoLabelTop = 40; 21 | versionLabelTop = 35; 22 | infoLabelBottom = 35; 23 | else 24 | logo_view_top_offset = 40 25 | logoLabelTop = 20; 26 | versionLabelTop = 20; 27 | infoLabelBottom = 20; 28 | end 29 | 30 | 31 | logoLabel = UILabel.alloc.init 32 | logoLabel.font = UIFont.boldSystemFontOfSize(17) 33 | logoLabel.textColor = "#222222".uicolor 34 | logoLabel.textAlignment = NSTextAlignmentCenter 35 | logoLabel.text = "简洁的可定制化阅读工具" 36 | self.view.addSubview(logoLabel) 37 | 38 | versionLabel = UILabel.alloc.init 39 | versionLabel.font = UIFont.systemFontOfSize(12) 40 | versionLabel.textColor = "#666666".uicolor 41 | versionLabel.textAlignment = NSTextAlignmentCenter 42 | versionLabel.text = NSString.stringWithFormat(app_version) 43 | self.view.addSubview(versionLabel) 44 | 45 | infoLabel = UILabel.alloc.init 46 | infoLabel.numberOfLines = 0 47 | infoLabel.backgroundColor = UIColor.clearColor 48 | infoLabel.font = UIFont.systemFontOfSize(12) 49 | infoLabel.textColor = "#666666".uicolor 50 | infoLabel.textAlignment = NSTextAlignmentCenter 51 | infoLabel.text = NSString.stringWithFormat("官网:https://coding.net \nE-mail:link@coding.net \n微博:Coding \n微信:扣钉Coding") 52 | self.view.addSubview(infoLabel) 53 | 54 | logoLabel.center_v_with_view(self.view) 55 | logoLabel.pin_edge_to_view_edge(:top, logo_view, :bottom, logoLabelTop) 56 | 57 | versionLabel.center_v_with_view(self.view) 58 | versionLabel.pin_edge_to_view_edge(:top, logoLabel, :bottom, versionLabelTop) 59 | 60 | p "top_offset: #{logo_view_top_offset}" 61 | 62 | logo_view.center_v_with_view(self.view) 63 | logo_view.pin_to_sueprview_edge_with_offset(:top, logo_view_top_offset) 64 | 65 | 66 | webSiteLabel = UILabel.nw 67 | webSiteLabel.textColor = "#666666".uicolor 68 | webSiteLabel.font = UIFont.systemFontOfSize(12) 69 | webSiteLabel.text = "http://lonelygod.me/" 70 | self.view << webSiteLabel 71 | webSiteLabel.center_v_with_view(self.view) 72 | webSiteLabel.pin_to_sueprview_edge_with_offset(:bottom, 30) 73 | 74 | discoveryLabel = UILabel.nw 75 | discoveryLabel.textColor = "#666666".uicolor 76 | discoveryLabel.font = UIFont.systemFontOfSize(12) 77 | discoveryLabel.text = "「来自D版带着爱」" 78 | self.view << discoveryLabel 79 | discoveryLabel.center_v_with_view(self.view) 80 | discoveryLabel.pin_edge_to_view_edge_with_offset(:bottom, webSiteLabel, :top, -10) 81 | 82 | self.title = title_text 83 | end 84 | 85 | def title_text 86 | params['title'] 87 | 88 | end 89 | 90 | 91 | end -------------------------------------------------------------------------------- /app/controller/history_controller.rb: -------------------------------------------------------------------------------- 1 | class HistoryCell < SSBaseTableCell 2 | 3 | def self.cellStyle 4 | UITableViewCellStyleDefault 5 | end 6 | 7 | end 8 | 9 | class HistoryController < UITableViewController 10 | 11 | def viewWillAppear(animated) 12 | revealController = self.revealViewController 13 | self.navigationController.view.addGestureRecognizer(revealController.panGestureRecognizer) 14 | self.navigationController.view.addGestureRecognizer(revealController.tapGestureRecognizer) 15 | end 16 | 17 | def viewWillDisappear(animated) 18 | revealController = self.revealViewController 19 | self.navigationController.view.removeGestureRecognizer(revealController.panGestureRecognizer) 20 | self.navigationController.view.removeGestureRecognizer(revealController.tapGestureRecognizer) 21 | end 22 | 23 | 24 | def viewDidLoad 25 | @items = object_from_cache(:history) 26 | @items = @items.map { | json_obj | 27 | Item.new json_obj 28 | } 29 | 30 | @data_source = SSArrayDataSource.alloc.initWithItems(@items) 31 | @data_source.cellConfigureBlock = lambda { | cell, it, parent_view, index_path | 32 | t = it.name 33 | # t = date_to_str(it.access_date, '[M-dd]') + ' ' + t 34 | cell.textLabel.text = t 35 | } 36 | 37 | @data_source.tableActionBlock = lambda {|at, pv, ip| false } 38 | @data_source.cellClass = HistoryCell 39 | 40 | @data_source.tableView = self.tableView 41 | tableView.dataSource = @data_source 42 | 43 | self.title = '历史' 44 | 45 | menu_bar_item = UIBarButtonItem.alloc.initWithImage(UIImage.imageNamed("menu"), style:UIBarButtonItemStylePlain, target:self.revealViewController, action:'revealToggle:') 46 | # clear_bar_item = UIBarButtonItem.alloc.initWithTitle('清除', style:UIBarButtonItemStylePlain, target:self.revealViewController, action:'revealToggle:') 47 | 48 | clear_bar_item = UIBarButtonItem.titled('清除') do 49 | 50 | UIAlertView.alert('确认清除历史记录?', 51 | message: '', 52 | buttons: ['取消', '确认'], 53 | ) do |button, button_index| 54 | if button == '确认' # or: button_index == 1 55 | remove_key(:history) 56 | @data_source.clearItems 57 | tableView.reloadData 58 | end 59 | end 60 | 61 | end 62 | 63 | 64 | self.navigationItem.leftBarButtonItem = menu_bar_item 65 | self.navigationItem.rightBarButtonItem = clear_bar_item 66 | 67 | end 68 | 69 | def tableView(tableView, didSelectRowAtIndexPath:indexPath) 70 | p "didSelect" 71 | 72 | tableView.deselectRowAtIndexPath(indexPath, animated:true) 73 | 74 | it = @data_source.itemAtIndexPath(indexPath) 75 | p "-----it", it 76 | rc = ReadController.new 77 | rc.item = it 78 | navigationController.pushViewController(rc, animated:true) 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /app/controller/list_controller.rb: -------------------------------------------------------------------------------- 1 | include Common 2 | include Router 3 | 4 | class ListController < UITableViewController 5 | 6 | attr_accessor :source 7 | 8 | def viewWillAppear(animated) 9 | revealController = self.revealViewController 10 | self.navigationController.view.addGestureRecognizer(revealController.panGestureRecognizer) 11 | self.navigationController.view.addGestureRecognizer(revealController.tapGestureRecognizer) 12 | end 13 | 14 | def viewWillDisappear(animated) 15 | revealController = self.revealViewController 16 | self.navigationController.view.removeGestureRecognizer(revealController.panGestureRecognizer) 17 | self.navigationController.view.removeGestureRecognizer(revealController.tapGestureRecognizer) 18 | end 19 | 20 | 21 | def viewDidLoad 22 | self.title = @source.display_name 23 | 24 | if source 25 | @dds = DataSource.build({source: source}) 26 | 27 | @ds = SSArrayDataSource.alloc.initWithItems([]) 28 | @ds.tableView = self.tableView 29 | @ds.tableActionBlock = lambda {|at, pv, ip| false } 30 | @ds.cellConfigureBlock = lambda do | cell, obj, parentview, indexPath | 31 | cell.textLabel.text = obj.name 32 | end 33 | 34 | refresh 35 | 36 | else 37 | msg "加载错误" 38 | end 39 | 40 | setup_nav_bar_items 41 | 42 | end 43 | 44 | def setup_nav_bar_items 45 | 46 | menu_item = UIBarButtonItem.imaged(:menu.uiimage) do 47 | p "load menu icon" 48 | self.revealViewController.revealToggle(nil) 49 | end 50 | 51 | self.navigationItem.leftBarButtonItem = menu_item 52 | 53 | refresh_item = UIBarButtonItem.imaged(:refresh.uiimage) do 54 | refresh 55 | end 56 | 57 | next_item = UIBarButtonItem.imaged(:next.uiimage) do 58 | next_page 59 | end 60 | 61 | back_item = UIBarButtonItem.imaged(:back.uiimage) do 62 | back_page 63 | end 64 | 65 | right_bar_button_items = [refresh_item, next_item, back_item, ] 66 | 67 | # 只有日报类的具有翻页功能 68 | unless source.instance_of? JSSource 69 | right_bar_button_items = [refresh_item] 70 | end 71 | 72 | self.navigationItem.rightBarButtonItems = right_bar_button_items 73 | end 74 | 75 | 76 | def refresh 77 | @page = 1 78 | @dds.page = @page 79 | reload_items 80 | end 81 | 82 | def reload_items 83 | @dds.page = @page 84 | 85 | msg("正在加载..") 86 | 87 | Dispatch::Queue.concurrent.async do 88 | @dds.items do | its | 89 | Dispatch::Queue.main.async do 90 | @ds.clearItems 91 | @ds.appendItems its 92 | hide_msg 93 | tableView.reloadData 94 | end 95 | end 96 | end 97 | end 98 | 99 | def next_page 100 | @page += 1 101 | @dds.page = @page 102 | reload_items 103 | end 104 | 105 | def back_page 106 | @page -= 1 107 | if @page < 1 108 | @page = 1 109 | end 110 | @dds.page = @page 111 | reload_items 112 | end 113 | 114 | def tableView(tv, didSelectRowAtIndexPath: indexPath) 115 | p "didSelect" 116 | 117 | tv.deselectRowAtIndexPath(indexPath, animated:true) 118 | 119 | it = @ds.itemAtIndexPath(indexPath) 120 | p "-----it", it 121 | 122 | history_list = [].concat(object_from_cache(:history) || []) 123 | # 确保不会重复添加 124 | history_list.delete_if { | hl | hl[:link] == it.link } 125 | 126 | hash_obj = it.to_hash 127 | p "----hash obj: #{hash_obj}" 128 | history_list.insert(0, hash_obj) 129 | cache_object(history_list, :history) 130 | 131 | rc = ReadController.new 132 | rc.item = it 133 | navigationController.pushViewController(rc, animated:true) 134 | end 135 | 136 | end 137 | -------------------------------------------------------------------------------- /app/controller/menu_controller.rb: -------------------------------------------------------------------------------- 1 | include Common 2 | 3 | class MenuController < UITableViewController 4 | 5 | TOP_CELL_HEIGHT = 64.5 6 | 7 | def viewDidLoad 8 | self.tableView.setTableFooterView(UIView.new) 9 | 10 | "article_source_changed".add_observer(self, "on_source_changed") 11 | 12 | refresh 13 | end 14 | 15 | def on_source_changed 16 | refresh 17 | end 18 | 19 | def dealloc 20 | "article_source_changed".remove_observer(self) 21 | end 22 | 23 | def refresh 24 | @ds = SSArrayDataSource.alloc.initWithItems(menu_items) 25 | @ds.tableView = self.tableView 26 | @ds.tableActionBlock = lambda {|at, pv, ip| false } 27 | @ds.cellConfigureBlock = lambda do | cell, obj, parentview, indexPath | 28 | 29 | if indexPath.row == 0 30 | # 第一行 31 | cell.selectionStyle = UITableViewCellSelectionStyleNone 32 | cell.textLabel.text = "" 33 | elsif indexPath.row == menu_items.size - 1 || indexPath.row == menu_items.size - 2 34 | # 设置和历史 35 | cell.textLabel.text = obj 36 | else 37 | # 源,显示名字 38 | cell.textLabel.text = obj.display_name 39 | end 40 | end 41 | end 42 | 43 | def tableView(tv, didSelectRowAtIndexPath: indexPath) 44 | tableView.deselectRowAtIndexPath(indexPath, animated:true) 45 | if indexPath.row == 0 46 | 47 | elsif indexPath.row == menu_items.count - 1 48 | # 设置 49 | c = find_router "/setting" 50 | present_controller c 51 | elsif indexPath.row == menu_items.count - 2 52 | # 历史 53 | history_controller = find_router("/history", dic={}) 54 | open_controller history_controller 55 | else 56 | c = ListController.new 57 | c.source = menu_items[indexPath.row] 58 | p "----c.source: #{c.source}" 59 | open_controller c 60 | end 61 | end 62 | 63 | 64 | def menu_items 65 | items = Config.menu_items 66 | items.concat(["历史", "设置"]) 67 | items = [""].concat(items) 68 | items 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /app/controller/read_controller.rb: -------------------------------------------------------------------------------- 1 | include Common 2 | 3 | class ReadController < UIViewController 4 | 5 | attr_accessor :item 6 | attr_accessor :params 7 | 8 | def viewDidLoad 9 | @web_view = UIWebView.nw 10 | @web_view.setBackgroundColor(UIColor.whiteColor) 11 | view.addSubview(@web_view) 12 | @web_view.delegate = self 13 | @web_view.pin_to_superview 14 | self.title = self.title_text 15 | load_data 16 | 17 | action_item = UIBarButtonItem.alloc.initWithBarButtonSystemItem(UIBarButtonSystemItemAction, target:self, action:"on_action:") 18 | pocket_item = UIBarButtonItem.alloc.initWithBarButtonSystemItem(UIBarButtonSystemItemAdd, target:self, action:"on_save:") 19 | 20 | self.navigationItem.setRightBarButtonItems([action_item, pocket_item]) 21 | end 22 | 23 | 24 | def webView(webView, shouldStartLoadWithRequest: request, navigationType: navigationType) 25 | url = request.URL.absoluteString 26 | components = NSURLComponents.componentsWithString(url) 27 | if components.scheme == "image" 28 | img_path = "http:#{components.path}" 29 | # msg(img_path) 30 | # 1.second.later do 31 | # hide_msg 32 | self.open_image(img_path) 33 | # end 34 | elsif components.scheme == "url" 35 | if components.path == "" 36 | false 37 | end 38 | full_url = "http:#{components.path}" 39 | UIActionSheet.alert('选择操作', buttons: ['取消', '保存到Pocket', '打开链接'] 40 | ) do |button| 41 | self.save_to_pocket(full_url) if button == '保存到Pocket' 42 | self.open_url(full_url) if button == '打开链接' 43 | end 44 | end 45 | true 46 | end 47 | 48 | def webViewDidFinishLoad(webView) 49 | hide_msg 50 | end 51 | 52 | def load_data 53 | req = self.link.nsurl.nsurlrequest 54 | @web_view.loadRequest(req) 55 | end 56 | 57 | def url 58 | @item.link 59 | end 60 | 61 | def link 62 | @item.link 63 | end 64 | 65 | def model_id 66 | @item.model_id 67 | end 68 | 69 | def title_text 70 | @item.name 71 | end 72 | 73 | def on_action(sender) 74 | UIActionSheet.alert('选择操作', buttons: ['取消', nil, '分享给微信好友', '分享到微信朋友圈', '跳转网页'] 75 | ) do |button| 76 | self.share_to_wx_friend if button == '分享给微信好友' 77 | self.share_to_wx_group if button == '分享到微信朋友圈' 78 | self.jump_to_web if button == '跳转网页' 79 | end 80 | end 81 | 82 | def on_save(sender) 83 | UIActionSheet.alert('选择操作', buttons: ['取消', nil, '保存到Pocket'] 84 | ) do |button| 85 | self.save_to_pocket link if button == '保存到Pocket' 86 | end 87 | end 88 | 89 | def share_to_wx_friend 90 | m = OSMessage.new 91 | m.title = '' 92 | m.desc = title_text 93 | m.link = link 94 | m.image="Icon-60".uiimage.nsdata 95 | 96 | share_success = lambda { | m | 97 | msg('分享成功', type=:success) 98 | } 99 | 100 | share_failure = lambda { | m, e | 101 | msg('分享失败', type=:failure) 102 | } 103 | 104 | OpenShare.shareToWeixinSession(m, Success:share_success, Fail:share_failure) 105 | end 106 | 107 | def share_to_wx_group 108 | 109 | m = OSMessage.new 110 | m.title = title_text 111 | m.desc = '' 112 | m.link = link 113 | m.image="Icon-60".uiimage.nsdata 114 | 115 | 116 | share_success = lambda { | m | 117 | msg('分享成功', type=:success) 118 | } 119 | 120 | share_failure = lambda { | m, e | 121 | msg('分享失败', type=:failure) 122 | } 123 | 124 | OpenShare.shareToWeixinTimeline(m, Success:share_success, Fail:share_failure) 125 | 126 | end 127 | 128 | def jump_to_web 129 | url = self.link 130 | url.nsurl.open 131 | end 132 | 133 | 134 | ############################################################################################## 135 | # action 136 | ############################################################################################## 137 | 138 | def save_to_evernote(src) 139 | 140 | end 141 | 142 | def save_to_pocket(src) 143 | access_token = NSUserDefaults[:pocket_access_token] 144 | 145 | puts("pocket access_token: #{access_token}") 146 | p "----src: #{src}" 147 | 148 | if access_token 149 | params = {"url" => src, 150 | "consumer_key" => Define::POCKET_CONSUMER_KEY, 151 | "access_token" => access_token} 152 | p "---params: #{params}" 153 | Http.post_form("https://getpocket.com/v3/add", params) do | s | 154 | p "---add to pocket: #{s}" 155 | msg("已保存到Pocket", type=:success) 156 | end 157 | else 158 | params = {"consumer_key" => Define::POCKET_CONSUMER_KEY, "redirect_uri" => "pocketapp5678:authorizationFinished"} 159 | Http.post_form("https://getpocket.com/v3/oauth/request", params) do | s | 160 | s = s.nsstring 161 | print(s.class) 162 | code = s.split("=")[1] 163 | NSUserDefaults[:pocket_code] = code 164 | url = "https://getpocket.com/auth/authorize?request_token=#{code}&redirect_uri=pocketapp5678:authorizationFinished" 165 | puts("auth url:#{url}") 166 | url.nsurl.open 167 | end 168 | end 169 | end 170 | 171 | def open_url(src) 172 | wvc = SVModalWebViewController.alloc.initWithAddress(src) 173 | self.presentViewController(wvc, animated:true, completion:nil) 174 | end 175 | 176 | def open_image(src) 177 | MotionImager.new({ 178 | url: src, 179 | placeholder: 'placeholder', 180 | presenting_from: WeakRef.new(self), 181 | }).show 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /app/controller/setting_controller.rb: -------------------------------------------------------------------------------- 1 | class SettingController < UITableViewController 2 | 3 | include Router 4 | include Common 5 | 6 | def viewDidLoad 7 | @titles = [['关于', '分享给朋友', '去AppStore打分'], ['反馈']] 8 | 9 | self.title = '设置' 10 | 11 | c = UIBarButtonItem.titled('关闭') do 12 | self.dismissViewControllerAnimated(true, completion:nil) 13 | end 14 | 15 | self.navigationItem.leftBarButtonItem = c 16 | 17 | r = UIBarButtonItem.titled('选择源') do 18 | s = find_router('/source', dic={}) 19 | nav = UINavigationController.alloc.initWithRootViewController(s) 20 | self.presentViewController(nav, animated:true, completion:nil) 21 | end 22 | 23 | self.navigationItem.rightBarButtonItem = r 24 | self.tableView.setTableFooterView(UIView.new); 25 | end 26 | 27 | def numberOfSectionsInTableView(table_view) 28 | @titles.count 29 | end 30 | 31 | def tableView(tableView, numberOfRowsInSection: section) 32 | @titles[section].count 33 | end 34 | 35 | def tableView(tableView, heightForRowAtIndexPath: indexPath) 36 | 44 37 | end 38 | 39 | def tableView(tableView, cellForRowAtIndexPath: indexPath) 40 | 41 | t = @titles[indexPath.section][indexPath.row] 42 | c = UITableViewCell.default('cell_identifier', 43 | accessory: :disclosure.uitablecellaccessory, 44 | selection: :blue.uitablecellselectionstyle, 45 | text: t, 46 | image: '', ) 47 | 48 | c 49 | end 50 | 51 | def tableView(tableView, didSelectRowAtIndexPath:indexPath) 52 | 53 | tableView.deselectRowAtIndexPath(indexPath, animated:true) 54 | 55 | if indexPath.section == 1 && indexPath.row == 0 56 | # 反馈 57 | feedbackViewController = LCUserFeedbackViewController.alloc.init 58 | feedbackViewController.feedbackTitle = nil 59 | feedbackViewController.contact = nil 60 | feedbackViewController.contactHeaderHidden = true 61 | feedbackViewController.presented = true 62 | feedbackViewController.navigationBarStyle = LCUserFeedbackNavigationBarStyleNone 63 | present_controller feedbackViewController 64 | elsif indexPath.section == 0 && indexPath.row == 0 65 | # 关于 66 | controller = find_router("/about", {"title" => "关于"}) 67 | self.navigationController.pushViewController(controller, animated:true) 68 | elsif indexPath.section == 0 && indexPath.row == 1 69 | # 分享给朋友 70 | m = OSMessage.new 71 | m.title = '' 72 | m.desc = '试试这个阅读应用吧' 73 | m.link = Config::APP_URL 74 | m.image="Icon-60".uiimage.nsdata 75 | 76 | share_success = lambda { | m | 77 | msg('谢谢您', type=:success) 78 | } 79 | 80 | share_failure = lambda { | m, e | 81 | msg('莫名的失败', type=:failure) 82 | } 83 | 84 | OpenShare.shareToWeixinSession(m, Success:share_success, Fail:share_failure) 85 | 86 | elsif indexPath.section == 0 && indexPath.row == 2 87 | # 去appstore 打分 88 | UIApplication.sharedApplication.openURL(NSURL.URLWithString(Config::APP_REVIEW_URL)) 89 | end 90 | end 91 | 92 | def mailComposeController(controller, didFinishWithResult:result, error:err) 93 | if result == MFMailComposeResultSent 94 | msg("发送成功", type=:success) 95 | end 96 | self.dismissViewControllerAnimated(true, completion:nil) 97 | end 98 | 99 | end 100 | -------------------------------------------------------------------------------- /app/controller/source_controller.rb: -------------------------------------------------------------------------------- 1 | include Common 2 | 3 | class SourceController < UIViewController 4 | 5 | def viewDidLoad 6 | 7 | self.view.backgroundColor = :white.uicolor 8 | 9 | self.view.addSubview(self.table_view) 10 | self.table_view.pin_to_superview 11 | self.table_view.delegate = self 12 | 13 | # 已经选择了的源 14 | 15 | self.navigationItem.titleView = self.segmented_control 16 | 17 | right_item = UIBarButtonItem.done do 18 | cache_object(@rss_list, :rss_list) 19 | cache_object(@daily_list, :daily_list) 20 | cache_object(@ten_read_list, :ten_read_list) 21 | 22 | # 确保缓存已经写入 23 | "article_source_changed".post_notification 24 | 25 | self.dismissViewControllerAnimated(true, completion:nil) 26 | end 27 | 28 | left_item = UIBarButtonItem.cancel do 29 | self.dismissViewControllerAnimated(true, completion:nil) 30 | end 31 | 32 | self.navigationItem.rightBarButtonItem = right_item 33 | self.navigationItem.leftBarButtonItem = left_item 34 | 35 | self.table_view.setTableFooterView(UIView.new) 36 | 37 | setup_data_source 38 | 39 | self.segmented_control.selectedSegmentIndex = 0 40 | self.segmented_control.sendActionsForControlEvents(UIControlEventValueChanged) 41 | 42 | @daily_list = object_from_cache(:daily_list) || [] 43 | @rss_list = object_from_cache(:rss_list) || [] 44 | @ten_read_list = object_from_cache(:ten_read_list) || [] 45 | 46 | @daily_list = [].concat @daily_list 47 | @rss_list = [].concat @rss_list 48 | @ten_read_list = [].concat @ten_read_list 49 | 50 | end 51 | 52 | def setup_data_source 53 | @data_source = SSArrayDataSource.alloc.initWithItems([]) 54 | @data_source.tableView = self.table_view 55 | @data_source.cellConfigureBlock = lambda { |cell, object, parent_view, index_path| 56 | cell.textLabel.text = object.display_name 57 | } 58 | 59 | @data_source.cellCreationBlock = lambda { |object, parent_view, index_path| 60 | a = :none 61 | 62 | p "----object:#{object}" 63 | 64 | if segmented_control.selectedSegmentIndex == 0 65 | # Daily 66 | a = :checkmark unless @daily_list.select { |n| n[:name] == object.display_name }.empty? 67 | elsif segmented_control.selectedSegmentIndex == 1 68 | # RSS 69 | a = :checkmark unless @rss_list.select { |n| n[:name] == object.display_name }.empty? 70 | 71 | elsif segmented_control.selectedSegmentIndex == 2 72 | # TenRead 73 | a = :checkmark unless @ten_read_list.select { |n| n[:name] == object.display_name }.empty? 74 | end 75 | 76 | cell = UITableViewCell.default('cell_identifier', 77 | accessory: a, 78 | selection: :blue, 79 | text: '') 80 | cell 81 | } 82 | 83 | @data_source.tableActionBlock = lambda {|at, pv, ip| false } 84 | table_view.dataSource = @data_source 85 | end 86 | 87 | def table_view 88 | @table_view = UITableView.nw unless @table_view 89 | @table_view 90 | end 91 | 92 | def segmented_control 93 | unless @segmented_control 94 | @segmented_control = UISegmentedControl.bar(['日报', 'RSS', '十阅']) 95 | @segmented_control.addTarget(self, action:'on_segmented_control_change:', forControlEvents:UIControlEventValueChanged) 96 | end 97 | @segmented_control 98 | end 99 | 100 | # 上面的segmented按钮选择的时候,设置当前的tableView的dataSource 101 | def on_segmented_control_change(sender) 102 | # 选择日报的时候,需要刷新列表 103 | if sender.selectedSegmentIndex == 0 104 | refresh_daily_source 105 | elsif sender.selectedSegmentIndex == 1 106 | # 选择RSS的时候,不需要刷新列表 107 | refresh_rss_source 108 | elsif sender.selectedSegmentIndex == 2 109 | refresh_ten_read_source 110 | end 111 | end 112 | 113 | def tableView(tableView, didSelectRowAtIndexPath:indexPath) 114 | tableView.deselectRowAtIndexPath(indexPath, animated:true) 115 | 116 | s = @data_source.itemAtIndexPath indexPath 117 | h = s.json_obj 118 | 119 | cell = tableView.cellForRowAtIndexPath(indexPath) 120 | if cell.accessoryType == UITableViewCellAccessoryCheckmark 121 | cell.accessoryType = UITableViewCellAccessoryNone 122 | 123 | if segmented_control.selectedSegmentIndex == 0 124 | # Daily 125 | @daily_list.delete_if { | it | it[:url] == h[:url] } 126 | elsif segmented_control.selectedSegmentIndex == 1 127 | # RSS 128 | @rss_list.delete_if { | it | it[:url] == h[:url] } 129 | elsif segmented_control.selectedSegmentIndex == 2 130 | # TenRead 131 | @ten_read_list.delete_if { | it | it[:url] == h[:url] } 132 | end 133 | else 134 | cell.accessoryType = UITableViewCellAccessoryCheckmark 135 | 136 | if segmented_control.selectedSegmentIndex == 0 137 | # Daily 138 | @daily_list << h 139 | elsif segmented_control.selectedSegmentIndex == 1 140 | # RSS 141 | @rss_list << h 142 | 143 | elsif segmented_control.selectedSegmentIndex == 2 144 | # TenRead 145 | @ten_read_list << h 146 | end 147 | end 148 | 149 | end 150 | 151 | def refresh_ten_read_source 152 | list_url = Config::APP_TEN_READ_SOURCE_LIST_URL 153 | 154 | @data_source.clearItems 155 | Http.get_string(list_url, {}) do | items_str | 156 | @data_source.clearItems 157 | items = BW::JSON.parse(items_str) 158 | items = items.map { | it | 159 | TRSource.build(it) 160 | } 161 | @data_source.appendItems items 162 | end 163 | end 164 | 165 | def refresh_rss_source 166 | list_url = Config::APP_RSS_SOURCE_LIST_URL 167 | @data_source.clearItems 168 | 169 | Http.get_string(list_url, {}) do | items_str | 170 | @data_source.clearItems 171 | items = BW::JSON.parse(items_str) 172 | items = items.map { | it | 173 | url = it['url'] 174 | h = {name: it['name'], url: url} 175 | RSSSource.build(h) 176 | } 177 | @data_source.appendItems items 178 | end 179 | end 180 | 181 | # 从服务器上拉数据回来 182 | def refresh_daily_source 183 | list_url = Config::APP_DAILY_SOURCE_LIST_URL 184 | @data_source.clearItems 185 | 186 | Http.get_string(list_url, {}) do | items_str | 187 | @data_source.clearItems 188 | items = BW::JSON.parse(items_str) 189 | items = items.map { | it | 190 | url = Config::APP_DAILY_SOURCE_BASE_URL + it['router_name'] 191 | h = {name: it['name'], url: url} 192 | JSSource.build(h) 193 | } 194 | @data_source.appendItems items 195 | end 196 | end 197 | end 198 | 199 | -------------------------------------------------------------------------------- /app/controller/start_controller.rb: -------------------------------------------------------------------------------- 1 | class StartController < UITableViewController 2 | 3 | def viewDidLoad 4 | menu_item = UIBarButtonItem.imaged(:menu.uiimage) do 5 | p "load menu icon" 6 | self.revealViewController.revealToggle(nil) 7 | end 8 | 9 | self.navigationItem.leftBarButtonItem = menu_item 10 | end 11 | 12 | def viewWillAppear(animated) 13 | revealController = self.revealViewController 14 | self.navigationController.view.addGestureRecognizer(revealController.panGestureRecognizer) 15 | self.navigationController.view.addGestureRecognizer(revealController.tapGestureRecognizer) 16 | end 17 | 18 | def viewWillDisappear(animated) 19 | revealController = self.revealViewController 20 | self.navigationController.view.removeGestureRecognizer(revealController.panGestureRecognizer) 21 | self.navigationController.view.removeGestureRecognizer(revealController.tapGestureRecognizer) 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /app/data/data_source.rb: -------------------------------------------------------------------------------- 1 | # 得到所有的日报类源,并作为TableView的DataSource来使用 2 | 3 | class DataSource < SSArrayDataSource 4 | 5 | attr_accessor :page 6 | 7 | def initialize(params) 8 | @source = params[:source] 9 | @page = 1 10 | end 11 | 12 | def items(&cb) 13 | @source.load_router do | a | 14 | if a 15 | # 加载页面 16 | @source.items_with_page(@page) do | items | 17 | if items 18 | cb.call(items) 19 | else 20 | cb.call([]) 21 | end 22 | end 23 | else 24 | cb.call([]) 25 | end 26 | end 27 | end 28 | 29 | def self.build(opts={}) 30 | 31 | unless opts.has_key? :source 32 | raise ArgumentError.new('source should not set') 33 | end 34 | 35 | DataSource.new opts 36 | 37 | end 38 | 39 | end 40 | 41 | -------------------------------------------------------------------------------- /app/data/one_store.rb: -------------------------------------------------------------------------------- 1 | # 这里面直接调用网络接口 2 | 3 | class OneStore 4 | DAILY_LIST_URL = 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/list' 5 | DAILY_SOURCE_BASE_URL = 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/' 6 | 7 | def self.daily_source_list(&callback) 8 | Http.get_json(DAILY_LIST_URL, {}) do | result | 9 | source_list = result.map { | d | 10 | display_name = d[:name] 11 | source_path = d[:router_name] 12 | url = "#{DAILY_SOURCE_BASE_URL}#{source_path}" 13 | s = {url: url, name: display_name} 14 | JSSource.build s 15 | } 16 | callback.call source_list 17 | end 18 | end 19 | 20 | def self.rss_source_list(&callback) 21 | 22 | end 23 | 24 | 25 | end 26 | -------------------------------------------------------------------------------- /app/data/source.rb: -------------------------------------------------------------------------------- 1 | # 从Coding上拉下来的JSSource通过这个类进行生成 2 | 3 | class JSSource 4 | 5 | CELL_CLASS = BasicCell 6 | 7 | attr_reader :display_name 8 | attr_reader :is_loaded 9 | attr_reader :url 10 | 11 | def initialize(opts = {}) 12 | @url = opts[:url] 13 | @display_name = opts[:name] 14 | @is_loaded = false 15 | @js_context = JSContext.alloc.init 16 | 17 | end 18 | 19 | def load_router(&callback) 20 | 21 | if @is_loaded 22 | callback.call(true) 23 | end 24 | 25 | Http.get_string(@url, {}) do | result | 26 | if result 27 | @js_context.evaluateScript(result) 28 | @is_loaded = true 29 | callback.call(true) 30 | else 31 | callback.call(false) 32 | end 33 | end 34 | end 35 | 36 | def self.build(opts = {}) 37 | 38 | unless opts.has_key? :name or opts.has_key? :js_content 39 | raise ArgumentError.new('name and js_content should be set') 40 | end 41 | 42 | m = JSSource.new(opts) 43 | return m 44 | end 45 | 46 | def path 47 | "/list" 48 | end 49 | 50 | def url_with_model(m) 51 | detail_url_func = context_router["detail_url"] 52 | url_value = detail_url_func.callWithArguments([m.to_json]) 53 | result = url_value.toString 54 | result 55 | end 56 | 57 | def items_with_page(p, &complete) 58 | url = page_url p 59 | Http.get_string(url, {}) do | json_str | 60 | items = items_from_json json_str 61 | items = items.map do | it | 62 | it.link = url_with_model(it) 63 | it 64 | end 65 | 66 | complete.call(items) 67 | end 68 | end 69 | 70 | def cell_config_action 71 | l = lambda { |cell, object, parent_view, index_path| 72 | cell.model = object 73 | } 74 | l 75 | end 76 | 77 | def page_url(p) 78 | func = context_router["page_url"] 79 | result = func.callWithArguments([p]) 80 | result.toString 81 | end 82 | 83 | def json_obj 84 | {name: @display_name, url: @url} 85 | end 86 | 87 | def items_from_json(json_str) 88 | func = context_router["items_from_json"] 89 | result = func.callWithArguments([json_str]) 90 | items_str = result.toString 91 | items_hash = BW::JSON.parse(items_str) 92 | items = items_hash.map { | m | Item.new m} 93 | end 94 | 95 | private 96 | def context_router 97 | unless @context_router 98 | @context_router = @js_context['Router'] 99 | end 100 | @context_router 101 | end 102 | 103 | def detail_url(detail_url) 104 | func = context_router["page_url"] 105 | result = func.callWithArguments([detail_url]) 106 | result.toString 107 | end 108 | 109 | end 110 | 111 | 112 | class RSSSource 113 | 114 | CELL_CLASS = BasicCell 115 | 116 | attr_reader :display_name 117 | attr_reader :is_loaded 118 | 119 | def initialize(opts = {}) 120 | @url = opts[:url] 121 | @display_name = opts[:name] 122 | @is_loaded = false 123 | end 124 | 125 | def load_router(&callback) 126 | callback.call(true) 127 | end 128 | 129 | def self.build(opts = {}) 130 | 131 | unless opts.has_key? :name or opts.has_key? :url 132 | raise ArgumentError.new('name and url should be set') 133 | end 134 | 135 | m = RSSSource.new(opts) 136 | return m 137 | end 138 | 139 | def path 140 | "/list" 141 | end 142 | 143 | def url_with_model(m) 144 | m.link 145 | end 146 | 147 | def items_with_page(p, &complete) 148 | 149 | @items_complete_callback = complete 150 | 151 | @feed_parser = BW::RSSParser.new(@url) 152 | @feed_parser.delegate = self 153 | @rss_items = [] 154 | @feed_parser.parse do |item| 155 | it = Item.new({:id => item.guid, 156 | :title => item.title, 157 | :createdDate => item.pubDate, 158 | :link => item.link, 159 | :excerpt => ""}) 160 | @rss_items << it 161 | end 162 | 163 | end 164 | 165 | def page_url(p) 166 | @url 167 | end 168 | 169 | def when_parser_is_done 170 | @items_complete_callback.call(@rss_items) 171 | end 172 | 173 | def json_obj 174 | {name: @display_name, url: @url} 175 | end 176 | end 177 | 178 | 179 | class TRSource 180 | 181 | CELL_CLASS = BasicCell 182 | 183 | attr_reader :display_name 184 | attr_reader :is_loaded 185 | attr_reader :url 186 | 187 | def initialize(opts = {}) 188 | @url = opts[:url] 189 | @display_name = opts[:name] 190 | @selector = opts[:selector] 191 | @base_url = opts[:base_url] 192 | @is_loaded = false 193 | end 194 | 195 | def load_router(&callback) 196 | 197 | if @is_loaded 198 | callback.call(true) 199 | end 200 | 201 | Http.get_string(url, {}) do | html | 202 | if html 203 | document = HTMLDocument.documentWithString html 204 | list = document.nodesMatchingSelector(@selector) 205 | @article_list = list.map { | r | 206 | url = r.attributes['href'] 207 | unless url.index('http://') or url.index('https://') 208 | url = @base_url + url 209 | end 210 | 211 | Item.new({name: r.textContent, link: url}) 212 | } 213 | @is_loaded = true 214 | callback.call(true) 215 | else 216 | callback.call(false) 217 | end 218 | end 219 | end 220 | 221 | def self.build(opts = {}) 222 | 223 | unless opts.has_key? :name or 224 | opts.has_key? :url or 225 | opts.has_key? :selector or 226 | opts.has_key? :base_url 227 | raise ArgumentError.new('name, url, base_url and selector should be set') 228 | end 229 | 230 | m = TRSource.new(opts) 231 | return m 232 | end 233 | 234 | def path 235 | "/list" 236 | end 237 | 238 | def url_with_model(m) 239 | m.url 240 | end 241 | 242 | def items_with_page(p, &complete) 243 | complete.call(@article_list) 244 | end 245 | 246 | def cell_config_action 247 | l = lambda { |cell, object, parent_view, index_path| 248 | cell.model = object 249 | } 250 | l 251 | end 252 | 253 | def page_url(p) 254 | url 255 | end 256 | 257 | def json_obj 258 | {name: @display_name, url: @url, selector: @selector, base_url: @base_url} 259 | end 260 | 261 | end 262 | 263 | 264 | -------------------------------------------------------------------------------- /app/data/source_manager.rb: -------------------------------------------------------------------------------- 1 | 2 | # 管理本地缓存的Source列表 3 | class SourceManager 4 | 5 | # 得到本地所有的日报类Source 6 | def daily_sources 7 | [] 8 | end 9 | 10 | # 得到本地所有的RSS类Source 11 | def rss_sources 12 | [] 13 | end 14 | 15 | # 将一个js router对象添加到列表中并持久化 16 | def add_daily_source(s) 17 | 18 | end 19 | 20 | # 将一个rss router对象添加到列表中并持久化 21 | def add_rss_source(r) 22 | 23 | end 24 | 25 | end -------------------------------------------------------------------------------- /app/lib/common.rb: -------------------------------------------------------------------------------- 1 | module Common 2 | 3 | def feedback_info 4 | app_version = s = NSBundle.mainBundle.infoDictionary.objectForKey("CFBundleVersion") 5 | device_model = UIDevice.currentDevice.model 6 | system_version = UIDevice.currentDevice.systemVersion 7 | return NSString.stringWithFormat("\n\n\n设备: %@ \niOS版本: %@ \n客户端版本: v%@", device_model, system_version, app_version) 8 | 9 | end 10 | 11 | def class_from_string(s) 12 | Module.const_get(s) 13 | end 14 | 15 | def msg(text, type=:normal) 16 | case type 17 | when :success 18 | SVProgressHUD.showSuccessWithStatus(text) 19 | when :failure 20 | SVProgressHUD.showErrorWithStatus(text) 21 | else 22 | SVProgressHUD.showWithStatus(text) 23 | end 24 | end 25 | 26 | def hide_msg 27 | SVProgressHUD.dismiss 28 | end 29 | 30 | def read_file(filename) 31 | name, ext = filename.split(".") 32 | 33 | path = NSBundle.mainBundle.pathForResource(name, ofType: ext) 34 | 35 | puts("path:", path) 36 | 37 | string = NSString.stringWithContentsOfFile(path, encoding: NSUTF8StringEncoding, error: nil) 38 | end 39 | 40 | def date_to_str(d, f='yyyyMMdd') 41 | d.string_with_format(f, options={:unicode => true}) 42 | end 43 | 44 | def cache_object(obj, k) 45 | NSUserDefaults[k] = obj 46 | end 47 | 48 | def remove_key(k) 49 | NSUserDefaults[k] = nil 50 | end 51 | 52 | def object_from_cache(k) 53 | NSUserDefaults[k] 54 | end 55 | 56 | def is_iphone6? 57 | [Device.screen.width, Device.screen.height] == [750/2, 1334/2] 58 | end 59 | 60 | def is_iphone6p? 61 | [Device.screen.width, Device.screen.height] == [1242/2, 2208/2] 62 | end 63 | 64 | def is_iphone5? 65 | [Device.screen.width, Device.screen.height] == [640/2, 1136/2] 66 | end 67 | 68 | def app_version 69 | NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleVersion") 70 | end 71 | 72 | def present_controller(c) 73 | sd = App.shared.delegate 74 | nav = UINavigationController.alloc.initWithRootViewController(c) 75 | self.presentViewController(nav, animated:true, completion:nil) 76 | end 77 | 78 | def open_controller(c) 79 | sd = App.shared.delegate 80 | nav = UINavigationController.alloc.initWithRootViewController(c) 81 | sd.revealController.frontViewController = nav 82 | self.revealViewController.revealToggle(nil) 83 | end 84 | 85 | def register_router(r, &callback) 86 | HHRouter.shared.map(r, toBlock:callback) 87 | end 88 | 89 | def find_router(r, dic={}) 90 | b = HHRouter.shared.matchBlock(r) 91 | c = b.call(dic) 92 | c 93 | end 94 | 95 | 96 | end 97 | 98 | class UIImageView 99 | 100 | def set_image_url(url, placeholder_image=nil) 101 | placeholder_image = 'placeholder'.uiimage unless placeholder_image.is_a? UIImage 102 | self.sd_setImageWithURL(url.nsurl, placeholderImage: placeholder_image) 103 | end 104 | 105 | end 106 | 107 | -------------------------------------------------------------------------------- /app/lib/config.rb: -------------------------------------------------------------------------------- 1 | module Config 2 | 3 | APP_DAILY_SOURCE_LIST_URL = "https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/list" 4 | APP_DAILY_SOURCE_BASE_URL = "https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/" 5 | APP_RSS_SOURCE_LIST_URL = "https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/rss_list" 6 | APP_TEN_READ_SOURCE_LIST_URL = "https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/ten_read_list" 7 | 8 | APP_URL = "http://itunes.apple.com/app/id1032943622" 9 | APP_REVIEW_URL = "itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=1032943622" 10 | 11 | 12 | def Config.menu_items 13 | daily_list = Common::object_from_cache(:daily_list) || [] 14 | rss_list = Common::object_from_cache(:rss_list) || [] 15 | ten_read_list = Common::object_from_cache(:ten_read_list) || [] 16 | 17 | daily_list = [].concat daily_list 18 | rss_list = [].concat rss_list 19 | ten_read_list = [].concat ten_read_list 20 | 21 | if daily_list.empty? and rss_list.empty? and rss_list.empty? 22 | daily_list << {url: 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/zhihu.r', name: '知乎日报'} 23 | daily_list << {url: 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/sf.r', name: 'SegmentFault'} 24 | 25 | rss_list << {name: "槽边往事", url: 'http://rss.catcoder.com/rss/26-b1a7b2a5407db7e0fa98f8c6e12f92a4594'} 26 | rss_list << {name: "MacTalk", url: 'http://rss.catcoder.com/rss/1-b1a7b2a5407db7e0fa98f8c6e12f92a4594'} 27 | 28 | ten_read_list << {base_url: 'http://www.jianshu.com', url: 'http://www.jianshu.com/trending/now', selector: 'h4>a', name: '简书'} 29 | ten_read_list << {base_url: 'http://news.dbanotes.net', url: 'http://news.dbanotes.net', selector: '.title>a', name: 'Startup News'} 30 | end 31 | 32 | daily_list = daily_list.map { |m| 33 | JSSource.build m 34 | } 35 | 36 | rss_list = rss_list.map { |r| 37 | RSSSource.build r 38 | } 39 | 40 | ten_read_list = ten_read_list.map { | m | 41 | TRSource.build m 42 | } 43 | 44 | result = [] 45 | result.concat daily_list 46 | result.concat rss_list 47 | result.concat ten_read_list 48 | 49 | result 50 | end 51 | 52 | end 53 | 54 | -------------------------------------------------------------------------------- /app/lib/define.rb.example: -------------------------------------------------------------------------------- 1 | module Define 2 | POCKET_CONSUMER_KEY = "YOUR POCKET KEY" 3 | AVCLOUD_ID = "YOUR AVCLOUD ID" 4 | AVCLOUD_KEY = "YOUR AVCLOUD KEY" 5 | WEIXIN_ID = "YOUR WEIXIN ID" 6 | end 7 | -------------------------------------------------------------------------------- /app/lib/http.rb: -------------------------------------------------------------------------------- 1 | module Http 2 | 3 | def Http.get_string(url, params, &callback) 4 | AFMotion::HTTP.get(url, params=params) do |result| 5 | callback.call(result.body) 6 | end 7 | end 8 | 9 | def Http.get_json(url, params, &callback) 10 | AFMotion::JSON.get(url, params=params) do |result| 11 | r = BW::JSON.parse result.body 12 | callback.call(r) 13 | end 14 | end 15 | 16 | def post_json(url, params, &callback) 17 | AFMotion::JSON.post(url, params=params) do |result| 18 | callback.call(result.object) 19 | end 20 | end 21 | 22 | def Http.post_form(url, params, &callback) 23 | AFMotion::HTTP.post(url, params=params) do |result| 24 | callback.call(result.object) 25 | end 26 | end 27 | 28 | def Http.get_url(url) 29 | u = NSURL.alloc.initWithString(url) 30 | NSString.stringWithContentsOfURL(u) 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /app/lib/pure_layout_motion.rb: -------------------------------------------------------------------------------- 1 | module PureLayoutMotion 2 | end 3 | 4 | class UIView 5 | 6 | LAYOUT_ATTR = {top: ALEdgeTop, bottom: ALEdgeBottom, leading: ALEdgeLeading, trailing: ALEdgeTrailing} 7 | 8 | def self.nw 9 | self.newAutoLayoutView 10 | end 11 | 12 | def pin_to_sueprview_edge(edge) 13 | self.autoPinEdgeToSuperviewEdge(LAYOUT_ATTR[edge]) 14 | end 15 | 16 | def pin_to_superview 17 | self.autoPinEdgesToSuperviewEdgesWithInsets(UIEdgeInsetsZero) 18 | end 19 | 20 | def pin_to_sueprview_edge_with_offset(edge, offset) 21 | self.autoPinEdgeToSuperviewEdge(LAYOUT_ATTR[edge], withInset:offset) 22 | end 23 | 24 | def pin_edge_to_view_edge(my_edge, view, other_edge) 25 | self.autoPinEdge(LAYOUT_ATTR[my_edge], toEdge:LAYOUT_ATTR[other_edge], ofView:view) 26 | end 27 | 28 | def pin_edge_to_view_edge(my_edge, view, other_edge, inset) 29 | self.autoPinEdge(LAYOUT_ATTR[my_edge], toEdge:LAYOUT_ATTR[other_edge], ofView:view, withOffset:inset) 30 | end 31 | 32 | def auto_center 33 | self.autoCenterInSuperview 34 | end 35 | 36 | def center_h_with_view(v) 37 | self.autoConstrainAttribute(ALAttributeHorizontal, toAttribute:ALAttributeHorizontal, ofView:v) 38 | end 39 | 40 | def center_v_with_view(v) 41 | self.autoConstrainAttribute(ALAttributeVertical, toAttribute:ALAttributeVertical, ofView:v) 42 | end 43 | 44 | def pin_edge_to_view_edge_with_offset(my_edge, view, other_edge, offset) 45 | self.autoPinEdge(LAYOUT_ATTR[my_edge], toEdge:LAYOUT_ATTR[other_edge], ofView:view, withOffset:offset) 46 | end 47 | 48 | def pin_size(s) 49 | self.autoSetDimensionsToSize(s) 50 | end 51 | 52 | end 53 | 54 | -------------------------------------------------------------------------------- /app/lib/router.rb: -------------------------------------------------------------------------------- 1 | module Router 2 | 3 | end 4 | -------------------------------------------------------------------------------- /app/model/item.rb: -------------------------------------------------------------------------------- 1 | class Item 2 | attr_accessor :id, :name, :time, :created_date, :excerpt, :tags, :link, :images, :access_date 3 | 4 | def initialize(json={}) 5 | @id = json[:id].to_s 6 | @name = json[:title] || json[:name] 7 | @time = json[:votes] 8 | @excerpt = json[:excerpt] 9 | @tags = json[:tags] 10 | @created_date = json[:createdDate] 11 | @link = json[:link] || json[:url] || json[:original_url] 12 | @link = @link.strip 13 | @images = json[:images] 14 | # 冗余的属性,用来标示history中的访问时间 15 | @access_date = json[:access_date] 16 | end 17 | 18 | def to_json 19 | result = { id: @id, 20 | name: @name, 21 | time: @time, 22 | excerpt: @excerpt, 23 | tags: @tags, 24 | created_date: @created_date, 25 | link: @link, 26 | access_date: NSDate.new.timeIntervalSince1970.to_s, 27 | images: @images} 28 | 29 | BW::JSON.generate(result) 30 | end 31 | 32 | def to_hash 33 | result = { id: @id, 34 | name: @name, 35 | time: @time, 36 | excerpt: @excerpt, 37 | tags: @tags, 38 | created_date: @created_date, 39 | link: @link, 40 | access_date: NSDate.new, 41 | images: @images} 42 | end 43 | end -------------------------------------------------------------------------------- /app/view/basic_cell.rb: -------------------------------------------------------------------------------- 1 | 2 | # 只显示title的cell 3 | class BasicCell < SSBaseTableCell 4 | 5 | def self.cellStyle 6 | UITableViewCellStyleDefault 7 | end 8 | 9 | def model=(m) 10 | self.textLabel.text = m.name 11 | end 12 | 13 | def self.height_for_model(m) 14 | 44 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/Default-568h@2x.png -------------------------------------------------------------------------------- /resources/Default-667h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/Default-667h@2x.png -------------------------------------------------------------------------------- /resources/Default-736h@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/Default-736h@3x.png -------------------------------------------------------------------------------- /resources/Icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/Icon-60.png -------------------------------------------------------------------------------- /resources/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/Icon-60@2x.png -------------------------------------------------------------------------------- /resources/Icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/Icon@3x.png -------------------------------------------------------------------------------- /resources/back@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/back@2x.png -------------------------------------------------------------------------------- /resources/menu@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/menu@2x.png -------------------------------------------------------------------------------- /resources/next@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/next@2x.png -------------------------------------------------------------------------------- /resources/refresh@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shiweifu/OneRead2/c46852981f8b0fbea1ba735f7534372f289d3602/resources/refresh@2x.png -------------------------------------------------------------------------------- /spec/daily_source_spec.rb: -------------------------------------------------------------------------------- 1 | describe '测试日报类源' do 2 | 3 | before do 4 | @list = [] 5 | 6 | url = 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/zhihu.r' 7 | name = '知乎日报' 8 | @js_obj = JSSource.build({url: url, name: name}) 9 | @items = [] 10 | 11 | end 12 | 13 | it 'get daily source list' do 14 | OneStore.daily_source_list do | result | 15 | @list = result 16 | end 17 | 18 | wait 3 do 19 | SpecHelper::sp('@list', @list) 20 | @list.size.should.not.be == 0 21 | (@list.first.instance_of? JSSource).should.be == true 22 | end 23 | end 24 | 25 | it 'daily source load' do 26 | 27 | (@js_obj.instance_of? JSSource).should.be == true 28 | 29 | @js_obj.load_router do | v | 30 | v.should.be == true 31 | end 32 | 33 | wait 1 do 34 | @js_obj.is_loaded.should.be == true 35 | @js_obj.path.should.equal "/list" 36 | url = @js_obj.page_url 0 37 | url.should.not.equal "" 38 | json_str = Http.get_url url 39 | items = @js_obj.items_from_json json_str 40 | (items.empty?).should.not == true 41 | (items.first.instance_of? Item).should.be == true 42 | i = items.first 43 | model_url = @js_obj.url_with_model i 44 | model_url.should.not.equal "" 45 | end 46 | end 47 | 48 | it 'daily dataSource load' do 49 | dds = DataSource.build({ source: @js_obj }) 50 | (dds.instance_of? DataSource).should.be == true 51 | dds.items do | result | 52 | (result.empty?).should.not == true 53 | end 54 | 55 | wait 2 do 56 | 57 | end 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /spec/helpers/extension.rb: -------------------------------------------------------------------------------- 1 | 2 | module SpecHelper 3 | 4 | def SpecHelper.sp(varname, var) 5 | p "-----#{varname}, #{var}, #{var.class}" 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /spec/main_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Application 'OneRead2'" do 2 | before do 3 | @app = UIApplication.sharedApplication 4 | end 5 | 6 | it "has one window" do 7 | @app.windows.size.should == 2 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/net_spec.rb: -------------------------------------------------------------------------------- 1 | describe "网络访问测试" do 2 | 3 | before do 4 | 5 | @string_url = 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/sf.r' 6 | @json_url = 'http://api.segmentfault.com/config/show' 7 | @body = '' 8 | @body_hash = {} 9 | 10 | end 11 | 12 | it 'get string from url' do 13 | 14 | Http::get_string(@string_url, {}) do | result | 15 | @body = result 16 | end 17 | 18 | wait 1 do 19 | @body.should.not == "" 20 | @body.class.should.equal String 21 | end 22 | end 23 | 24 | it 'get json from url' do 25 | 26 | Http::get_json(@json_url, {}) do | result | 27 | @body_hash = result 28 | end 29 | 30 | wait 1 do 31 | @body_hash.class.should.equal Hash 32 | end 33 | 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /spec/router_list_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe '测试源列表' do 3 | 4 | 5 | before do 6 | 7 | @zhihu_url = 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/zhihu.r' 8 | @sf_url = 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/sf.r' 9 | @mono_url = 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/mono.r' 10 | @dgtle_url = 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/dgtle.r' 11 | @js_content = "" 12 | @js_obj = nil 13 | @items = [] 14 | 15 | end 16 | 17 | # it 'SegmentFault' do 18 | # true.should == false 19 | # end 20 | # 21 | # it 'Zhihu' do 22 | # 23 | # true.should == false 24 | # end 25 | 26 | it 'Dgtle' do 27 | @js_obj = JSSource.build({url: @dgtle_url, name: "Dgtle"}) 28 | @is_loaded = false 29 | @js_obj.load_router do | is_loaded | 30 | @is_loaded = is_loaded 31 | end 32 | 33 | wait 3 do 34 | @is_loaded.should == true 35 | p "here" 36 | 37 | @js_obj.items_with_page(1) do | items | 38 | SpecHelper::sp("items", items) 39 | (items.empty?).should.not == true 40 | (items.first.instance_of? Item).should.be == true 41 | items.first.link.should.not.equal "" 42 | items.first.name.should.not.equal "" 43 | items.first.id.should.not.equal "" 44 | @item = items.first 45 | 46 | url = @js_obj.url_with_model(@item) 47 | url.should.not.equal "" 48 | end 49 | 50 | wait 3 do 51 | 52 | end 53 | 54 | end 55 | 56 | 57 | end 58 | 59 | it 'Mono' do 60 | 61 | @js_obj = JSSource.build({url: @mono_url, name: "Mono"}) 62 | @is_loaded = false 63 | @js_obj.load_router do | is_loaded | 64 | @is_loaded = is_loaded 65 | end 66 | 67 | wait 3 do 68 | @is_loaded.should == true 69 | p "here" 70 | 71 | @js_obj.items_with_page(1) do | items | 72 | SpecHelper::sp("items", items) 73 | (items.empty?).should.not == true 74 | (items.first.instance_of? Item).should.be == true 75 | items.first.link.should.not.equal "" 76 | items.first.name.should.not.equal "" 77 | items.first.id.should.not.equal "" 78 | @item = items.first 79 | 80 | url = @js_obj.url_with_model(@item) 81 | url.should.not.equal "" 82 | end 83 | 84 | wait 3 do 85 | 86 | end 87 | 88 | end 89 | 90 | end 91 | end -------------------------------------------------------------------------------- /spec/rss_source_spec.rb: -------------------------------------------------------------------------------- 1 | describe '测试公众号源' do 2 | 3 | before do 4 | @rss_test = 'http://www.hi-pda.com/forum/rss.php?fid=2&auth=614fraJZUi3pNmeDRdhFmKEHcy%2B6rz82xnfs3Rug7G1VjpOnclRfyxSgDR%2B6Ew' 5 | @rss_source = RSSSource.build({name: "hi-pda", url: @rss_test}) 6 | @items = [] 7 | end 8 | 9 | it 'test RSS Source' do 10 | @rss_source.load_router do | b | 11 | end 12 | 13 | @rss_source.items_with_page(1) do | items | 14 | @items = items 15 | end 16 | 17 | wait 3 do 18 | (@items.empty?).should.not == true 19 | (@items.first.instance_of? Item).should.be == true 20 | end 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /spec/tenread_load_spec.rb: -------------------------------------------------------------------------------- 1 | describe '测试加载十阅的源' do 2 | 3 | before do 4 | @list_url = 'https://coding.net/u/shiweifu/p/OneReadRouter/git/raw/master/ten_read_list' 5 | 6 | @hacker_news = 'https://news.ycombinator.com/' 7 | @hacker_news_selector = '.title>a' 8 | 9 | @jianshu_url = 'http://www.jianshu.com/trending/now' 10 | @jianshu_selector = 'h4>a' 11 | @jianshu_base_url = 'http://www.jianshu.com' 12 | end 13 | 14 | it 'load tenread list' do 15 | @body_hash = [] 16 | 17 | Http::get_json(@list_url, {}) do | result | 18 | @body_hash = result 19 | end 20 | 21 | wait 1 do 22 | @body_hash.class.should.equal Array 23 | (@body_hash.empty?).should.not == true 24 | end 25 | 26 | end 27 | 28 | it 'parse html' do 29 | @result = [] 30 | 31 | Http::get_string(@jianshu_url, {}) do | html | 32 | document = HTMLDocument.documentWithString html 33 | list = document.nodesMatchingSelector(@jianshu_selector) 34 | @result = list.map { | r | 35 | {name: r.textContent, link: @jianshu_base_url + r.attributes['href']} 36 | } 37 | end 38 | 39 | wait 5 do 40 | @result.class.should.equal Array 41 | (@result.empty?).should.not == true 42 | p @result.to_s 43 | end 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /spec/tr_source_spec.rb: -------------------------------------------------------------------------------- 1 | describe '测试十阅的源' do 2 | 3 | before do 4 | 5 | @source = TRSource.build({base_url: 'http://www.jianshu.com', 6 | url: 'http://www.jianshu.com/trending/now', 7 | selector: 'h4>a', 8 | name: '简书'}) 9 | end 10 | 11 | it 'test tenread source' do 12 | 13 | @source.load_router do | b | 14 | end 15 | 16 | wait 3 do 17 | @source.items_with_page 0 do | its | 18 | @items = its 19 | end 20 | end 21 | 22 | wait 5 do 23 | @source.is_loaded.should.be == true 24 | (@items.empty?).should.not == true 25 | (@items.first.instance_of? Item).should.be == true 26 | end 27 | end 28 | 29 | end 30 | --------------------------------------------------------------------------------