diff --git a/Gemfile.lock b/Gemfile.lock index 4cd0586f2..f1fec246c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,7 +78,7 @@ GEM multi_json (1.7.3) nokogiri (1.5.9) pg (0.15.1) - pry (0.9.12.1) + pry (0.9.12.2) coderay (~> 1.0.5) method_source (~> 0.8) slop (~> 3.4) @@ -128,7 +128,7 @@ GEM tilt (>= 1.3.0) sinatra-flash (0.3.0) sinatra (>= 1.0.0) - slop (3.4.4) + slop (3.4.5) sqlite3 (1.3.7) thor (0.18.1) thread (0.0.8) diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index e7705c047..6cd49c6bc 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -14,13 +14,20 @@ class Stringer < Sinatra::Base erb :archive end + get "/starred" do + @starred_stories = StoryRepository.starred(params[:page]) + + erb :starred + end + put "/stories/:id" do json_params = JSON.parse(request.body.read, symbolize_names: true) story = StoryRepository.fetch(params[:id]) story.is_read = !!json_params[:is_read] story.keep_unread = !!json_params[:keep_unread] - + story.is_starred = !!json_params[:is_starred] + StoryRepository.save(story) end @@ -29,4 +36,4 @@ class Stringer < Sinatra::Base redirect to("/news") end -end \ No newline at end of file +end diff --git a/app/public/css/styles.css b/app/public/css/styles.css index fe9d4a125..40735f551 100644 --- a/app/public/css/styles.css +++ b/app/public/css/styles.css @@ -179,6 +179,25 @@ li.story.open .story-preview { margin-right: 25px; } +.story-starred-box { + display: inline-block; + float: left; + padding-left: 4px; + width: 20%; +} + +.story-starred { + display: inline-block; + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + margin-right: 25px; +} + .story-body { margin-left: 20px; margin-right: 20px; diff --git a/app/public/js/app.js b/app/public/js/app.js index 7fa5949ed..a33a5788d 100644 --- a/app/public/js/app.js +++ b/app/public/js/app.js @@ -47,6 +47,16 @@ var Story = Backbone.Model.extend({ if (this.shouldSave()) this.save(); }, + toggleStarred: function() { + if (this.get("is_starred")) { + this.set("is_starred", false); + } else { + this.set("is_starred", true); + } + + if (this.shouldSave()) this.save(); + }, + close: function() { this.set("open", false); }, @@ -73,7 +83,8 @@ var StoryView = Backbone.View.extend({ events: { "click .story-preview" : "storyClicked", - "click .story-keep-unread" : "toggleKeepUnread" + "click .story-keep-unread" : "toggleKeepUnread", + "click .story-starred" : "toggleStarred" }, initialize: function() { @@ -83,6 +94,7 @@ var StoryView = Backbone.View.extend({ this.listenTo(this.model, 'change:open', this.itemOpened); this.listenTo(this.model, 'change:is_read', this.itemRead); this.listenTo(this.model, 'change:keep_unread', this.itemKeepUnread); + this.listenTo(this.model, 'change:is_starred', this.itemStarred); }, render: function() { @@ -110,11 +122,16 @@ var StoryView = Backbone.View.extend({ if (!this.$el.visible()) window.scrollTo(0, this.$el.offset().top); }, - itemKeepUnread: function(){ + itemKeepUnread: function() { var icon = this.model.get("keep_unread") ? "icon-check" : "icon-check-empty"; this.$(".story-keep-unread > i").attr("class", icon); }, + itemStarred: function() { + var icon = this.model.get("is_starred") ? "icon-star" : "icon-star-empty"; + this.$(".story-starred > i").attr("class", icon); + }, + storyClicked: function() { this.model.toggle(); window.scrollTo(0, this.$el.offset().top); @@ -122,6 +139,11 @@ var StoryView = Backbone.View.extend({ toggleKeepUnread: function() { this.model.toggleKeepUnread(); + }, + + toggleStarred: function(e) { + e.stopPropagation(); + this.model.toggleStarred(); } }); @@ -203,6 +225,11 @@ var StoryList = Backbone.Collection.extend({ toggleCurrentKeepUnread: function() { if (this.cursorPosition < 0) this.cursorPosition = 0; this.at(this.cursorPosition).toggleKeepUnread(); + }, + + toggleCurrentStarred: function() { + if (this.cursorPosition < 0) this.cursorPosition = 0; + this.at(this.cursorPosition).toggleStarred(); } }); @@ -263,6 +290,10 @@ var AppView = Backbone.View.extend({ toggleCurrentKeepUnread: function() { this.stories.toggleCurrentKeepUnread(); + }, + + toggleCurrentStarred: function() { + this.stories.toggleCurrentStarred(); } }); @@ -287,4 +318,4 @@ $(document).ready(function() { Mousetrap.bind("?", function() { $("#shortcuts").modal('toggle'); }); -}); \ No newline at end of file +}); diff --git a/app/repositories/story_repository.rb b/app/repositories/story_repository.rb index 552ca2ae9..b8e20767a 100644 --- a/app/repositories/story_repository.rb +++ b/app/repositories/story_repository.rb @@ -8,6 +8,7 @@ def self.add(entry, feed) permalink: entry.url, body: extract_content(entry), is_read: false, + is_starred: false, published: entry.published || Time.now) end @@ -31,6 +32,11 @@ def self.read(page = 1) Story.where(is_read: true).order("published desc").page(page).per_page(15) end + def self.starred(page = 1) + Story.where(is_starred: true) + .order("published desc").page(page).per_page(15) + end + def self.read_count Story.where(is_read: true).count end diff --git a/app/views/js/stories.js.erb b/app/views/js/stories.js.erb index 866d4c547..f937cce8a 100644 --- a/app/views/js/stories.js.erb +++ b/app/views/js/stories.js.erb @@ -35,5 +35,9 @@ Mousetrap.bind(["m"], function() { StoryApp.toggleCurrentKeepUnread(); }); + + Mousetrap.bind(["s"], function() { + StoryApp.toggleCurrentStarred(); + }); }); - \ No newline at end of file + diff --git a/app/views/js/templates/_story.js.erb b/app/views/js/templates/_story.js.erb index 56460f982..60d6c482d 100644 --- a/app/views/js/templates/_story.js.erb +++ b/app/views/js/templates/_story.js.erb @@ -1,11 +1,16 @@ \ No newline at end of file + diff --git a/app/views/partials/_action_bar.erb b/app/views/partials/_action_bar.erb index 326fdf89d..800cf2ef5 100644 --- a/app/views/partials/_action_bar.erb +++ b/app/views/partials/_action_bar.erb @@ -11,6 +11,9 @@
+ + + diff --git a/app/views/partials/_feed_action_bar.erb b/app/views/partials/_feed_action_bar.erb index fd8f302c5..f11a3d0c4 100644 --- a/app/views/partials/_feed_action_bar.erb +++ b/app/views/partials/_feed_action_bar.erb @@ -6,6 +6,9 @@
+ + + diff --git a/app/views/partials/_shortcuts.erb b/app/views/partials/_shortcuts.erb index ce896bc46..0cc2e32ed 100644 --- a/app/views/partials/_shortcuts.erb +++ b/app/views/partials/_shortcuts.erb @@ -9,6 +9,7 @@
  • n/p: <%= t('partials.shortcuts.keys.np') %>
  • o <%= t('partials.shortcuts.keys.or') %> enter: <%= t('partials.shortcuts.keys.oenter') %>
  • m: <%= t('partials.shortcuts.keys.m') %>
  • +
  • s: <%= t('partials.shortcuts.keys.s') %>
  • v: <%= t('partials.shortcuts.keys.v') %>
  • shift+a: <%= t('partials.shortcuts.keys.shifta') %>
  • diff --git a/app/views/starred.erb b/app/views/starred.erb new file mode 100644 index 000000000..816f39937 --- /dev/null +++ b/app/views/starred.erb @@ -0,0 +1,30 @@ +
    + <%= render_partial :feed_action_bar %> +
    + +<% unless @starred_stories.empty? %> + <%= render_js :stories, { stories: @starred_stories } %> + +
    + +
    + + +<% else %> +
    +

    <%= t('starred.sorry') %>

    +
    +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 7387899ef..67675905c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4,6 +4,11 @@ en: of: of previous: Previous sorry: Sorry, you haven't read any stories yet! + starred: + next: Next + of: of + previous: Previous + sorry: Sorry, you haven't starred any stories yet! date: abbr_month_names: - @@ -70,6 +75,7 @@ en: mark_all: Mark all as read refresh: Refresh view_feeds: View feeds + starred_stories: Starred stories feed: last_fetched: never: Never @@ -83,10 +89,12 @@ en: archived_stories: Archived stories feeds: View feeds home: Return to Stories + starred_stories: Starred stories shortcuts: keys: jk: Next/previous story m: Mark item as read/unread + s: Mark item as starred/unstarred np: Move down/move up oenter: Toggle story open/closed or: or diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index c02dfa781..980ed2cda 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -4,6 +4,11 @@ zh-CN: of: of previous: 上一页 sorry: 呃,你现在还没有已经读过的故事 + starred: + next: 下一页 + of: of + previous: 上一页 + sorry: 呃,你现在还没有为任何故事加注星标 date: abbr_month_names: - @@ -70,6 +75,7 @@ zh-CN: mark_all: 全部标为已读 refresh: 刷新 view_feeds: 查看订阅列表 + starred_stories: 加星标的故事 feed: last_fetched: never: 尚未成功读取过 @@ -83,10 +89,12 @@ zh-CN: archived_stories: 已读故事 feeds: 查看订阅列表 home: 返回未读故事列表 + starred_stories: 加星标的故事 shortcuts: keys: jk: 下一个/上一个故事 m: 将一个条目标为已读/未读 + s: 为一个条目加注星标/取消星标 np: 向上/向下移动 oenter: 点击打开/关闭 or: 或 diff --git a/db/migrate/20130513044029_add_is_starred_status_for_stories.rb b/db/migrate/20130513044029_add_is_starred_status_for_stories.rb new file mode 100644 index 000000000..63423ab31 --- /dev/null +++ b/db/migrate/20130513044029_add_is_starred_status_for_stories.rb @@ -0,0 +1,5 @@ +class AddIsStarredStatusForStories < ActiveRecord::Migration + def change + add_column :stories, :is_starred, :boolean, default: false + end +end diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index 8e7c36be2..297d17fd3 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -71,6 +71,21 @@ end end + describe "GET /starred" do + let(:starred_one) { StoryFactory.build(is_starred: true) } + let(:starred_two) { StoryFactory.build(is_starred: true) } + let(:stories) { [starred_one, starred_two].paginate } + before { StoryRepository.stub(:starred).and_return(stories) } + + it "displays the list of starred stories with pagination" do + get "/starred" + + page = last_response.body + page.should have_tag("#stories") + page.should have_tag("div#pagination") + end + end + describe "PUT /stories/:id" do before { StoryRepository.stub(:fetch).and_return(story_one) } context "is_read parameter" do @@ -112,6 +127,24 @@ end end end + + context "is_starred parameter" do + context "when it is not malformed" do + it "marks a story as permanently starred" do + put "/stories/#{story_one.id}", {is_starred: true}.to_json + + story_one.is_starred.should eq true + end + end + + context "when it is malformed" do + it "marks a story as permanently starred" do + put "/stories/#{story_one.id}", {is_starred: "malformed"}.to_json + + story_one.is_starred.should eq true + end + end + end end describe "/mark_all_as_read" do @@ -124,4 +157,4 @@ URI::parse(last_response.location).path.should eq "/news" end end -end \ No newline at end of file +end diff --git a/spec/factories/story_factory.rb b/spec/factories/story_factory.rb index d4f7d022a..d294f252d 100644 --- a/spec/factories/story_factory.rb +++ b/spec/factories/story_factory.rb @@ -19,6 +19,7 @@ def self.build(params = {}) body: params[:body] || Faker::Lorem.paragraph, feed: params[:feed] || FeedFactory.build, is_read: params[:is_read] || false, + is_starred: params[:is_starred] || false, published: params[:published] ||Time.now) end end