├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── README.md ├── Rakefile ├── app ├── assets │ ├── fonts │ │ ├── icons.eot │ │ ├── icons.otf │ │ ├── icons.svg │ │ ├── icons.ttf │ │ └── icons.woff │ ├── images │ │ ├── .keep │ │ ├── favicon.ico │ │ ├── loading.gif │ │ ├── missing.jpg │ │ └── night.jpg │ ├── javascripts │ │ ├── application.js │ │ ├── jquery.infinitescroll.js │ │ ├── likeships.js.coffee │ │ ├── notifications.js.coffee │ │ ├── rainyday.js │ │ ├── semantic.js │ │ ├── songs.js.coffee │ │ ├── textcomplete.js │ │ └── ting.js.coffee │ └── stylesheets │ │ ├── animate.css │ │ ├── application.css.scss │ │ ├── comments.css.scss │ │ ├── likeships.css.scss │ │ ├── notifications.css.scss │ │ ├── semantic.css.erb │ │ ├── songs.css.scss │ │ ├── textcomplete.css.scss │ │ ├── ting.css.scss │ │ └── users.css.scss ├── controllers │ ├── application_controller.rb │ ├── comments_controller.rb │ ├── concerns │ │ └── .keep │ ├── likeships_controller.rb │ ├── notifications_controller.rb │ ├── password_resets_controller.rb │ ├── sessions_controller.rb │ ├── songs_controller.rb │ └── users_controller.rb ├── helpers │ ├── application_helper.rb │ ├── comments_helper.rb │ ├── likeships_helper.rb │ ├── notifications_helper.rb │ ├── password_resets_helper.rb │ ├── sessions_helper.rb │ ├── songs_helper.rb │ └── users_helper.rb ├── mailers │ ├── .keep │ └── user_mailer.rb ├── models │ ├── .keep │ ├── comment.rb │ ├── concerns │ │ └── .keep │ ├── likeship.rb │ ├── notification.rb │ ├── song.rb │ └── user.rb ├── uploaders │ └── avatar_uploader.rb └── views │ ├── comments │ ├── _comment.html.slim │ ├── _comments.html.slim │ ├── _form.html.slim │ ├── _recent_comments.html.slim │ ├── create.js.erb │ └── destroy.js.erb │ ├── kaminari │ ├── _gap.html.slim │ ├── _page.html.slim │ └── _paginator.html.slim │ ├── layouts │ ├── _navbar.html.slim │ ├── application.html.slim │ └── users_form.html.slim │ ├── likeships │ ├── _like.html.slim │ ├── _likes.html.slim │ ├── _unlike.html.slim │ ├── create.js.erb │ └── destroy.js.erb │ ├── notifications │ ├── _notifications.html.slim │ ├── clear.js.erb │ ├── destroy.js.erb │ ├── index.html.slim │ └── notification │ │ ├── _comment.html.slim │ │ └── _mention.html.slim │ ├── password_resets │ ├── _form.html.slim │ ├── edit.html.slim │ └── new.html.slim │ ├── sessions │ ├── create.js.erb │ └── new.html.slim │ ├── shared │ ├── _error_messages.html.slim │ └── _flash.html.erb │ ├── songs │ ├── _form.html.slim │ ├── _play_list.html.slim │ ├── _share_buttons.html.slim │ ├── _song.html.slim │ ├── _songs.html.slim │ ├── collect.html.slim │ ├── create.js.erb │ ├── destroy.js.erb │ ├── edit.html.slim │ ├── index.html.slim │ ├── new.html.slim │ ├── show.html.slim │ └── update.js.erb │ ├── user_mailer │ ├── activation_needed_email.html.erb │ ├── activation_needed_email.text.erb │ ├── activation_success_email.html.erb │ ├── activation_success_email.text.erb │ ├── reset_password_email.html.erb │ └── reset_password_email.text.erb │ └── users │ ├── _user_panel.html.slim │ ├── create.js.erb │ ├── edit.html.slim │ ├── favorite_songs.js.erb │ ├── new.html.slim │ ├── recent_comments.js.erb │ ├── show.html.slim │ ├── update.js.erb │ └── user_songs.js.erb ├── bin ├── bundle ├── rails └── rake ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── kaminari_config.rb │ ├── mime_types.rb │ ├── session_store.rb │ ├── sorcery.rb │ └── wrap_parameters.rb ├── locales │ ├── en.yml │ ├── kaminari.en.yml │ ├── kaminari.zh-CN.yml │ └── zh-CN.yml ├── routes.rb └── secrets.yml ├── db ├── migrate │ ├── 20141206123135_sorcery_core.rb │ ├── 20141206123136_sorcery_remember_me.rb │ ├── 20141206123137_sorcery_reset_password.rb │ ├── 20141208045147_create_songs.rb │ ├── 20141212155258_create_comments.rb │ ├── 20141213092401_create_likeships.rb │ ├── 20141214004426_create_notifications.rb │ └── 20141223012634_sorcery_user_activation.rb └── schema.rb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── test ├── controllers │ ├── .keep │ ├── comments_controller_test.rb │ ├── likeships_controller_test.rb │ ├── notifications_controller_test.rb │ ├── password_resets_controller_test.rb │ ├── sessions_controller_test.rb │ ├── songs_controller_test.rb │ └── users_controller_test.rb ├── fixtures │ ├── .keep │ ├── comments.yml │ ├── likeships.yml │ ├── notifications.yml │ ├── songs.yml │ └── users.yml ├── helpers │ ├── .keep │ ├── comments_helper_test.rb │ ├── likeships_helper_test.rb │ ├── notifications_helper_test.rb │ ├── password_resets_helper_test.rb │ ├── sessions_helper_test.rb │ ├── songs_helper_test.rb │ └── users_helper_test.rb ├── integration │ └── .keep ├── mailers │ ├── .keep │ ├── previews │ │ └── user_mailer_preview.rb │ └── user_mailer_test.rb ├── models │ ├── .keep │ ├── comment_test.rb │ ├── likeship_test.rb │ ├── notification_test.rb │ ├── song_test.rb │ └── user_test.rb └── test_helper.rb └── vendor └── assets ├── javascripts └── .keep └── stylesheets └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | capybara-*.html 3 | .rspec 4 | /log 5 | /tmp 6 | /db/*.sqlite3 7 | /db/*.sqlite3-journal 8 | /public/system 9 | /coverage/ 10 | /spec/tmp 11 | **.orig 12 | rerun.txt 13 | pickle-email-*.html 14 | 15 | # TODO Comment out these rules if you are OK with secrets being uploaded to the repo 16 | config/initializers/secret_token.rb 17 | #config/secrets.yml 18 | 19 | ## Environment normalisation: 20 | /.bundle 21 | /vendor/bundle 22 | 23 | # these should all be checked in to normalise the environment: 24 | # Gemfile.lock, .ruby-version, .ruby-gemset 25 | 26 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 27 | .rvmrc 28 | 29 | # if using bower-rails ignore default bower_components path bower.json files 30 | /vendor/assets/bower_components 31 | *.bowerrc 32 | bower.json 33 | 34 | # Ignore upload images 35 | /public/uploads/ 36 | 37 | # Ignore Spring files. 38 | /spring/*.pid 39 | 40 | .DS_Store 41 | 42 | #config/environments/*.rb 43 | 44 | # Automatically precompile assets before pushing to Heroku 45 | public/** -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - "2.0.0" 5 | 6 | bundler_args: --without production 7 | 8 | services: 9 | - memcached -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://ruby.taobao.org' 2 | 3 | 4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 5 | gem 'rails', '4.2' 6 | 7 | # Use Uglifier as compressor for JavaScript assets 8 | gem 'uglifier', '>= 1.3.0' 9 | 10 | # Use CoffeeScript for .js.coffee assets and views 11 | gem 'coffee-rails', '~> 4.0.1' 12 | 13 | # Compass 14 | gem 'sass-rails', '~> 4.0.5' 15 | gem 'compass-rails' 16 | 17 | # Use jquery as the JavaScript library 18 | gem 'jquery-rails' 19 | 20 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks 21 | gem 'turbolinks' 22 | 23 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 24 | gem 'jbuilder', '~> 2.0' 25 | 26 | # bundle exec rake doc:rails generates the API under doc/api. 27 | gem 'sdoc', '~> 0.4.0', group: :doc 28 | 29 | # Font-awesome 30 | gem "font-awesome-rails" 31 | 32 | # Upload avatar 33 | gem 'carrierwave' 34 | gem 'mini_magick' 35 | gem 'remotipart' 36 | 37 | # Pagination 38 | gem 'kaminari' 39 | 40 | # Parse XML data 41 | gem 'nokogiri' 42 | 43 | # Authentication 44 | gem 'sorcery', '0.9.0' 45 | 46 | # Slim 47 | gem 'slim' 48 | 49 | # Memcached 50 | gem 'dalli' 51 | 52 | # i18n 53 | gem 'rails-i18n' 54 | 55 | group :production do 56 | gem 'pg', '0.15.1' 57 | gem 'rails_12factor', '0.0.2' 58 | end 59 | 60 | group :development, :test do 61 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 62 | gem 'byebug' 63 | # Access an IRB console on exception pages or by using <%= console %> in views 64 | gem 'web-console', '~> 2.0.0.beta4' 65 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 66 | gem 'spring' 67 | end 68 | 69 | 70 | group :development do 71 | gem 'sqlite3' 72 | gem "better_errors" 73 | gem 'binding_of_caller' 74 | gem 'meta_request' 75 | gem 'traceroute' 76 | end 77 | 78 | group :test do 79 | gem 'minitest-reporters', '1.0.5' 80 | gem 'guard-minitest', '2.3.1' 81 | end 82 | 83 | # require: false so bcrypt is loaded only when has_secure_password is used. 84 | # This is to avoid ActiveModel (and by extension the entire framework) 85 | # being dependent on a binary library. 86 | gem 'bcrypt', require: false 87 | 88 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://ruby.taobao.org/ 3 | specs: 4 | actionmailer (4.2.0) 5 | actionpack (= 4.2.0) 6 | actionview (= 4.2.0) 7 | activejob (= 4.2.0) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.0) 11 | actionview (= 4.2.0) 12 | activesupport (= 4.2.0) 13 | rack (~> 1.6.0) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 17 | actionview (4.2.0) 18 | activesupport (= 4.2.0) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 23 | activejob (4.2.0) 24 | activesupport (= 4.2.0) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.0) 27 | activesupport (= 4.2.0) 28 | builder (~> 3.1) 29 | activerecord (4.2.0) 30 | activemodel (= 4.2.0) 31 | activesupport (= 4.2.0) 32 | arel (~> 6.0) 33 | activesupport (4.2.0) 34 | i18n (~> 0.7) 35 | json (~> 1.7, >= 1.7.7) 36 | minitest (~> 5.1) 37 | thread_safe (~> 0.3, >= 0.3.4) 38 | tzinfo (~> 1.1) 39 | ansi (1.5.0) 40 | arel (6.0.0) 41 | bcrypt (3.1.10) 42 | better_errors (2.1.1) 43 | coderay (>= 1.0.0) 44 | erubis (>= 2.6.6) 45 | rack (>= 0.9.0) 46 | binding_of_caller (0.7.2) 47 | debug_inspector (>= 0.0.1) 48 | builder (3.2.2) 49 | byebug (4.0.2) 50 | columnize (= 0.9.0) 51 | callsite (0.0.11) 52 | carrierwave (0.10.0) 53 | activemodel (>= 3.2.0) 54 | activesupport (>= 3.2.0) 55 | json (>= 1.7) 56 | mime-types (>= 1.16) 57 | celluloid (0.16.0) 58 | timers (~> 4.0.0) 59 | chunky_png (1.3.4) 60 | coderay (1.1.0) 61 | coffee-rails (4.0.1) 62 | coffee-script (>= 2.2.0) 63 | railties (>= 4.0.0, < 5.0) 64 | coffee-script (2.3.0) 65 | coffee-script-source 66 | execjs 67 | coffee-script-source (1.9.1) 68 | columnize (0.9.0) 69 | compass (0.12.7) 70 | chunky_png (~> 1.2) 71 | fssm (>= 0.2.7) 72 | sass (~> 3.2.19) 73 | compass-rails (2.0.0) 74 | compass (>= 0.12.2) 75 | dalli (2.7.3) 76 | debug_inspector (0.0.2) 77 | erubis (2.7.0) 78 | execjs (2.4.0) 79 | faraday (0.9.1) 80 | multipart-post (>= 1.2, < 3) 81 | ffi (1.9.8) 82 | font-awesome-rails (4.3.0.0) 83 | railties (>= 3.2, < 5.0) 84 | formatador (0.2.5) 85 | fssm (0.2.10) 86 | globalid (0.3.3) 87 | activesupport (>= 4.1.0) 88 | guard (2.12.5) 89 | formatador (>= 0.2.4) 90 | listen (~> 2.7) 91 | lumberjack (~> 1.0) 92 | nenv (~> 0.1) 93 | notiffany (~> 0.0) 94 | pry (>= 0.9.12) 95 | shellany (~> 0.0) 96 | thor (>= 0.18.1) 97 | guard-minitest (2.3.1) 98 | guard (~> 2.0) 99 | minitest (>= 3.0) 100 | hike (1.2.3) 101 | hitimes (1.2.2) 102 | i18n (0.7.0) 103 | jbuilder (2.2.11) 104 | activesupport (>= 3.0.0, < 5) 105 | multi_json (~> 1.2) 106 | jquery-rails (4.0.3) 107 | rails-dom-testing (~> 1.0) 108 | railties (>= 4.2.0) 109 | thor (>= 0.14, < 2.0) 110 | json (1.8.2) 111 | jwt (1.4.1) 112 | kaminari (0.16.3) 113 | actionpack (>= 3.0.0) 114 | activesupport (>= 3.0.0) 115 | listen (2.9.0) 116 | celluloid (>= 0.15.2) 117 | rb-fsevent (>= 0.9.3) 118 | rb-inotify (>= 0.9) 119 | loofah (2.0.1) 120 | nokogiri (>= 1.5.9) 121 | lumberjack (1.0.9) 122 | mail (2.6.3) 123 | mime-types (>= 1.16, < 3) 124 | meta_request (0.3.4) 125 | callsite (~> 0.0, >= 0.0.11) 126 | rack-contrib (~> 1.1) 127 | railties (>= 3.0.0, < 5.0.0) 128 | method_source (0.8.2) 129 | mime-types (2.4.3) 130 | mini_magick (4.1.0) 131 | mini_portile (0.6.2) 132 | minitest (5.5.1) 133 | minitest-reporters (1.0.5) 134 | ansi 135 | builder 136 | minitest (>= 5.0) 137 | ruby-progressbar 138 | multi_json (1.11.0) 139 | multi_xml (0.5.5) 140 | multipart-post (2.0.0) 141 | nenv (0.2.0) 142 | nokogiri (1.6.6.2) 143 | mini_portile (~> 0.6.0) 144 | notiffany (0.0.6) 145 | nenv (~> 0.1) 146 | shellany (~> 0.0) 147 | oauth (0.4.7) 148 | oauth2 (1.0.0) 149 | faraday (>= 0.8, < 0.10) 150 | jwt (~> 1.0) 151 | multi_json (~> 1.3) 152 | multi_xml (~> 0.5) 153 | rack (~> 1.2) 154 | pg (0.15.1) 155 | pry (0.10.1) 156 | coderay (~> 1.1.0) 157 | method_source (~> 0.8.1) 158 | slop (~> 3.4) 159 | rack (1.6.0) 160 | rack-contrib (1.2.0) 161 | rack (>= 0.9.1) 162 | rack-test (0.6.3) 163 | rack (>= 1.0) 164 | rails (4.2.0) 165 | actionmailer (= 4.2.0) 166 | actionpack (= 4.2.0) 167 | actionview (= 4.2.0) 168 | activejob (= 4.2.0) 169 | activemodel (= 4.2.0) 170 | activerecord (= 4.2.0) 171 | activesupport (= 4.2.0) 172 | bundler (>= 1.3.0, < 2.0) 173 | railties (= 4.2.0) 174 | sprockets-rails 175 | rails-deprecated_sanitizer (1.0.3) 176 | activesupport (>= 4.2.0.alpha) 177 | rails-dom-testing (1.0.5) 178 | activesupport (>= 4.2.0.beta, < 5.0) 179 | nokogiri (~> 1.6.0) 180 | rails-deprecated_sanitizer (>= 1.0.1) 181 | rails-html-sanitizer (1.0.2) 182 | loofah (~> 2.0) 183 | rails-i18n (4.0.4) 184 | i18n (~> 0.6) 185 | railties (~> 4.0) 186 | rails_12factor (0.0.2) 187 | rails_serve_static_assets 188 | rails_stdout_logging 189 | rails_serve_static_assets (0.0.4) 190 | rails_stdout_logging (0.0.3) 191 | railties (4.2.0) 192 | actionpack (= 4.2.0) 193 | activesupport (= 4.2.0) 194 | rake (>= 0.8.7) 195 | thor (>= 0.18.1, < 2.0) 196 | rake (10.4.2) 197 | rb-fsevent (0.9.4) 198 | rb-inotify (0.9.5) 199 | ffi (>= 0.5.0) 200 | rdoc (4.2.0) 201 | remotipart (1.2.1) 202 | ruby-progressbar (1.7.1) 203 | sass (3.2.19) 204 | sass-rails (4.0.5) 205 | railties (>= 4.0.0, < 5.0) 206 | sass (~> 3.2.2) 207 | sprockets (~> 2.8, < 3.0) 208 | sprockets-rails (~> 2.0) 209 | sdoc (0.4.1) 210 | json (~> 1.7, >= 1.7.7) 211 | rdoc (~> 4.0) 212 | shellany (0.0.1) 213 | slim (3.0.3) 214 | temple (~> 0.7.3) 215 | tilt (>= 1.3.3, < 2.1) 216 | slop (3.6.0) 217 | sorcery (0.9.0) 218 | bcrypt (~> 3.1) 219 | oauth (~> 0.4, >= 0.4.4) 220 | oauth2 (>= 0.8.0) 221 | spring (1.3.3) 222 | sprockets (2.12.3) 223 | hike (~> 1.2) 224 | multi_json (~> 1.0) 225 | rack (~> 1.0) 226 | tilt (~> 1.1, != 1.3.0) 227 | sprockets-rails (2.2.4) 228 | actionpack (>= 3.0) 229 | activesupport (>= 3.0) 230 | sprockets (>= 2.8, < 4.0) 231 | sqlite3 (1.3.10) 232 | temple (0.7.5) 233 | thor (0.19.1) 234 | thread_safe (0.3.5) 235 | tilt (1.4.1) 236 | timers (4.0.1) 237 | hitimes 238 | traceroute (0.4.0) 239 | rails (>= 3.0.0) 240 | turbolinks (2.5.3) 241 | coffee-rails 242 | tzinfo (1.2.2) 243 | thread_safe (~> 0.1) 244 | uglifier (2.7.1) 245 | execjs (>= 0.3.0) 246 | json (>= 1.8.0) 247 | web-console (2.0.0) 248 | activemodel (~> 4.0) 249 | binding_of_caller (>= 0.7.2) 250 | railties (~> 4.0) 251 | sprockets-rails (>= 2.0, < 4.0) 252 | 253 | PLATFORMS 254 | ruby 255 | 256 | DEPENDENCIES 257 | bcrypt 258 | better_errors 259 | binding_of_caller 260 | byebug 261 | carrierwave 262 | coffee-rails (~> 4.0.1) 263 | compass-rails 264 | dalli 265 | font-awesome-rails 266 | guard-minitest (= 2.3.1) 267 | jbuilder (~> 2.0) 268 | jquery-rails 269 | kaminari 270 | meta_request 271 | mini_magick 272 | minitest-reporters (= 1.0.5) 273 | nokogiri 274 | pg (= 0.15.1) 275 | rails (= 4.2) 276 | rails-i18n 277 | rails_12factor (= 0.0.2) 278 | remotipart 279 | sass-rails (~> 4.0.5) 280 | sdoc (~> 0.4.0) 281 | slim 282 | sorcery (= 0.9.0) 283 | spring 284 | sqlite3 285 | traceroute 286 | turbolinks 287 | uglifier (>= 1.3.0) 288 | web-console (~> 2.0.0.beta4) 289 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # Defines the matching rules for Guard. 2 | guard :minitest, spring: true, all_on_start: false do 3 | watch(%r{^test/(.*)/?(.*)_test\.rb$}) 4 | watch('test/test_helper.rb') { 'test' } 5 | watch('config/routes.rb') { integration_tests } 6 | watch(%r{^app/models/(.*?)\.rb$}) do |matches| 7 | "test/models/#{matches[1]}_test.rb" 8 | end 9 | watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches| 10 | resource_tests(matches[1]) 11 | end 12 | watch(%r{^app/views/([^/]*?)/.*\.html\.erb$}) do |matches| 13 | ["test/controllers/#{matches[1]}_controller_test.rb"] + 14 | integration_tests(matches[1]) 15 | end 16 | watch(%r{^app/helpers/(.*?)_helper\.rb$}) do |matches| 17 | integration_tests(matches[1]) 18 | end 19 | watch('app/views/layouts/application.html.erb') do 20 | 'test/integration/site_layout_test.rb' 21 | end 22 | watch('app/helpers/sessions_helper.rb') do 23 | integration_tests << 'test/helpers/sessions_helper_test.rb' 24 | end 25 | watch('app/controllers/sessions_controller.rb') do 26 | ['test/controllers/sessions_controller_test.rb', 27 | 'test/integration/users_login_test.rb'] 28 | end 29 | watch('app/controllers/account_activations_controller.rb') do 30 | 'test/integration/users_signup_test.rb' 31 | end 32 | watch(%r{app/views/users/*}) do 33 | resource_tests('users') + 34 | ['test/integration/microposts_interface_test.rb'] 35 | end 36 | end 37 | 38 | # Returns the integration tests corresponding to the given resource. 39 | def integration_tests(resource = :all) 40 | if resource == :all 41 | Dir["test/integration/*"] 42 | else 43 | Dir["test/integration/#{resource}_*.rb"] 44 | end 45 | end 46 | 47 | # Returns the controller tests corresponding to the given resource. 48 | def controller_test(resource) 49 | "test/controllers/#{resource}_controller_test.rb" 50 | end 51 | 52 | # Returns all tests for the given resource. 53 | def resource_tests(resource) 54 | integration_tests(resource) << controller_test(resource) 55 | end 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ting 2 | 3 | [![Build Status](https://travis-ci.org/Aufree/ting.svg?branch=master)](https://travis-ci.org/Aufree/ting) 4 | 5 | Ting is a music social networking project written in Ruby on Rails and Semantic-UI. 6 | 7 | You can check out the demo at [this link](http://tinger.herokuapp.com). 8 | 9 | **You can ping me on [Twitter](https://twitter.com/_Paul_King_) or follow me on [Weibo](http://weibo.com/jinfali).** 10 | 11 | ## Screen Shots 12 | 13 | ![](http://ww1.sinaimg.cn/large/76dc7f1bgw1ent5zzgeyvj21kw11416d.jpg) 14 | 15 | ![](http://ww1.sinaimg.cn/large/76dc7f1bgw1ent5xvakuyj21kw114qce.jpg) 16 | 17 | ![](http://ww4.sinaimg.cn/large/76dc7f1bgw1ent5z2xqrnj21kw114n2c.jpg) 18 | 19 | ![](http://ww4.sinaimg.cn/large/76dc7f1bgw1ent60hhp39j21kw11443g.jpg) 20 | 21 | ![](http://ww1.sinaimg.cn/large/76dc7f1bgw1ent60ssvxgj21kw1147aa.jpg) 22 | 23 | ## Requirements 24 | 25 | Ruby 2.0.0 + 26 | Memcached 1.4 + 27 | ImageMagick 6.8 + 28 | 29 | 30 | ## Installation 31 | 32 | $ git clone git://github.com/Aufree/ting.git 33 | $ cd ting 34 | 35 | ### Linux(Ubuntu) 36 | 37 | $ sudo apt-get update 38 | $ sudo apt-get install memcached imagemagick 39 | 40 | ### Mac OS 41 | 42 | $ brew install memcached && brew install imagemagick 43 | 44 | ## Run 45 | 46 | $ bundle install 47 | $ rake db:migrate 48 | # ensure that memcached has started up 49 | $ rails s 50 | 51 | ## Testing 52 | 53 | $ rake test 54 | 55 | ## License 56 | 57 | Copyright (c) 2015 Paul King 58 | 59 | --------------- 60 | 61 | Released under the MIT license: 62 | 63 | * [www.opensource.org/licenses/MIT](http://www.opensource.org/licenses/MIT) 64 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/assets/fonts/icons.eot -------------------------------------------------------------------------------- /app/assets/fonts/icons.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/assets/fonts/icons.otf -------------------------------------------------------------------------------- /app/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /app/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/assets/fonts/icons.woff -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/assets/images/loading.gif -------------------------------------------------------------------------------- /app/assets/images/missing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/assets/images/missing.jpg -------------------------------------------------------------------------------- /app/assets/images/night.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/assets/images/night.jpg -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | //= require jquery.remotipart 4 | //= require turbolinks 5 | //= require jquery.infinitescroll 6 | //= require_tree . 7 | 8 | Turbolinks.enableProgressBar(); -------------------------------------------------------------------------------- /app/assets/javascripts/jquery.infinitescroll.js: -------------------------------------------------------------------------------- 1 | /*! 2 | -------------------------------- 3 | Infinite Scroll 4 | -------------------------------- 5 | + https://github.com/paulirish/infinite-scroll 6 | + version 2.1.0 7 | + Copyright 2011/12 Paul Irish & Luke Shumard 8 | + Licensed under the MIT license 9 | 10 | + Documentation: http://infinite-scroll.com/ 11 | */ 12 | ;(function(e){if(typeof define==="function"&&define.amd){define(["jquery"],e)}else{e(jQuery)}})(function(e,t){"use strict";e.infinitescroll=function(n,r,i){this.element=e(i);if(!this._create(n,r)){this.failed=true}};e.infinitescroll.defaults={loading:{finished:t,finishedMsg:"Congratulations, you've reached the end of the internet.",img:"data:image/gif;base64,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",msg:null,msgText:"Loading the next set of posts...",selector:null,speed:"fast",start:t},state:{isDuringAjax:false,isInvalidPage:false,isDestroyed:false,isDone:false,isPaused:false,isBeyondMaxPage:false,currPage:1},debug:false,behavior:t,binder:e(window),nextSelector:"div.navigation a:first",navSelector:"div.navigation",contentSelector:null,extraScrollPx:150,itemSelector:"div.post",animate:false,pathParse:t,dataType:"html",appendCallback:true,bufferPx:40,errorCallback:function(){},infid:0,pixelsFromNavToBottom:t,path:t,prefill:false,maxPage:t};e.infinitescroll.prototype={_binding:function(n){var r=this,i=r.options;i.v="2.0b2.120520";if(!!i.behavior&&this["_binding_"+i.behavior]!==t){this["_binding_"+i.behavior].call(this);return}if(n!=="bind"&&n!=="unbind"){this._debug("Binding value "+n+" not valid");return false}if(n==="unbind"){this.options.binder.unbind("smartscroll.infscr."+r.options.infid)}else{this.options.binder[n]("smartscroll.infscr."+r.options.infid,function(){r.scroll()})}this._debug("Binding",n)},_create:function(r,i){var s=e.extend(true,{},e.infinitescroll.defaults,r);this.options=s;var o=e(window);var u=this;if(!u._validate(r)){return false}var a=e(s.nextSelector).attr("href");if(!a){this._debug("Navigation selector not found");return false}s.path=s.path||this._determinepath(a);s.contentSelector=s.contentSelector||this.element;s.loading.selector=s.loading.selector||s.contentSelector;s.loading.msg=s.loading.msg||e('
Loading...
'+s.loading.msgText+"
");(new Image).src=s.loading.img;if(s.pixelsFromNavToBottom===t){s.pixelsFromNavToBottom=e(document).height()-e(s.navSelector).offset().top;this._debug("pixelsFromNavToBottom: "+s.pixelsFromNavToBottom)}var f=this;s.loading.start=s.loading.start||function(){e(s.navSelector).hide();s.loading.msg.appendTo(s.loading.selector).show(s.loading.speed,e.proxy(function(){this.beginAjax(s)},f))};s.loading.finished=s.loading.finished||function(){if(!s.state.isBeyondMaxPage)s.loading.msg.fadeOut(s.loading.speed)};s.callback=function(n,r,u){if(!!s.behavior&&n["_callback_"+s.behavior]!==t){n["_callback_"+s.behavior].call(e(s.contentSelector)[0],r,u)}if(i){i.call(e(s.contentSelector)[0],r,s,u)}if(s.prefill){o.bind("resize.infinite-scroll",n._prefill)}};if(r.debug){if(Function.prototype.bind&&(typeof console==="object"||typeof console==="function")&&typeof console.log==="object"){["log","info","warn","error","assert","dir","clear","profile","profileEnd"].forEach(function(e){console[e]=this.call(console[e],console)},Function.prototype.bind)}}this._setup();if(s.prefill){this._prefill()}return true},_prefill:function(){function i(){return e(n.options.contentSelector).height()<=r.height()}var n=this;var r=e(window);this._prefill=function(){if(i()){n.scroll()}r.bind("resize.infinite-scroll",function(){if(i()){r.unbind("resize.infinite-scroll");n.scroll()}})};this._prefill()},_debug:function(){if(true!==this.options.debug){return}if(typeof console!=="undefined"&&typeof console.log==="function"){if(Array.prototype.slice.call(arguments).length===1&&typeof Array.prototype.slice.call(arguments)[0]==="string"){console.log(Array.prototype.slice.call(arguments).toString())}else{console.log(Array.prototype.slice.call(arguments))}}else if(!Function.prototype.bind&&typeof console!=="undefined"&&typeof console.log==="object"){Function.prototype.call.call(console.log,console,Array.prototype.slice.call(arguments))}},_determinepath:function(n){var r=this.options;if(!!r.behavior&&this["_determinepath_"+r.behavior]!==t){return this["_determinepath_"+r.behavior].call(this,n)}if(!!r.pathParse){this._debug("pathParse manual");return r.pathParse(n,this.options.state.currPage+1)}else if(n.match(/^(.*?)\b2\b(.*?$)/)){n=n.match(/^(.*?)\b2\b(.*?$)/).slice(1)}else if(n.match(/^(.*?)2(.*?$)/)){if(n.match(/^(.*?page=)2(\/.*|$)/)){n=n.match(/^(.*?page=)2(\/.*|$)/).slice(1);return n}n=n.match(/^(.*?)2(.*?$)/).slice(1)}else{if(n.match(/^(.*?page=)1(\/.*|$)/)){n=n.match(/^(.*?page=)1(\/.*|$)/).slice(1);return n}else{this._debug("Sorry, we couldn't parse your Next (Previous Posts) URL. Verify your the css selector points to the correct A tag. If you still get this error: yell, scream, and kindly ask for help at infinite-scroll.com.");r.state.isInvalidPage=true}}this._debug("determinePath",n);return n},_error:function(n){var r=this.options;if(!!r.behavior&&this["_error_"+r.behavior]!==t){this["_error_"+r.behavior].call(this,n);return}if(n!=="destroy"&&n!=="end"){n="unknown"}this._debug("Error",n);if(n==="end"||r.state.isBeyondMaxPage){this._showdonemsg()}r.state.isDone=true;r.state.currPage=1;r.state.isPaused=false;r.state.isBeyondMaxPage=false;this._binding("unbind")},_loadcallback:function(r,i,s){var o=this.options,u=this.options.callback,a=o.state.isDone?"done":!o.appendCallback?"no-append":"append",f;if(!!o.behavior&&this["_loadcallback_"+o.behavior]!==t){this["_loadcallback_"+o.behavior].call(this,r,i);return}switch(a){case"done":this._showdonemsg();return false;case"no-append":if(o.dataType==="html"){i="
"+i+"
";i=e(i).find(o.itemSelector)}if(i.length===0){return this._error("end")}break;case"append":var l=r.children();if(l.length===0){return this._error("end")}f=document.createDocumentFragment();while(r[0].firstChild){f.appendChild(r[0].firstChild)}this._debug("contentSelector",e(o.contentSelector)[0]);e(o.contentSelector)[0].appendChild(f);i=l.get();break}o.loading.finished.call(e(o.contentSelector)[0],o);if(o.animate){var c=e(window).scrollTop()+e(o.loading.msg).height()+o.extraScrollPx+"px";e("html,body").animate({scrollTop:c},800,function(){o.state.isDuringAjax=false})}if(!o.animate){o.state.isDuringAjax=false}u(this,i,s);if(o.prefill){this._prefill()}},_nearbottom:function(){var r=this.options,i=0+e(document).height()-r.binder.scrollTop()-e(window).height();if(!!r.behavior&&this["_nearbottom_"+r.behavior]!==t){return this["_nearbottom_"+r.behavior].call(this)}this._debug("math:",i,r.pixelsFromNavToBottom);return i-r.bufferPx-1&&e(n[r]).length===0){this._debug("Your "+r+" found no elements.");return false}}return true},bind:function(){this._binding("bind")},destroy:function(){this.options.state.isDestroyed=true;this.options.loading.finished();return this._error("destroy")},pause:function(){this._pausing("pause")},resume:function(){this._pausing("resume")},beginAjax:function(r){var i=this,s=r.path,o,u,a,f;r.state.currPage++;if(r.maxPage!==t&&r.state.currPage>r.maxPage){r.state.isBeyondMaxPage=true;this.destroy();return}o=e(r.contentSelector).is("table, tbody")?e(""):e("
");u=typeof s==="function"?s(r.state.currPage):s.join(r.state.currPage);i._debug("heading into ajax",u);a=r.dataType==="html"||r.dataType==="json"?r.dataType:"html+callback";if(r.appendCallback&&r.dataType==="html"){a+="+callback"}switch(a){case"html+callback":i._debug("Using HTML via .load() method");o.load(u+" "+r.itemSelector,t,function(t){i._loadcallback(o,t,u)});break;case"html":i._debug("Using "+a.toUpperCase()+" via $.ajax() method");e.ajax({url:u,dataType:r.dataType,complete:function(t,n){f=typeof t.isResolved!=="undefined"?t.isResolved():n==="success"||n==="notmodified";if(f){i._loadcallback(o,t.responseText,u)}else{i._error("end")}}});break;case"json":i._debug("Using "+a.toUpperCase()+" via $.ajax() method");e.ajax({dataType:"json",type:"GET",url:u,success:function(e,n,s){f=typeof s.isResolved!=="undefined"?s.isResolved():n==="success"||n==="notmodified";if(r.appendCallback){if(r.template!==t){var a=r.template(e);o.append(a);if(f){i._loadcallback(o,a)}else{i._error("end")}}else{i._debug("template must be defined.");i._error("end")}}else{if(f){i._loadcallback(o,e,u)}else{i._error("end")}}},error:function(){i._debug("JSON ajax request failed.");i._error("end")}});break}},retrieve:function(r){r=r||null;var i=this,s=i.options;if(!!s.behavior&&this["retrieve_"+s.behavior]!==t){this["retrieve_"+s.behavior].call(this,r);return}if(s.state.isDestroyed){this._debug("Instance is destroyed");return false}s.state.isDuringAjax=true;s.loading.start.call(e(s.contentSelector)[0],s)},scroll:function(){var n=this.options,r=n.state;if(!!n.behavior&&this["scroll_"+n.behavior]!==t){this["scroll_"+n.behavior].call(this);return}if(r.isDuringAjax||r.isInvalidPage||r.isDone||r.isDestroyed||r.isPaused){return}if(!this._nearbottom()){return}this.retrieve()},toggle:function(){this._pausing()},unbind:function(){this._binding("unbind")},update:function(n){if(e.isPlainObject(n)){this.options=e.extend(true,this.options,n)}}};e.fn.infinitescroll=function(n,r){var i=typeof n;switch(i){case"string":var s=Array.prototype.slice.call(arguments,1);this.each(function(){var t=e.data(this,"infinitescroll");if(!t){return false}if(!e.isFunction(t[n])||n.charAt(0)==="_"){return false}t[n].apply(t,s)});break;case"object":this.each(function(){var t=e.data(this,"infinitescroll");if(t){t.update(n)}else{t=new e.infinitescroll(n,r,this);if(!t.failed){e.data(this,"infinitescroll",t)}}});break}return this};var n=e.event,r;n.special.smartscroll={setup:function(){e(this).bind("scroll",n.special.smartscroll.handler)},teardown:function(){e(this).unbind("scroll",n.special.smartscroll.handler)},handler:function(t,n){var i=this,s=arguments;t.type="smartscroll";if(r){clearTimeout(r)}r=setTimeout(function(){e(i).trigger("smartscroll",s)},n==="execAsap"?0:100)}};e.fn.smartscroll=function(e){return e?this.bind("smartscroll",e):this.trigger("smartscroll",["execAsap"])}}); -------------------------------------------------------------------------------- /app/assets/javascripts/likeships.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/notifications.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/songs.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/textcomplete.js: -------------------------------------------------------------------------------- 1 | /*! jquery-textcomplete - v0.3.6 - 2014-12-11 */if("undefined"==typeof jQuery)throw new Error("jQuery.textcomplete requires jQuery");+function(a){"use strict";var b=function(a){console.warn&&console.warn(a)};a.fn.textcomplete=function(c,d){var e=Array.prototype.slice.call(arguments);return this.each(function(){var f=a(this),g=f.data("textComplete");if(g||(g=new a.fn.textcomplete.Completer(this,d||{}),f.data("textComplete",g)),"string"==typeof c){if(!g)return;e.shift(),g[c].apply(g,e),"destroy"===c&&f.removeData("textComplete")}else a.each(c,function(c){a.each(["header","footer","placement","maxCount"],function(a){c[a]&&(g.option[a]=c[a],b(a+"as a strategy param is deprecated. Use option."),delete c[a])})}),g.register(a.fn.textcomplete.Strategy.parse(c))})}}(jQuery),+function(a){"use strict";function b(c,d){if(this.$el=a(c),this.id="textcomplete"+e++,this.strategies=[],this.views=[],this.option=a.extend({},b._getDefaults(),d),!this.$el.is("textarea")&&!c.isContentEditable&&"true"!=c.contentEditable)throw new Error("textcomplete must be called on a Textarea or a ContentEditable.");if(c===document.activeElement)this.initialize();else{var f=this;this.$el.one("focus."+this.id,function(){f.initialize()})}}var c=function(a){var b,c;return function(){var d=Array.prototype.slice.call(arguments);if(b)return c=d,void 0;b=!0;var e=this;d.unshift(function f(){if(c){var d=c;c=void 0,d.unshift(f),a.apply(e,d)}else b=!1}),a.apply(this,d)}},d=function(a){return"[object String]"===Object.prototype.toString.call(a)},e=0;b._getDefaults=function(){return b.DEFAULTS||(b.DEFAULTS={appendTo:a("body"),zIndex:"100"}),b.DEFAULTS},a.extend(b.prototype,{id:null,option:null,strategies:null,adapter:null,dropdown:null,$el:null,initialize:function(){var b=this.$el.get(0);this.dropdown=new a.fn.textcomplete.Dropdown(b,this,this.option);var c,d;this.option.adapter?c=this.option.adapter:(d=this.$el.is("textarea")?"number"==typeof b.selectionEnd?"Textarea":"IETextarea":"ContentEditable",c=a.fn.textcomplete[d]),this.adapter=new c(b,this,this.option)},destroy:function(){this.$el.off("."+this.id),this.adapter&&this.adapter.destroy(),this.dropdown&&this.dropdown.destroy(),this.$el=this.adapter=this.dropdown=null},trigger:function(a,b){this.dropdown||this.initialize(),null!=a||(a=this.adapter.getTextFromHeadToCaret());var c=this._extractSearchQuery(a);if(c.length){var d=c[1];if(b&&this._term===d)return;this._term=d,this._search.apply(this,c)}else this._term=null,this.dropdown.deactivate()},fire:function(a){var b=Array.prototype.slice.call(arguments,1);return this.$el.trigger(a,b),this},register:function(a){Array.prototype.push.apply(this.strategies,a)},select:function(a,b){this.adapter.select(a,b),this.fire("change").fire("textComplete:select",a,b),this.adapter.focus()},_clearAtNext:!0,_term:null,_extractSearchQuery:function(a){for(var b=0;b').css({display:"none",left:0,position:"absolute",zIndex:b.zIndex}).appendTo(c)),d}}),a.extend(b.prototype,{$el:null,$inputEl:null,completer:null,footer:null,header:null,id:null,maxCount:10,placement:"",shown:!1,data:[],className:"",destroy:function(){this.deactivate(),this.$el.off("."+this.id),this.$inputEl.off("."+this.id),this.clear(),this.$el=this.$inputEl=this.completer=null,delete d[this.id]},render:function(b){var c=this._buildContents(b),d=a.map(this.data,function(a){return a.value});this.data.length?(this._renderHeader(d),this._renderFooter(d),c&&(this._renderContents(c),this._activateIndexedItem()),this._setScroll()):this.shown&&this.deactivate()},setPosition:function(a){return this.$el.css(this._applyPlacement(a)),this},clear:function(){this.$el.html(""),this.data=[],this._index=0,this._$header=this._$footer=null},activate:function(){return this.shown||(this.clear(),this.$el.show(),this.className&&this.$el.addClass(this.className),this.completer.fire("textComplete:show"),this.shown=!0),this},deactivate:function(){return this.shown&&(this.$el.hide(),this.className&&this.$el.removeClass(this.className),this.completer.fire("textComplete:hide"),this.shown=!1),this},isUp:function(a){return 38===a.keyCode||a.ctrlKey&&80===a.keyCode},isDown:function(a){return 40===a.keyCode||a.ctrlKey&&78===a.keyCode},isEnter:function(a){var b=a.ctrlKey||a.altKey||a.metaKey||a.shiftKey;return!b&&(13===a.keyCode||9===a.keyCode||this.option.completeOnSpace===!0&&32===a.keyCode)},isPageup:function(a){return 33===a.keyCode},isPagedown:function(a){return 34===a.keyCode},_data:null,_index:null,_$header:null,_$footer:null,_bindEvents:function(){this.$el.on("mousedown."+this.id,".textcomplete-item",a.proxy(this._onClick,this)),this.$el.on("mouseover."+this.id,".textcomplete-item",a.proxy(this._onMouseover,this)),this.$inputEl.on("keydown."+this.id,a.proxy(this._onKeydown,this))},_onClick:function(b){var c=a(b.target);b.preventDefault(),b.originalEvent.keepTextCompleteDropdown=this.id,c.hasClass("textcomplete-item")||(c=c.closest(".textcomplete-item"));var d=this.data[parseInt(c.data("index"),10)];this.completer.select(d.value,d.strategy);var e=this;setTimeout(function(){e.deactivate()},0)},_onMouseover:function(b){var c=a(b.target);b.preventDefault(),c.hasClass("textcomplete-item")||(c=c.closest(".textcomplete-item")),this._index=parseInt(c.data("index"),10),this._activateIndexedItem()},_onKeydown:function(a){this.shown&&(this.isUp(a)?(a.preventDefault(),this._up()):this.isDown(a)?(a.preventDefault(),this._down()):this.isEnter(a)?(a.preventDefault(),this._enter()):this.isPageup(a)?(a.preventDefault(),this._pageup()):this.isPagedown(a)&&(a.preventDefault(),this._pagedown()))},_up:function(){0===this._index?this._index=this.data.length-1:this._index-=1,this._activateIndexedItem(),this._setScroll()},_down:function(){this._index===this.data.length-1?this._index=0:this._index+=1,this._activateIndexedItem(),this._setScroll()},_enter:function(){var a=this.data[parseInt(this._getActiveElement().data("index"),10)];this.completer.select(a.value,a.strategy),this._setScroll()},_pageup:function(){var b=0,c=this._getActiveElement().position().top-this.$el.innerHeight();this.$el.children().each(function(d){return a(this).position().top+a(this).outerHeight()>c?(b=d,!1):void 0}),this._index=b,this._activateIndexedItem(),this._setScroll()},_pagedown:function(){var b=this.data.length-1,c=this._getActiveElement().position().top+this.$el.innerHeight();this.$el.children().each(function(d){return a(this).position().top>c?(b=d,!1):void 0}),this._index=b,this._activateIndexedItem(),this._setScroll()},_activateIndexedItem:function(){this.$el.find(".textcomplete-item.active").removeClass("active"),this._getActiveElement().addClass("active")},_getActiveElement:function(){return this.$el.children(".textcomplete-item:nth("+this._index+")")},_setScroll:function(){var a=this._getActiveElement(),b=a.position().top,c=a.outerHeight(),d=this.$el.innerHeight(),e=this.$el.scrollTop();0===this._index||this._index==this.data.length-1||0>b?this.$el.scrollTop(b+e):b+c>d&&this.$el.scrollTop(b+c+e-d)},_buildContents:function(a){var b,d,e,f="";for(d=0;d',f+=b.strategy.template(b.value),f+="");return f},_renderHeader:function(b){if(this.header){this._$header||(this._$header=a('
  • ').prependTo(this.$el));var c=a.isFunction(this.header)?this.header(b):this.header;this._$header.html(c)}},_renderFooter:function(b){if(this.footer){this._$footer||(this._$footer=a('').appendTo(this.$el));var c=a.isFunction(this.footer)?this.footer(b):this.footer;this._$footer.html(c)}},_renderContents:function(a){this._$footer?this._$footer.before(a):this.$el.append(a)},_applyPlacement:function(a){return-1!==this.placement.indexOf("top")?a={top:"auto",bottom:this.$el.parent().height()-a.top+a.lineHeight,left:a.left}:(a.bottom="auto",delete a.lineHeight),-1!==this.placement.indexOf("absleft")?a.left=0:-1!==this.placement.indexOf("absright")&&(a.right=0,a.left="auto"),a}}),a.fn.textcomplete.Dropdown=b}(jQuery),+function(a){"use strict";function b(b){a.extend(this,b),this.cache&&(this.search=c(this.search))}var c=function(a){var b={};return function(c,d){b[c]?d(b[c]):a.call(this,c,function(a){b[c]=(b[c]||[]).concat(a),d.apply(null,arguments)})}};b.parse=function(c){return a.map(c,function(a){return new b(a)})},a.extend(b.prototype,{match:null,replace:null,search:null,cache:!1,context:function(){return!0},index:2,template:function(a){return a},idProperty:null}),a.fn.textcomplete.Strategy=b}(jQuery),+function(a){"use strict";function b(){}var c=Date.now||function(){return(new Date).getTime()},d=function(a,b){var d,e,f,g,h,i=function(){var j=c()-g;b>j?d=setTimeout(i,b-j):(d=null,h=a.apply(f,e),f=e=null)};return function(){return f=this,e=arguments,g=c(),d||(d=setTimeout(i,b)),h}};a.extend(b.prototype,{id:null,completer:null,el:null,$el:null,option:null,initialize:function(b,c,e){this.el=b,this.$el=a(b),this.id=c.id+this.constructor.name,this.completer=c,this.option=e,this.option.debounce&&(this._onKeyup=d(this._onKeyup,this.option.debounce)),this._bindEvents()},destroy:function(){this.$el.off("."+this.id),this.$el=this.el=this.completer=null},select:function(){throw new Error("Not implemented")},getCaretPosition:function(){var a=this._getCaretRelativePosition(),b=this.$el.offset();return a.top+=b.top,a.left+=b.left,a},focus:function(){this.$el.focus()},_bindEvents:function(){this.$el.on("keyup."+this.id,a.proxy(this._onKeyup,this))},_onKeyup:function(a){this._skipSearch(a)||this.completer.trigger(this.getTextFromHeadToCaret(),!0)},_skipSearch:function(a){switch(a.keyCode){case 40:case 38:return!0}if(a.ctrlKey)switch(a.keyCode){case 78:case 80:return!0}}}),a.fn.textcomplete.Adapter=b}(jQuery),+function(a){"use strict";function b(a,b,c){this.initialize(a,b,c)}b.DIV_PROPERTIES={left:-9999,position:"absolute",top:0,whiteSpace:"pre-wrap"},b.COPY_PROPERTIES=["border-width","font-family","font-size","font-style","font-variant","font-weight","height","letter-spacing","word-spacing","line-height","text-decoration","text-align","width","padding-top","padding-right","padding-bottom","padding-left","margin-top","margin-right","margin-bottom","margin-left","border-style","box-sizing","tab-size"],a.extend(b.prototype,a.fn.textcomplete.Adapter.prototype,{select:function(b,c){var d=this.getTextFromHeadToCaret(),e=this.el.value.substring(this.el.selectionEnd),f=c.replace(b);a.isArray(f)&&(e=f[1]+e,f=f[0]),d=d.replace(c.match,f),this.$el.val(d+e),this.el.selectionStart=this.el.selectionEnd=d.length},_getCaretRelativePosition:function(){var b=a("
    ").css(this._copyCss()).text(this.getTextFromHeadToCaret()),c=a("").text(".").appendTo(b);this.$el.before(b);var d=c.position();return d.top+=c.height()-this.$el.scrollTop(),d.lineHeight=c.height(),b.remove(),d},_copyCss:function(){return a.extend({overflow:this.el.scrollHeight>this.el.offsetHeight?"scroll":"auto"},b.DIV_PROPERTIES,this._getStyles())},_getStyles:function(a){var c=a("
    ").css(["color"]).color;return"undefined"!=typeof c?function(){return this.$el.css(b.COPY_PROPERTIES)}:function(){var c=this.$el,d={};return a.each(b.COPY_PROPERTIES,function(a,b){d[b]=c.css(b)}),d}}(a),getTextFromHeadToCaret:function(){return this.el.value.substring(0,this.el.selectionEnd)}}),a.fn.textcomplete.Textarea=b}(jQuery),+function(a){"use strict";function b(b,d,e){this.initialize(b,d,e),a(""+c+"").css({position:"absolute",top:-9999,left:-9999}).insertBefore(b)}var c="吶";a.extend(b.prototype,a.fn.textcomplete.Textarea.prototype,{select:function(b,c){var d=this.getTextFromHeadToCaret(),e=this.el.value.substring(d.length),f=c.replace(b);a.isArray(f)&&(e=f[1]+e,f=f[0]),d=d.replace(c.match,f),this.$el.val(d+e),this.el.focus();var g=this.el.createTextRange();g.collapse(!0),g.moveEnd("character",d.length),g.moveStart("character",d.length),g.select()},getTextFromHeadToCaret:function(){this.el.focus();var a=document.selection.createRange();a.moveStart("character",-this.el.value.length);var b=a.text.split(c);return 1===b.length?b[0]:b[1]}}),a.fn.textcomplete.IETextarea=b}(jQuery),+function(a){"use strict";function b(a,b,c){this.initialize(a,b,c)}a.extend(b.prototype,a.fn.textcomplete.Adapter.prototype,{select:function(b,c){var d=this.getTextFromHeadToCaret(),e=window.getSelection(),f=e.getRangeAt(0),g=f.cloneRange();g.selectNodeContents(f.startContainer);var h=g.toString(),i=h.substring(f.startOffset),j=c.replace(b);a.isArray(j)&&(i=j[1]+i,j=j[0]),d=d.replace(c.match,j),f.selectNodeContents(f.startContainer),f.deleteContents();var k=document.createTextNode(d+i);f.insertNode(k),f.setStart(k,d.length),f.collapse(!0),e.removeAllRanges(),e.addRange(f)},_getCaretRelativePosition:function(){var b=window.getSelection().getRangeAt(0).cloneRange(),c=document.createElement("span");b.insertNode(c),b.selectNodeContents(c),b.deleteContents();var d=a(c),e=d.offset();e.left-=this.$el.offset().left,e.top+=d.height()-this.$el.offset().top,e.lineHeight=d.height();var f=this.$el.attr("dir")||this.$el.css("direction");return"rtl"===f&&(e.left-=this.listView.$el.width()),e},getTextFromHeadToCaret:function(){var a=window.getSelection().getRangeAt(0),b=a.cloneRange();return b.selectNodeContents(a.startContainer),b.toString().substring(0,a.startOffset)}}),a.fn.textcomplete.ContentEditable=b}(jQuery); 2 | /* 3 | //@ sourceMappingURL=dist/jquery.textcomplete.min.map 4 | */ -------------------------------------------------------------------------------- /app/assets/javascripts/ting.js.coffee: -------------------------------------------------------------------------------- 1 | (($) -> 2 | Boker = 3 | init: -> 4 | self = this 5 | Bootup = -> 6 | self.siteBootUp() 7 | return 8 | 9 | PageUpdate = -> 10 | self.sitePageUpdate() 11 | return 12 | 13 | PageRestore = -> 14 | self.sitePageRestore() 15 | 16 | $(document).on "page:load", Bootup 17 | $(document).on "page:update", PageUpdate 18 | $(document).on "page:restore", PageRestore 19 | return 20 | 21 | siteBootUp: -> 22 | self = this 23 | self.initSemanticUiTools() 24 | self.initAvatarPreview() 25 | self.initUploadAvatar() 26 | self.initCheckXiamiInfo() 27 | self.initLoadingForm() 28 | self.initCustomDataConfirm() 29 | self.initInfiniteScrolling() 30 | self.initReplyUser() 31 | self.initRainyDay() 32 | self.initAutocompleteAtUser() 33 | self.initReplyOnPressKey() 34 | return 35 | 36 | sitePageUpdate: -> 37 | self = this 38 | self.initCloseMessage() 39 | # self.initGetNotificationsCount() 40 | self.initPlayer() 41 | return 42 | 43 | sitePageRestore: -> 44 | self = this 45 | self.initRemoveLoading() 46 | $('.ui.sticky').sticky('refresh'); 47 | return 48 | 49 | # Run semantic ui 50 | initSemanticUiTools: -> 51 | $(".run-popup").popup() 52 | $(".ui.selection.dropdown").dropdown() 53 | $('.ui.sticky').sticky({offset: 100, bottomOffset: 50, context: '#main'}) 54 | 55 | $('.modal-toggle').click -> 56 | $('.song-form').modal('show') 57 | 58 | $('.forget-password').click -> 59 | $('.reset-pwd-form').modal('show') 60 | return false 61 | 62 | $('#song-loading').progress('increment') 63 | $('.dimmer-image').dimmer({on: 'hover'}) 64 | $('.ui.radio.checkbox').checkbox() 65 | $('.secondary.menu').find('.item').click -> 66 | $('.item.active').removeClass('active') 67 | $(this).addClass('active') 68 | $('.status-panel').html('
    '+$('.secondary').data('loading')+'
    ') 69 | return 70 | 71 | # Avatar preview 72 | initAvatarPreview: -> 73 | $(document).ready -> 74 | AvatarPreview = (avatar) -> 75 | if avatar.files and avatar.files[0] 76 | reader = new FileReader() 77 | reader.onload = (e) -> 78 | $(".user-avatar").attr "src", e.target.result 79 | return 80 | reader.readAsDataURL avatar.files[0] 81 | return 82 | 83 | $(".upload-avatar").change -> 84 | AvatarPreview this 85 | return 86 | return 87 | return 88 | 89 | # Upload avatar 90 | initUploadAvatar: -> 91 | $("input#user_avatar").css "display", "none" 92 | $(".user-avatar-upload").click -> 93 | $(".upload-avatar").click() 94 | return 95 | return 96 | 97 | # Auto close message 98 | initCloseMessage: -> 99 | $(".message .close").on "click", -> 100 | $(this).closest(".message").fadeOut() 101 | return 102 | return 103 | 104 | # Player 105 | initPlayer: -> 106 | $('.playBtn').unbind('click') 107 | $('.playBtn').click -> 108 | self = $(this) 109 | play_icon = self.find('i.play') 110 | pause_icon = self.find('i.pause') 111 | $player = $('#player').get(0) 112 | $audio = $('audio') 113 | 114 | if $audio.attr('data-xiami_id') is $(this).attr 'data-xiami_id' 115 | if $player.paused 116 | $player.play() 117 | $('.rotating').removeClass 'stop-rotate' 118 | play_icon.removeClass('play').addClass 'pause' 119 | else 120 | $player.pause() 121 | $('.rotating').addClass 'stop-rotate' 122 | $('.pause').removeClass('pause').addClass 'play' 123 | else 124 | play_icon.addClass('spinner loading') 125 | playMusic = (music) -> 126 | $.get 'http://xiamirun.avosapps.com/run?song=http://www.xiami.com/song/' + music, (data) -> 127 | if data 128 | $('.stop-rotate').removeClass 'stop-rotate' 129 | play_icon.removeClass('spinner loading') 130 | $('.rotating').removeClass 'rotating' 131 | self.siblings('.image').addClass 'rotating' 132 | self.parents('.image').addClass 'rotating' 133 | $audio.attr 134 | "src": data.url 135 | "data-xiami_id": music 136 | $('.pause').removeClass('pause').addClass 'play' 137 | play_icon.removeClass('play').addClass 'pause' 138 | $player.play() 139 | return 140 | playMusic($(this).data('xiami_id')) 141 | 142 | $audio.on 'ended', -> 143 | $('.rotating').removeClass 'rotating' 144 | $('.pause').removeClass('pause').addClass 'play' 145 | next_song = $(self).parents('.songs-list').next('.songs-list') 146 | $audio.attr 147 | "src": '' 148 | "data-xiami_id": -1 149 | next_song.find('.playBtn').click() 150 | return 151 | return 152 | 153 | # Polling for change of notifications count 154 | initGetNotificationsCount: -> 155 | interval = null 156 | clearTimeout(interval) 157 | if $('#unread-count').length > 0 158 | interval = setTimeout (-> 159 | $.post "/notifications/count", (data) -> 160 | if data > 0 161 | ( if data > 99 then '99' else data) 162 | $('#unread-count').addClass('red').text data 163 | return 164 | return 165 | ),30000 166 | return 167 | 168 | # Auto check whether the xiami id is exists 169 | initCheckXiamiInfo: -> 170 | $('input#song_s_id').blur -> 171 | $('#song-errors').removeClass().html "" 172 | s_id = $('input#song_s_id').val() 173 | $btn = $(this) 174 | $btn.addClass 'loading' 175 | if s_id.length > 0 176 | $.get("http://xiamirun.avosapps.com/run?song=http://www.xiami.com/song/" + s_id, (data) -> 177 | # parse single quotation marks for song title 178 | title = data.title.replace(/'/,"'") 179 | $('input#song_title').val(title) 180 | $('input#song_artist').val(data.artist) 181 | $('#song-errors').addClass("animated zoomIn").html '
    '+$('#xiami-errors').data('passed')+'
    ' 182 | return 183 | ).fail( -> 184 | $('input#song_title').val("") 185 | $('input#song_artist').val('') 186 | $('#song-errors').addClass("animated bounceIn").html '
    '+$('#xiami-errors').data('cant_fetch')+'
    ' 187 | return 188 | ).always -> 189 | $btn.removeClass 'loading' 190 | return 191 | else 192 | $('#song-errors').addClass("animated flash").html('
    '+$('#xiami-errors').data('blank')+'
    ') 193 | $(this).removeClass 'loading' 194 | return 195 | return 196 | 197 | # Add the loading effects to the form 198 | initLoadingForm: -> 199 | $(':submit').click -> 200 | $('.ui.form').addClass "loading" 201 | $('.ui.form .button').addClass "disabled" 202 | 203 | # Customizing confirmation dialogs in Rails 204 | initCustomDataConfirm: -> 205 | $.rails.allowAction = (link) -> 206 | return true unless link.attr("data-confirm") 207 | $.rails.showConfirmDialog link 208 | false 209 | 210 | $.rails.confirmed = (link) -> 211 | link.removeAttr "data-confirm" 212 | link.trigger "click.rails" 213 | 214 | $.rails.showConfirmDialog = (link) -> 215 | html = undefined 216 | message = undefined 217 | $(".confirm-modal").remove() 218 | message = link.attr("data-confirm") 219 | html = "

    " + message + "

    No
    Yes
    " 220 | $("body").append html 221 | $(".confirm-modal").modal "show" 222 | $(".confirm-modal .actions .positive").on "click", -> 223 | $.rails.confirmed link 224 | 225 | # When user click to go back, it should get a better works 226 | initRemoveLoading: -> 227 | $('.ui.form').removeClass "loading" 228 | 229 | # Infinite Scrolling 230 | initInfiniteScrolling: -> 231 | $('#pagination').css('display', 'none') 232 | $("#songs").infinitescroll 233 | loading: 234 | finishedMsg: '
    '+$('#pagination').data('loaded')+'
    ' 235 | img: $('#songs').data('image') 236 | msgText: "" 237 | navSelector: "nav.pagination" 238 | nextSelector: "nav.pagination a[rel=next]" 239 | itemSelector: "#songs .songs-list" 240 | animate: false 241 | 242 | # Click to reply user 243 | initReplyUser: -> 244 | $('.reply-user').click '.reply', -> 245 | value = $('textarea').val() 246 | name = "@" + $(this).find('.reply').attr 'data-name' 247 | $('textarea').val(value + " " + name + " ") 248 | $('textarea').focus() 249 | 250 | # Auto complete at user 251 | initAutocompleteAtUser: -> 252 | at_users = [] 253 | user = undefined 254 | $users = $(".author") 255 | i = 0 256 | 257 | while i < $users.length 258 | user = $users.eq(i).html() 259 | at_users.push user if $.inArray(user, at_users) is -1 260 | i++ 261 | $("textarea").textcomplete [ 262 | mentions: at_users 263 | match: /\B@(\w*)$/ 264 | search: (term, callback) -> 265 | callback $.map(@mentions, (mention) -> 266 | (if mention.indexOf(term) is 0 then mention else null) 267 | ) 268 | return 269 | 270 | index: 1 271 | replace: (mention) -> 272 | "@" + mention + " " 273 | ], 274 | appendTo: "body" 275 | 276 | # Rainy day 277 | initRainyDay: -> 278 | image = document.getElementById("rainyday") 279 | if typeof(image) != 'undefined' and image != null 280 | image.onload = -> 281 | engine = new RainyDay(image: this) 282 | engine.rain [[ 283 | 3 284 | 2 285 | 2 286 | ]], 100 287 | return 288 | image.crossOrigin = "anonymous" 289 | 290 | initReplyOnPressKey: -> 291 | $(document).on 'keydown', 'textarea', (e) -> 292 | if (e.keyCode == 10 or e.keyCode == 13) and (e.ctrlKey or e.keyCode == 91) 293 | $(this).parents('form').submit() 294 | false 295 | 296 | window.Boker = Boker 297 | return 298 | ) jQuery 299 | 300 | $(document).ready -> 301 | Boker.init() 302 | Boker.siteBootUp() 303 | return 304 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.scss: -------------------------------------------------------------------------------- 1 | /* 2 | *= require semantic 3 | *= require animate 4 | *= require_self 5 | */ 6 | 7 | @import "compass"; 8 | @import "textcomplete"; 9 | @import "users"; 10 | @import "songs"; 11 | @import "comments"; 12 | @import "notifications"; 13 | @import "ting"; -------------------------------------------------------------------------------- /app/assets/stylesheets/comments.css.scss: -------------------------------------------------------------------------------- 1 | .ui.comments { 2 | max-width: none; 3 | } 4 | .comment-content { 5 | padding-top: 5px; 6 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/likeships.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the likeships controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/notifications.css.scss: -------------------------------------------------------------------------------- 1 | h3.notifications-header { 2 | margin-top: 0; 3 | margin-bottom: 0; 4 | } 5 | #notifications { 6 | margin-top: 30px; 7 | .ui.animated.list { 8 | .item { 9 | padding: 10px; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/songs.css.scss: -------------------------------------------------------------------------------- 1 | #main { 2 | min-height: 500px; 3 | .modal-toggle { 4 | width: 35px; 5 | line-height: 20px; 6 | padding: 10px; 7 | position: fixed; 8 | left: 130px; 9 | top: 100px; 10 | } 11 | .modal-en { 12 | width: auto; 13 | left: 40px; 14 | } 15 | } 16 | .songs-list { 17 | width: 100%; 18 | } 19 | .song-container { 20 | .album-pic { 21 | float: left; 22 | margin-right: 30px; 23 | } 24 | } 25 | .song-info { 26 | height: 100%; 27 | } 28 | .song-content { 29 | line-height: 1.6; 30 | font-size: 1.1em; 31 | color: #6B6B6B; 32 | margin-top: 15px; 33 | } 34 | .song-footer { 35 | width: 100%; 36 | padding-right: 5px; 37 | margin-top: 30px; 38 | } 39 | .musician { 40 | float: right; 41 | } 42 | .com-count { 43 | float: left; 44 | line-height: 36px; 45 | } 46 | .album-pic { 47 | width: 180px; 48 | position: relative; 49 | .outer-pic { 50 | width: 180px; 51 | height: 180px; 52 | border: 1px solid #A0FF99; 53 | img { 54 | min-width: 180px; 55 | min-height: 180px; 56 | } 57 | .dimmer { 58 | @include border-radius(50%); 59 | } 60 | } 61 | } 62 | .operate { 63 | float: right; 64 | } 65 | #song-edit { 66 | textarea#song_content { 67 | min-height: 300px; 68 | } 69 | } 70 | #infscr-loading { 71 | img { 72 | display: block; 73 | margin: 0 auto; 74 | } 75 | } 76 | .song-panel { 77 | p { 78 | margin-top: 30px; 79 | color: #00b5ad; 80 | } 81 | } 82 | .song-tools-divider { 83 | padding-left: 5px; 84 | padding-right: 5px; 85 | color: #ccc; 86 | 87 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/textcomplete.css.scss: -------------------------------------------------------------------------------- 1 | /* Textcomplete */ 2 | .dropdown-menu { 3 | background-color: white; 4 | @include box-shadow(#CCCCCC 0px 2px 10px); 5 | } 6 | 7 | .dropdown-menu li { 8 | min-width: 10rem; 9 | padding: 5px 10px; 10 | } 11 | 12 | .dropdown-menu li:first-child { 13 | border-top: none; 14 | } 15 | 16 | .dropdown-menu li:hover, 17 | .dropdown-menu .active { 18 | background-color: #18A7DD; 19 | a { 20 | color: #fff; 21 | } 22 | } 23 | 24 | 25 | /* SHOULD not modify */ 26 | .dropdown-menu { 27 | list-style: none; 28 | padding: 0; 29 | margin: 0; 30 | } 31 | 32 | .dropdown-menu a:hover { 33 | cursor: pointer; 34 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/ting.css.scss: -------------------------------------------------------------------------------- 1 | html.turbolinks-progress-bar::before { 2 | background-color: #FFB8B8 !important; 3 | height: 3px !important; 4 | } 5 | ::selection { 6 | background-color: rgba(229, 253, 254, 1); 7 | } 8 | .ui.message { 9 | box-shadow: none !important; 10 | } 11 | body { 12 | font-family: arial, sans-serif; 13 | overflow-x: hidden; 14 | } 15 | .container { 16 | padding-top: 35px; 17 | padding-right: 14px; 18 | & > .grid { 19 | padding-top: 50px; 20 | } 21 | } 22 | #logo { 23 | position: static; 24 | } 25 | :focus { 26 | outline: none; 27 | } 28 | .flash { 29 | box-shadow: none !important; 30 | } 31 | #t-flash { 32 | width: 101%; 33 | position: absolute; 34 | top: -14px; 35 | z-index: 9999; 36 | } 37 | #canvas-bg { 38 | background-color: rgba(91, 189, 114, 0.78); 39 | position: fixed; 40 | z-index: -1; 41 | width: 100%; 42 | height: 100%; 43 | } 44 | #particles { 45 | position: fixed; 46 | } 47 | #site-status { 48 | h3 { 49 | margin-top: 0; 50 | } 51 | } 52 | .ui.form { 53 | margin-top: 10px; 54 | } 55 | .rotating { 56 | animation: rotating 8s linear infinite; 57 | -webkit-animation: rotating 8s linear infinite; 58 | } 59 | @keyframes rotating { 60 | from { 61 | transform: rotate(0deg); 62 | } 63 | to { 64 | transform: rotate(360deg); 65 | } 66 | } 67 | @-webkit-keyframes rotating { 68 | from { 69 | -webkit-transform: rotate(0deg); 70 | } 71 | to { 72 | -webkit-transform: rotate(360deg); 73 | } 74 | } 75 | @-moz-keyframes rotating { 76 | from { 77 | -moz-transform: rotate(0deg); 78 | } 79 | to { 80 | -moz-transform: rotate(360deg); 81 | } 82 | } 83 | .stop-rotate { 84 | animation-play-state: paused; 85 | -webkit-animation-play-state: paused; 86 | } 87 | .pull-left { 88 | float: left; 89 | } 90 | .pull-right { 91 | float: right; 92 | } 93 | .help-inline { 94 | margin-top: 8px; 95 | margin-right: 10px; 96 | } 97 | 98 | //Chrome 99 | input:-webkit-autofill { 100 | -webkit-box-shadow: 0 0 0px 1000px rgba(255, 194, 157, 0.5) inset !important; 101 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/users.css.scss: -------------------------------------------------------------------------------- 1 | #rainyday { 2 | width: 100%; 3 | height: 100%; 4 | position: absolute; 5 | z-index: -1; 6 | } 7 | .wallpaper { 8 | position: absolute; 9 | z-index: 100; 10 | width: 100%; 11 | height: 100%; 12 | .signup-form { 13 | margin: 0 auto; 14 | padding-top: 180px; 15 | width: 25%; 16 | .ui.form { 17 | background-color: rgba(225, 225, 225, 0.35); 18 | padding: 25px; 19 | @include border-radius(5px); 20 | a { 21 | color: rgba(0, 0, 0, 0.8); 22 | } 23 | } 24 | } 25 | .login-form { 26 | @extend .signup-form; 27 | padding-top: 200px; 28 | } 29 | } 30 | .pin-button { 31 | position: relative; 32 | top: 30px; 33 | z-index: 3000; 34 | right: 40px; 35 | .button { 36 | margin-right: 10px; 37 | } 38 | } 39 | .user-avatar { 40 | cursor: pointer; 41 | margin: 0 auto; 42 | } 43 | .mini-avatar { 44 | width: 20px !important; 45 | height: 20px !important; 46 | } 47 | .thumb { 48 | width: 100px; 49 | height: 100px; 50 | } 51 | .lg { 52 | width: 200px; 53 | height: 200px; 54 | } 55 | #full-button { 56 | padding: 0; 57 | .button { 58 | width: 49%; 59 | } 60 | } 61 | .session-buttons { 62 | width: 100%; 63 | .button { 64 | width: 49%; 65 | } 66 | } 67 | #user-show { 68 | .info-column { 69 | @include border-radius(5px); 70 | padding: 25px; 71 | color: #666; 72 | .user-status { 73 | p { 74 | line-height: 1.63; 75 | } 76 | } 77 | } 78 | .content-column { 79 | @include border-radius(5px); 80 | padding: 30px; 81 | margin-bottom: 50px; 82 | .status-panel { 83 | min-height: 100%; 84 | padding-left: 5px; 85 | padding-top: 15px; 86 | a { 87 | color: #fff; 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | include UsersHelper 6 | before_action :set_locale 7 | 8 | private 9 | 10 | def set_locale 11 | I18n.locale = current_user.try(:locale) || I18n.default_locale 12 | end 13 | end -------------------------------------------------------------------------------- /app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | before_action :require_login 3 | before_action :find_song 4 | 5 | def create 6 | @comment = @song.comments.build(comment_params.merge(user: current_user)) 7 | @comment.save 8 | @comments = @song.comments.all 9 | respond_to do |format| 10 | format.html { redirect_to @song } 11 | format.js 12 | end 13 | end 14 | 15 | def destroy 16 | @comment = current_user.comments.find(params[:id]) 17 | @comment.destroy 18 | respond_to do |format| 19 | format.html { redirect_to @song } 20 | format.js 21 | end 22 | end 23 | 24 | private 25 | def comment_params 26 | params.require(:comment).permit(:content) 27 | end 28 | 29 | def find_song 30 | @song = Song.find(params[:song_id]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/likeships_controller.rb: -------------------------------------------------------------------------------- 1 | class LikeshipsController < ApplicationController 2 | before_action :require_login, :find_likeable 3 | 4 | include LikeshipsHelper 5 | def create 6 | current_user.like!(params[:likeable_type], params[:likeable_id]) 7 | respond_to do |format| 8 | format.html { redirect_to correct_path } 9 | format.js 10 | end 11 | end 12 | 13 | def destroy 14 | current_user.unlike!(params[:likeable_type], params[:likeable_id]) 15 | respond_to do |format| 16 | format.html { redirect_to correct_path } 17 | format.js 18 | end 19 | end 20 | 21 | private 22 | def find_likeable 23 | @likeable = Likeship.likeable(Kernel.const_get(params[:likeable_type]).find(params[:likeable_id])) 24 | end 25 | 26 | end -------------------------------------------------------------------------------- /app/controllers/notifications_controller.rb: -------------------------------------------------------------------------------- 1 | class NotificationsController < ApplicationController 2 | before_action :require_login 3 | 4 | def index 5 | @notifications = current_user.notifications 6 | .order(id: :desc) 7 | .page(params[:page]) 8 | .per(25) 9 | 10 | @notifications.unread.update_all(read: true, updated_at: Time.now.utc) 11 | @user_panel = true 12 | end 13 | 14 | def destroy 15 | @notification = current_user.notifications.find params[:id] 16 | @notification.destroy 17 | end 18 | 19 | def clear 20 | @notifications = current_user.notifications 21 | current_user.notifications.delete_all 22 | respond_to do |format| 23 | format.html { redirect_to notifications_path } 24 | format.js 25 | end 26 | end 27 | 28 | def count 29 | render text: current_user.notifications.unread.count 30 | end 31 | 32 | end -------------------------------------------------------------------------------- /app/controllers/password_resets_controller.rb: -------------------------------------------------------------------------------- 1 | class PasswordResetsController < ApplicationController 2 | skip_before_action :require_login 3 | 4 | # request password reset. 5 | # you get here when the user entered his email in the reset password form and submitted it. 6 | def create 7 | @user = User.find_by_email(params[:email]) 8 | 9 | # This line sends an email to the user with instructions on how to reset their password (a url with a random token) 10 | @user.deliver_reset_password_instructions! if @user 11 | 12 | # Tell the user instructions have been sent whether or not email was found. 13 | # This is to not leak information to attackers about which emails exist in the system. 14 | flash[:info] = "#{t('.hint')}" 15 | redirect_to root_path 16 | end 17 | 18 | # This is the reset password form. 19 | def edit 20 | @token = params[:id] 21 | @user = User.load_from_reset_password_token(params[:id]) 22 | 23 | if @user.blank? 24 | not_authenticated 25 | return 26 | end 27 | end 28 | 29 | # This action fires when the user has sent the reset password form. 30 | def update 31 | @token = params[:id] 32 | @user = User.load_from_reset_password_token(params[:id]) 33 | 34 | if @user.blank? 35 | not_authenticated 36 | return 37 | end 38 | 39 | # the next line makes the password confirmation validation work 40 | @user.password_confirmation = params[:user][:password_confirmation] 41 | # the next line clears the temporary token and updates the password 42 | if @user.change_password!(params[:user][:password]) 43 | flash[:success] = "#{t('.successfully_updated_password')}" 44 | auto_login(@user) 45 | redirect_to root_path 46 | else 47 | render :action => "edit" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | before_action :not_login_user, only: [ :new ] 3 | 4 | def new 5 | @user = User.new 6 | render layout: "users_form" 7 | end 8 | 9 | def create 10 | if @user = login(params[:session][:email], params[:session][:password], params[:session][:remember]) 11 | flash[:success] = "#{t('.successfully')}" 12 | respond_to do |format| 13 | format.html { redirect_back_or_to root_path } 14 | format.js 15 | end 16 | else 17 | user = User.find_by_email(params[:session][:email]) 18 | if user && user.activation_state == "pending" 19 | flash.now[:warning] = "sessions.new.inactivation" 20 | @inactivation = true 21 | else 22 | flash.now[:error] = "#{t('sessions.new.invalid_email_password_combination')}" 23 | end 24 | respond_to do |format| 25 | format.html { render action: 'new' } 26 | format.js 27 | end 28 | end 29 | end 30 | 31 | def destroy 32 | logout 33 | redirect_to root_path 34 | flash[:success] = "#{t('.successfully')}" 35 | end 36 | 37 | private 38 | 39 | def not_login_user 40 | if logged_in? 41 | redirect_to root_path 42 | flash[:warning] = "#{t('users.has_already_logged_in')}" 43 | end 44 | end 45 | 46 | end -------------------------------------------------------------------------------- /app/controllers/songs_controller.rb: -------------------------------------------------------------------------------- 1 | class SongsController < ApplicationController 2 | before_action :require_login, only: [ :new, :create, :collect, :edit, :update, :destroy ] 3 | before_action :find_song, only: [ :edit, :update, :destroy ] 4 | 5 | def index 6 | @songs = Song.includes(:user).all.order("created_at desc").page params[:page] 7 | @hot_songs = Song.hot_songs 8 | @user_panel = true 9 | end 10 | 11 | def new 12 | @song = current_user.songs.build 13 | end 14 | 15 | def show 16 | @song = Song.includes(:comments).find(params[:id]) 17 | @comments = @song.comments.includes(:user, :likeships).page(params[:page]).per(25) 18 | end 19 | 20 | def collect 21 | songs_id = current_user.likeships.where("likeable_type = ?", "Song").collect(&:likeable_id) 22 | songs = Song.find(songs_id).reverse! 23 | @user_panel = true 24 | if songs.empty? 25 | @recommend = true 26 | @songs = Song.hot_songs.page params[:page] 27 | else 28 | @songs = Kaminari.paginate_array(songs).page(params[:page]) 29 | end 30 | end 31 | 32 | def create 33 | @song = current_user.songs.build(song_params) 34 | if @song.save 35 | flash.now[:success] = "#{t('.success')}" 36 | respond_to do |format| 37 | format.html { redirect_to root_path } 38 | format.js 39 | end 40 | else 41 | flash.now[:error] = "#{t('.faild')}" 42 | respond_to do |format| 43 | format.html { render 'new' } 44 | format.js 45 | end 46 | end 47 | end 48 | 49 | def edit 50 | end 51 | 52 | def update 53 | if @song.update_attributes params.require(:song).permit(:content) 54 | flash.now[:success] = "#{t('.successfully')}" 55 | respond_to do |format| 56 | format.html { redirect_to @song } 57 | format.js 58 | end 59 | else 60 | respond_to do |format| 61 | format.html { render 'edit' } 62 | format.js 63 | end 64 | end 65 | end 66 | 67 | def destroy 68 | @song = current_user.songs.find(params[:id]) 69 | if @song.destroy 70 | flash[:success] = "#{t('.successfully')}" 71 | respond_to do |format| 72 | format.html { redirect_to root_path } 73 | format.js 74 | end 75 | end 76 | end 77 | 78 | private 79 | 80 | def song_params 81 | params.require(:song).permit(:s_id, :content) 82 | end 83 | 84 | def find_song 85 | @song = current_user.songs.find(params[:id]) 86 | rescue ActiveRecord::RecordNotFound 87 | flash[:warning] = "#{t('permission_denied')}" 88 | redirect_to root_path 89 | end 90 | 91 | end 92 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | before_action :require_login, only: [ :edit, :update ] 3 | before_action :find_correct_user, only: [ :edit, :update ] 4 | before_action :not_login_user, only: [ :new, :create ] 5 | before_action :find_user, only: [ :show, :user_songs, :favorite_songs, :recent_comments ] 6 | 7 | def show 8 | if @user 9 | @songs = @user.songs.order("created_at desc") 10 | else 11 | redirect_to root_path 12 | flash[:error] = "#{t('.no_user')}" 13 | end 14 | end 15 | 16 | def new 17 | @user = User.new 18 | @signup = true 19 | render layout: "users_form" 20 | end 21 | 22 | def create 23 | @user = User.new(user_params) 24 | if @user.save 25 | flash[:success] = "#{t('.successfully')}" 26 | respond_to do |format| 27 | format.html { redirect_to root_path } 28 | format.js 29 | end 30 | else 31 | respond_to do |format| 32 | format.html { render 'new' } 33 | format.js 34 | end 35 | end 36 | end 37 | 38 | def edit 39 | end 40 | 41 | def update 42 | if @user.update_attributes(update_user_params) 43 | flash[:success] = "#{t('.successfully')}" 44 | respond_to do |format| 45 | format.html { redirect_to root_path } 46 | format.js 47 | end 48 | else 49 | flash.now[:error] = "#{t('.faild')}" 50 | respond_to do |format| 51 | format.html { render 'edit' } 52 | format.js 53 | end 54 | end 55 | end 56 | 57 | def activate 58 | if (@user = User.load_from_activation_token(params[:id])) 59 | @user.activate! 60 | flash[:success] = "#{t('.activated')}" 61 | auto_login(@user) 62 | else 63 | not_authenticated 64 | end 65 | end 66 | 67 | def user_songs 68 | @songs = @user.songs.order("created_at desc") 69 | end 70 | 71 | def favorite_songs 72 | songs_id = @user.likeships.where("likeable_type = ?", "Song").collect(&:likeable_id) 73 | @songs = Song.find(songs_id).reverse! 74 | 75 | respond_to do |format| 76 | format.js 77 | end 78 | end 79 | 80 | def recent_comments 81 | @comments = @user.comments.includes(:song).order("created_at desc") 82 | respond_to do |format| 83 | format.js 84 | end 85 | end 86 | 87 | def language 88 | if params[:locale] 89 | I18n.default_locale = params[:locale] 90 | redirect_to :back 91 | end 92 | end 93 | 94 | private 95 | 96 | def find_correct_user 97 | @user = User.find_by_name(params[:id]) 98 | unless current_user?(@user) 99 | flash[:warning] = "#{t('permission_denied')}" 100 | redirect_to root_path 101 | end 102 | end 103 | 104 | def find_user 105 | @user = User.find_by_name(params[:id]) 106 | end 107 | 108 | def user_params 109 | params.require(:user).permit(:name, :email, :password, :password_confirmation) 110 | end 111 | 112 | def update_user_params 113 | params.require(:user).permit(:bio, :avatar, :locale, :password, :password_confirmation) 114 | end 115 | 116 | def not_login_user 117 | if logged_in? 118 | redirect_to root_path 119 | flash[:warning] = "#{t('users.has_already_logged_in')}" 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def parse_text(text) 3 | text = text.gsub /@(\w+)/ do |username| 4 | name = username.gsub('@', '') 5 | link_to(username, user_path(name)) 6 | end 7 | sanitize text.gsub(/\n/, '
    '), tags: %w{ br a }, attributes: %w{ href } 8 | end 9 | end -------------------------------------------------------------------------------- /app/helpers/comments_helper.rb: -------------------------------------------------------------------------------- 1 | module CommentsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/likeships_helper.rb: -------------------------------------------------------------------------------- 1 | module LikeshipsHelper 2 | def likeable_likes_tag like 3 | @count = Likeship.where("likeable_type = ? and likeable_id = ?", like[:likeable_type], like[:likeable_id]).count 4 | if @count > 0 5 | likes_count = @count 6 | else 7 | likes_count = '' 8 | end 9 | 10 | if current_user && current_user.liking?(like) 11 | link_title = "unlike" 12 | link_path_method = "delete" 13 | fa_icon = '' 14 | else 15 | link_title = "like" 16 | link_path_method = "post" 17 | fa_icon = '' 18 | end 19 | 20 | if current_user.blank? 21 | "#{likes_count}#{fa_icon}".html_safe 22 | else 23 | link_to "#{likes_count}#{fa_icon}".html_safe, 24 | likeship_path(like), 25 | title: link_title, 26 | class: "animated zoomIn", 27 | method: link_path_method, 28 | remote: true 29 | end 30 | 31 | end 32 | 33 | def correct_path 34 | if params[:likeable_type].to_s == "Song" 35 | song_path(params[:likeable_id]) 36 | elsif params[:likeable_type].to_s == "Comment" 37 | Comment.find(params[:likeable_id]).song 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /app/helpers/notifications_helper.rb: -------------------------------------------------------------------------------- 1 | module NotificationsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/password_resets_helper.rb: -------------------------------------------------------------------------------- 1 | module PasswordResetsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module SessionsHelper 2 | end -------------------------------------------------------------------------------- /app/helpers/songs_helper.rb: -------------------------------------------------------------------------------- 1 | module SongsHelper 2 | require 'open-uri' 3 | require 'json' 4 | 5 | def get_xiami_info(object = {}) 6 | begin 7 | url = "http://xiamirun.avosapps.com/run?song=http://www.xiami.com/song/" + object.s_id.to_s 8 | doc = JSON.parse(open(url).read) 9 | rescue JSON::ParserError 10 | object.title = "" 11 | else 12 | object.title = doc['title'] 13 | object.artist = doc['artist'] 14 | object.pic = doc['cover'] 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | # Returns true if the given user is the current user. 3 | def current_user?(user) 4 | user == current_user 5 | end 6 | 7 | def require_login 8 | if !logged_in? 9 | redirect_to login_url 10 | flash[:warning] ="#{I18n.t('users.login_first')}" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/mailers/.keep -------------------------------------------------------------------------------- /app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | class UserMailer < ActionMailer::Base 2 | default from: "notify.ting@gmail.com" 3 | 4 | def reset_password_email(user) 5 | @user = user 6 | @url = edit_password_reset_url(user.reset_password_token) 7 | mail(:to => user.email, 8 | :subject => "#{t('mailer.subject.reset_password')}") 9 | end 10 | 11 | def activation_needed_email(user) 12 | @user = user 13 | @url = "http://0.0.0.0:3000/users/#{user.activation_token}/activate" 14 | mail(:to => user.email, 15 | :subject => "#{t('mailer.subject.welcome')}") 16 | end 17 | 18 | def activation_success_email(user) 19 | @user = user 20 | @url = "http://0.0.0.0:3000/login" 21 | mail(:to => user.email, 22 | :subject => "#{t('mailer.subject.done')}") 23 | end 24 | end -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/models/.keep -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :song, counter_cache: true, touch: true 4 | has_many :likeships, as: :likeable, foreign_key: "likeable_id", dependent: :destroy 5 | has_many :notifications, as: 'subject', dependent: :delete_all 6 | validates :user_id, :song_id, :content, presence: true 7 | validates :content, length: { maximum: 10000 } 8 | after_create :get_at_users 9 | after_destroy :delete_all_notifications 10 | 11 | def delete_all_notifications 12 | notifications.delete_all 13 | end 14 | 15 | def send_notification_to_at_users(at_users) 16 | (Array.wrap(at_users) - [self.user]).each do |at_user| 17 | Notification.create(user: at_user, subject: self, name: 'mention') 18 | end 19 | end 20 | 21 | def send_notification_to_musician(at_users) 22 | musician = self.song.user 23 | comment_owner_id = self.user_id 24 | 25 | if (Array.wrap(at_users).map(&:id) | [comment_owner_id] & [musician.id]).blank? 26 | Notification.create(user: musician, subject: self, name: 'comment') 27 | end 28 | end 29 | 30 | private 31 | 32 | def get_at_users 33 | mentioned_names_ary = self.content.gsub(/@(\w+)/).each_with_object([]) do |name, ary| 34 | ary.push name.gsub('@', '') 35 | end 36 | 37 | at_users = User.where(name: mentioned_names_ary).load 38 | send_notification_to_at_users(at_users) 39 | send_notification_to_musician(at_users) 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/likeship.rb: -------------------------------------------------------------------------------- 1 | class Likeship < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :likeable, polymorphic: true, touch: true 4 | validates :user_id, :likeable_id, :likeable_type, presence: true 5 | 6 | def self.likeable(like) 7 | likeable = { likeable_type: like.class.to_s, likeable_id: like.id } 8 | end 9 | 10 | end -------------------------------------------------------------------------------- /app/models/notification.rb: -------------------------------------------------------------------------------- 1 | class Notification < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :subject, polymorphic: true 4 | 5 | scope :named, -> (name) { where(name: name) } 6 | scope :unread, -> { where(read: false) } 7 | end -------------------------------------------------------------------------------- /app/models/song.rb: -------------------------------------------------------------------------------- 1 | class Song < ActiveRecord::Base 2 | include SongsHelper 3 | belongs_to :user 4 | has_many :comments, dependent: :delete_all 5 | has_many :likeships, as: :likeable, foreign_key: "likeable_id", dependent: :destroy 6 | validates :user_id, presence: true 7 | validates :content, presence: true, length: { maximum: 10000 } 8 | validates :title, :artist, presence: { message: :fetch } 9 | validates :s_id, presence: true, uniqueness: true, numericality: { greater_than: 0 } 10 | 11 | before_validation :set_xiami_info 12 | 13 | scope :hot_songs, -> { joins(:likeships). 14 | group('songs.id'). 15 | order('count(likeships.id) desc'). 16 | limit(10) } 17 | 18 | def set_xiami_info 19 | get_xiami_info(self) 20 | end 21 | 22 | end -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | authenticates_with_sorcery! 3 | mount_uploader :avatar, AvatarUploader 4 | has_many :songs, dependent: :delete_all 5 | has_many :comments, dependent: :destroy 6 | has_many :likeships, foreign_key: "user_id", dependent: :destroy 7 | has_many :notifications, dependent: :delete_all 8 | VALID_EMAIL_REGEX =/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i 9 | VALID_NAME_REGEX = /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ 10 | validates :name, presence: true, 11 | length: { maximum: 50 }, 12 | format: { with: VALID_NAME_REGEX }, 13 | uniqueness: { case_sensitive: false } 14 | validates :email, presence: true, 15 | format: { with: VALID_EMAIL_REGEX }, 16 | uniqueness: { case_sensitive: true } 17 | validates :password, presence: true, 18 | confirmation: true, 19 | length: { in: 6..16 }, 20 | on: :create 21 | validates :bio, length: { maximum: 140 } 22 | 23 | #Find user by name 24 | def to_param 25 | name 26 | end 27 | 28 | #Like 29 | def liking?(likeable) 30 | likeships.find_by(likeable) 31 | end 32 | 33 | def like!(type, id) 34 | likeships.create!(likeable_type: type, likeable_id: id) 35 | end 36 | 37 | def unlike!(type, id) 38 | likeships.find_by(likeable_type: type, likeable_id: id).destroy 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/uploaders/avatar_uploader.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class AvatarUploader < CarrierWave::Uploader::Base 4 | 5 | # Include RMagick or MiniMagick support: 6 | # include CarrierWave::RMagick 7 | include CarrierWave::MiniMagick 8 | 9 | # Choose what kind of storage to use for this uploader: 10 | storage :file 11 | # storage :fog 12 | 13 | # Override the directory where uploaded files will be stored. 14 | # This is a sensible default for uploaders that are meant to be mounted: 15 | def store_dir 16 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" 17 | end 18 | 19 | def extension_white_list 20 | %w(jpg jpeg gif png) 21 | end 22 | 23 | version :large do 24 | process :resize_to_fill => [200,200] 25 | end 26 | 27 | version :thumb do 28 | process :resize_to_fill => [100,100] 29 | end 30 | 31 | version :small, :from_version => :thumb do 32 | process resize_to_fill: [50, 50] 33 | end 34 | 35 | def default_url 36 | # For Rails 3.1+ asset pipeline compatibility: 37 | # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_')) 38 | 39 | #"/images/fallback/" + [version_name, "default.png"].compact.join('_') 40 | "missing.jpg" 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /app/views/comments/_comment.html.slim: -------------------------------------------------------------------------------- 1 | - cache comment do 2 | - user= comment.user 3 | .comment id="comment-#{comment.id}" 4 | = link_to user, class: "avatar commenter" do 5 | = image_tag(user.avatar_url(:small)) 6 | .content 7 | = link_to user.name, user, class: "author" 8 | .text.comment-content = parse_text(comment.content) 9 | .actions 10 | span.item id="like-comment-#{comment.id}" 11 | - @likeable = Likeship.likeable(comment) 12 | = likeable_likes_tag @likeable 13 | - if current_user == user 14 | = link_to [@song, comment], method: :delete, remote: true, data: { confirm: "#{t('form.are_you_sure')}" } do 15 | i.pink.trash.icon 16 | - else 17 | span.reply-user 18 | a.reply data-name="#{user.name}" 19 | i.reply.orange.icon -------------------------------------------------------------------------------- /app/views/comments/_comments.html.slim: -------------------------------------------------------------------------------- 1 | - cache @comments do 2 | - @comments.each do |comment| 3 | = render 'comments/comment', comment: comment -------------------------------------------------------------------------------- /app/views/comments/_form.html.slim: -------------------------------------------------------------------------------- 1 | - comment = @song.comments.build 2 | = form_for [@song, comment], remote: true do |f| 3 | #comment-errors 4 | = render 'shared/error_messages', object: comment 5 | .ui.form 6 | .field 7 | = f.text_area :content 8 | = f.submit "#{t('form.submit')}", class: "ui inverted button green pull-right" 9 | span.pull-right.help-inline Ctrl+Enter 10 | .field -------------------------------------------------------------------------------- /app/views/comments/_recent_comments.html.slim: -------------------------------------------------------------------------------- 1 | - if comments.any? 2 | - cache comments do 3 | .ui.comments 4 | - comments.each do |comment| 5 | - cache comment do 6 | - song = comment.song 7 | .comment id="comment-#{comment.id}" 8 | = link_to @user, class: "avatar commenter" do 9 | = image_tag(@user.avatar_url(:small)) 10 | .content 11 | = link_to song.title, song, class: "author" 12 | .text.comment-content = parse_text(comment.content) 13 | .ui.divider 14 | - else 15 | .ui.center.aligned.basic.segment 16 | p =t('comments.no_comments_yet') 17 | p ...>_<... -------------------------------------------------------------------------------- /app/views/comments/create.js.erb: -------------------------------------------------------------------------------- 1 | $('.ui.form').removeClass('loading') 2 | $('.ui.form .button').removeClass('disabled') 3 | <% if @comment.errors.empty? %> 4 | if($('.commenter').length > 0){ 5 | new_comment = "<%= j render 'comment', comment: @comment %>"; 6 | $('.comments').append(new_comment); 7 | animated(); 8 | } 9 | else{ 10 | $('.comments').html("<%= j render 'comment', comment: @comment %>"); 11 | animated(); 12 | } 13 | <% else %> 14 | $('#comment-errors').html("<%= j render 'shared/error_messages', object: @comment %>"); 15 | $('#error_explanation').addClass('animated shake'); 16 | <% end %> 17 | 18 | function animated() { 19 | $('#comment-<%= @comment.id %>').addClass('animated bounceIn'); 20 | $('textarea').val(""); 21 | } -------------------------------------------------------------------------------- /app/views/comments/destroy.js.erb: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $('#comment-<%= @comment.id %>').addClass('animated bounceOut') 3 | .one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', 4 | function(){ 5 | $('#comment-<%= @comment.id %>').remove(); 6 | if(!$('.comments .commenter').length>0){ 7 | $('.comments').html('
    ...>_<...
    ') 8 | } 9 | } 10 | ); 11 | }); -------------------------------------------------------------------------------- /app/views/kaminari/_gap.html.slim: -------------------------------------------------------------------------------- 1 | .item 2 | = t('views.pagination.truncate').html_safe 3 | -------------------------------------------------------------------------------- /app/views/kaminari/_page.html.slim: -------------------------------------------------------------------------------- 1 | - if page.current? 2 | a.item.active href=url title="#{page}" 3 | = page 4 | - else 5 | a.item href=url title="#{page}" 6 | = page 7 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.slim: -------------------------------------------------------------------------------- 1 | = paginator.render do 2 | .ui.center.aligned.basic.segment 3 | nav.ui.pagination.menu 4 | - each_page do |page| 5 | - if page.left_outer? || page.right_outer? || page.inside_window? 6 | == page_tag page 7 | - elsif !page.was_truncated? 8 | == gap_tag 9 | == next_page_tag unless current_page.last? -------------------------------------------------------------------------------- /app/views/layouts/_navbar.html.slim: -------------------------------------------------------------------------------- 1 | - if logged_in? 2 | = render 'users/user_panel' 3 | - else 4 | .ui.grid.form.basic.segment 5 | #full-button.ui.buttons.sixteen.wide.column 6 | = link_to "#{t('users.signup')}", :signup, class: "ui blue inverted button" 7 | .or 8 | = link_to "#{t('users.login')}", :login, class: "ui purple inverted button" 9 | .ui.red.inverted.center.aligned.segment 10 | #site-status 11 | = link_to "中文简体", set_locale_url(locale: "zh-CN"), class: "ui red label" 12 | = link_to "English", set_locale_url(locale: "en"), class: "ui red label" 13 | .ui.divider 14 | .site-params 15 | p #{t('.total_users')}: #{t('users.show.users_count', count: User.count)} 16 | p #{t('users.show.total_songs')}: #{t('users.show.songs_count', count: Song.count)} 17 | p #{t('users.show.total_comments')}: #{t('users.show.comments_count', count: Comment.count)} -------------------------------------------------------------------------------- /app/views/layouts/application.html.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Ting 5 | = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true 6 | = javascript_include_tag 'application', 'data-turbolinks-track' => true 7 | = csrf_meta_tags 8 | = favicon_link_tag 'favicon.ico' 9 | body 10 | .ui.fixed.green.inverted.menu.center.aligned.segment 11 | = link_to root_path, id: "logo", class: "item" 12 | i.music.icon 13 | = render 'shared/flash' 14 | .container.animated.fadeIn 15 | - if @user_panel 16 | .ui.grid 17 | .two.wide.column 18 | #main.nine.wide.column 19 | = yield 20 | .right.ui.rail 21 | .ui.sticky 22 | = render 'layouts/navbar' 23 | - else 24 | = yield -------------------------------------------------------------------------------- /app/views/layouts/users_form.html.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Ting 5 | = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true 6 | = javascript_include_tag 'application', 'data-turbolinks-track' => true 7 | = csrf_meta_tags 8 | body 9 | 10 | .pin-button 11 | = link_to "#{t('.home')}", songs_path, class: "ui blue inverted pull-right button" 12 | - if @signup 13 | = link_to "#{t('users.login')}", :login, class: "ui green inverted pull-right button" 14 | - else 15 | = link_to "#{t('users.signup')}", :signup, class: "ui green inverted pull-right button" 16 | img#rainyday src="#{asset_path("night.jpg")}" 17 | .wallpaper 18 | = yield -------------------------------------------------------------------------------- /app/views/likeships/_like.html.slim: -------------------------------------------------------------------------------- 1 | i.red.heart.icon 2 | = form_for current_user.likeships.build do |f| 3 | = f.hidden_field :likeable_type, value: @likeable[:likeable_type] 4 | = f.hidden_field :likeable_id, value: @likeable[:likeable_id] 5 | = f.submit -------------------------------------------------------------------------------- /app/views/likeships/_likes.html.slim: -------------------------------------------------------------------------------- 1 | - if logged_in? 2 | - if current_user.liking?(@likeable) 3 | = render 'likeships/unlike' 4 | - else 5 | = render 'likeships/like' -------------------------------------------------------------------------------- /app/views/likeships/_unlike.html.slim: -------------------------------------------------------------------------------- 1 | i.empty.heart.icon 2 | = form_for current_user.likeships.find_by(@likeable), html; { method: :delete } do |f| 3 | = f.submit -------------------------------------------------------------------------------- /app/views/likeships/create.js.erb: -------------------------------------------------------------------------------- 1 | if('<%= params[:likeable_type] %>' == 'Song'){ 2 | $('#like-song-<%= params[:likeable_id] %>').html("<%= j(likeable_likes_tag @likeable) %>") 3 | }else { 4 | $('#like-comment-<%= params[:likeable_id] %>').html("<%= j(likeable_likes_tag @likeable) %>") 5 | } -------------------------------------------------------------------------------- /app/views/likeships/destroy.js.erb: -------------------------------------------------------------------------------- 1 | if('<%= params[:likeable_type] %>' == 'Song'){ 2 | $('#like-song-<%= params[:likeable_id] %>').html("<%= j(likeable_likes_tag @likeable) %>") 3 | }else { 4 | $('#like-comment-<%= params[:likeable_id] %>').html("<%= j(likeable_likes_tag @likeable) %>") 5 | } -------------------------------------------------------------------------------- /app/views/notifications/_notifications.html.slim: -------------------------------------------------------------------------------- 1 | - cache @notifications do 2 | #notifications 3 | .ui.animated.list 4 | - @notifications.each do |notification| 5 | .ui.feed 6 | .event id="notification-#{notification.id}" 7 | = render "notifications/notification/#{notification.name}", notification: notification 8 | = link_to notification_path(notification), remote: true, method: :delete, data: { confirm: "#{t('form.are_you_sure')}" }, class: "pull-right delete-notification" do 9 | i.red.trash.icon -------------------------------------------------------------------------------- /app/views/notifications/clear.js.erb: -------------------------------------------------------------------------------- 1 | $('#notifications').html("<%= j render 'notifications' %>") -------------------------------------------------------------------------------- /app/views/notifications/destroy.js.erb: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $('#notification-<%= @notification.id %>').addClass('animated lightSpeedOut') 3 | .one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', 4 | function(){ 5 | $(this).remove(); 6 | } 7 | ); 8 | }); -------------------------------------------------------------------------------- /app/views/notifications/index.html.slim: -------------------------------------------------------------------------------- 1 | .ui.stacked.segment 2 | .ui.ribbon.green.label 3 | h3.notifications-header =t('.notifications') 4 | = link_to "#{t('.empty_notifications')}", clear_notifications_path, remote: true, method: :delete, data: { confirm: "#{t('form.are_you_sure')}", disable_with: "#{t('.clearing')}" }, class: "ui red button pull-right" 5 | = render 'notifications' 6 | = paginate @notifications -------------------------------------------------------------------------------- /app/views/notifications/notification/_comment.html.slim: -------------------------------------------------------------------------------- 1 | - comment = notification.subject 2 | - user = comment.user 3 | - song = comment.song 4 | - author = song_path(song) + "#comment-#{comment.id}" 5 | 6 | - cache notification do 7 | .label 8 | = image_tag user.avatar_url(:small) 9 | .content 10 | .summary 11 | = link_to user.name, user, class: "ui label orange" 12 | | #{t('.add_a_comment_at')} 13 | = link_to song.title, author 14 | .extra.text 15 | = parse_text(comment.content) -------------------------------------------------------------------------------- /app/views/notifications/notification/_mention.html.slim: -------------------------------------------------------------------------------- 1 | - comment = notification.subject 2 | - user = comment.user 3 | - song = comment.song 4 | - author = song_path(song) + "#comment-#{comment.id}" 5 | 6 | - cache notification do 7 | .label 8 | = image_tag user.avatar_url(:small), class: "ui avatar image" 9 | .content 10 | .summary 11 | = link_to user.name, user, class: "ui label red" 12 | | #{t('.mention_you_at')} 13 | = link_to song.title, author 14 | .extra.text 15 | = parse_text(comment.content) -------------------------------------------------------------------------------- /app/views/password_resets/_form.html.slim: -------------------------------------------------------------------------------- 1 | = form_tag password_resets_path, :method => :post do 2 | .ui.form 3 | .field 4 | = text_field_tag :email, nil, placeholder: "#{t('activerecord.attributes.user.email')}" 5 | = submit_tag "#{t('form.reset_password')}", class: "ui blue fluid button" -------------------------------------------------------------------------------- /app/views/password_resets/edit.html.slim: -------------------------------------------------------------------------------- 1 | .ui.grid 2 | .six.wide.column 3 | .four.wide.column 4 | h2.header.center =t('.modify_password') 5 | .ui.form 6 | #password-edit-errors 7 | = render 'shared/error_messages', object: @user 8 | = form_for @user, :url => password_reset_path(@token), :html => {:method => :put} do |f| 9 | .field 10 | = f.text_field :email, placeholder: "#{t('activerecord.attributes.user.name')}", disabled: "disabled" 11 | .field 12 | = f.password_field :password, placeholder: "#{t('activerecord.attributes.user.password')}" 13 | .field 14 | = f.password_field :password_confirmation, placeholder: "#{t('activerecord.attributes.user.password_confirmation')}" 15 | = f.submit "#{t('form.save')}", class: "ui blue fluid button" -------------------------------------------------------------------------------- /app/views/password_resets/new.html.slim: -------------------------------------------------------------------------------- 1 | .ui.grid 2 | .six.wide.column 3 | .four.wide.column 4 | = render 'form' -------------------------------------------------------------------------------- /app/views/sessions/create.js.erb: -------------------------------------------------------------------------------- 1 | $('.ui.form').removeClass('loading') 2 | <% if logged_in? %> 3 | Turbolinks.visit('<%= root_path %>'); 4 | <% else %> 5 | $('.login-form').find('.field').addClass('error') 6 | $errors = $('#login-errors'); 7 | $errors.addClass('ui error message'); 8 | <% if @inactivation %> 9 | $errors.html($('.errors-info').data('inactivation')); 10 | <% else %> 11 | $errors.html($('.errors-info').data('error')); 12 | <% end %> 13 | $errors.transition('tada'); 14 | <% end %> -------------------------------------------------------------------------------- /app/views/sessions/new.html.slim: -------------------------------------------------------------------------------- 1 | .login-form 2 | = form_for :session, url: sessions_path, remote: true do |f| 3 | .ui.form.basic.segment 4 | #login-errors 5 | .field 6 | = f.text_field :email, placeholder: "#{t('activerecord.attributes.user.email')}" 7 | .field 8 | = f.password_field :password, placeholder: "#{t('activerecord.attributes.user.password')}" 9 | .field 10 | .ui.toggle.checkbox 11 | = f.check_box :remember 12 | = f.label :remember, "#{t('.remember_me')}" 13 | = link_to "#{t('.forget_password')}?", new_password_reset_path, class: "forget-password pull-right" 14 | = f.submit "#{t('users.login')}", class: "ui green fluid button" 15 | 16 | .errors-info data-error="#{t('.invalid_email_password_combination')}" data-inactivation="#{t('.inactivation')}" 17 | .small.ui.modal.reset-pwd-form 18 | i.close.red.icon 19 | .content 20 | = render 'password_resets/form' -------------------------------------------------------------------------------- /app/views/shared/_error_messages.html.slim: -------------------------------------------------------------------------------- 1 | - if object.errors.any? 2 | #error_explanation 3 | .ui.error.message 4 | i.close.icon 5 | .header 6 | | #{t('.the_form_contains')} 7 | - if I18n.locale.to_s == "zh-CN" 8 | = object.errors.count 9 | | 个错误 10 | - else 11 | = pluralize(object.errors.count, "error") 12 | ul.list 13 | - object.errors.full_messages.each do |msg| 14 | li = msg -------------------------------------------------------------------------------- /app/views/shared/_flash.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |key, value| %> 2 |
    3 | <%= value %> 4 |
    5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/songs/_form.html.slim: -------------------------------------------------------------------------------- 1 | = form_for song, remote: true do |f| 2 | #song-errors 3 | = render 'shared/error_messages', object: song 4 | .ui.form 5 | .fields 6 | .eight.wide.field 7 | .ui.labeled.input 8 | .ui.teal.label http://www.xiami.com/song/ 9 | - if action_name == "edit" 10 | = f.text_field :s_id, placeholder: "#{t('activerecord.attributes.song.s_id')}", disabled: "disabled" 11 | - else 12 | = f.text_field :s_id, placeholder: "#{t('activerecord.attributes.song.s_id')}" 13 | .four.wide.field 14 | = f.text_field :title, placeholder: "#{t('.title')}", disabled: "disabled" 15 | .four.wide.field 16 | = f.text_field :artist, placeholder: "#{t('.singer')}", disabled: "disabled" 17 | .field 18 | = f.text_area :content, placeholder: "#{t('.content')}" 19 | = f.hidden_field :pic 20 | = f.submit "#{t('form.submit')}", class: "ui submit button orange right floated", data: { disable_with: "#{t('form.submitting')}" } 21 | span.pull-right.help-inline Ctrl+Enter 22 | javascript: 23 | if($('input#song_title').val().length > 0){ 24 | title = $('input#song_title').val().replace(/'/,"'") 25 | $('input#song_title').val(title) 26 | } 27 | #xiami-errors data-blank="#{t('activerecord.attributes.song.s_id')} #{t('.cant_be_blank')}" data-cant_fetch="#{t('songs.cant_fetch')}" data-passed="#{t('.passed')}" -------------------------------------------------------------------------------- /app/views/songs/_play_list.html.slim: -------------------------------------------------------------------------------- 1 | - if songs.any? 2 | - for song in songs 3 | .songs-list 4 | a data-xiami_id="#{song.s_id}" class="ui playBtn" 5 | i.play.icon 6 | = link_to song.title.html_safe, song 7 | .ui.hidden.divider 8 | - else 9 | .ui.center.aligned.basic.segment 10 | p =t('.no_songs_yet') 11 | p ...>_<... -------------------------------------------------------------------------------- /app/views/songs/_share_buttons.html.slim: -------------------------------------------------------------------------------- 1 | p.share-button 2 | a href="http://service.weibo.com/share/share.php?url=#{song_url(@song)}&type=3&pic=#{song.pic}&title=#{song.title}" target="_blank" 3 | i.weibo.red.icon 4 | a href="https://twitter.com/intent/tweet?url=#{song_url(song)}&text=#{song.title}&via=Ting" target="_blank" 5 | i.twitter.blue.icon 6 | a href="http://www.facebook.com/sharer.php?u=#{song_url(song)}" target="_blank" 7 | i.facebook.purple.icon -------------------------------------------------------------------------------- /app/views/songs/_song.html.slim: -------------------------------------------------------------------------------- 1 | - cache [song, index] do 2 | section.ui.two.column.grid.segment.stacked.songs-list id="song-#{song.id}" 3 | .five.wide.column 4 | - unless index 5 | .ui.left.red.corner.label 6 | i.heart.icon 7 | .album-pic 8 | .outer-pic.ui.circular.image 9 | img src="#{song.pic}" 10 | a data-xiami_id="#{song.s_id}" class="ui corner label playBtn" 11 | i.play.icon 12 | .eleven.wide.column.song-info 13 | - if current_user == song.user 14 | .operate 15 | = link_to edit_song_path(song), class: "edit-song" do 16 | i.green.pencil.icon 17 | = link_to song_path(song), data: { method: "delete", confirm: "#{t('form.are_you_sure')}", remote: true } do 18 | i.red.trash.icon 19 | .ui.right 20 | h3.header 21 | i.orange.sound.icon 22 | = link_to song.title.html_safe, song 23 | .singer 24 | | #{t('activerecord.attributes.song.artist')}: 25 | a = song.artist 26 | .song-content 27 | = parse_text(song.content) 28 | .song-footer 29 | .com-count 30 | - author = song_url(song) + "#comments" 31 | = link_to author do 32 | i.green.comment.icon 33 | = song.comments.size 34 | .musician 35 | = image_tag(song.user.avatar_url(:thumb), class: "ui avatar image") 36 | = link_to song.user.name, song.user -------------------------------------------------------------------------------- /app/views/songs/_songs.html.slim: -------------------------------------------------------------------------------- 1 | - cache [:songs, @songs] do 2 | - if index 3 | - if logged_in? 4 | - class_name = "modal-en" unless I18n.locale.to_s == "zh-CN" 5 | .ui.green.button.modal-toggle class="#{class_name}" =t('.share_a_song') 6 | .ui.modal.song-form 7 | i.close.red.icon 8 | .content 9 | = render 'songs/form', song: current_user.songs.new 10 | 11 | - if @recommend 12 | .ui.orange.message 13 | .ui.center.aligned.basic.segment =t('.recommend_songs') 14 | #songs data-image="#{asset_path("loading.gif")}" 15 | - for song in @songs 16 | = render 'song', song: song, index: index 17 | #pagination data-loaded="#{t('.loaded')}" 18 | = paginate @songs 19 | 20 | audio id="player" data-xiami_id="-1" -------------------------------------------------------------------------------- /app/views/songs/collect.html.slim: -------------------------------------------------------------------------------- 1 | = render 'songs', index: false -------------------------------------------------------------------------------- /app/views/songs/create.js.erb: -------------------------------------------------------------------------------- 1 | <% if @song.errors.empty? %> 2 | new_song = "<%= j render 'song', song: @song, index: true %>"; 3 | $('#songs').prepend(new_song); 4 | animated(); 5 | <% else %> 6 | $('#song-errors').html("<%= j render 'shared/error_messages', object: @song %>"); 7 | $('#error_explanation').transition('tada'); 8 | <% end %> 9 | $('.ui.form').removeClass('loading') 10 | $('.ui.form .button').removeClass('disabled') 11 | 12 | function animated() { 13 | $('#error_explanation').html(""); 14 | $('#song-<%= @song.id %>').addClass('animated flipInX'); 15 | $('#new_song').trigger('reset'); 16 | $('.close.red').click(); 17 | } -------------------------------------------------------------------------------- /app/views/songs/destroy.js.erb: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $('#song-<%= @song.id %>').addClass('animated flipOutX') 3 | .one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', 4 | function(){ 5 | $('#song-<%= @song.id %>').remove(); 6 | } 7 | ); 8 | }); -------------------------------------------------------------------------------- /app/views/songs/edit.html.slim: -------------------------------------------------------------------------------- 1 | .ui.grid 2 | .four.wide.column 3 | #song-edit.eight.wide.column 4 | = render 'form', song: @song -------------------------------------------------------------------------------- /app/views/songs/index.html.slim: -------------------------------------------------------------------------------- 1 | = render 'songs', index: true -------------------------------------------------------------------------------- /app/views/songs/new.html.slim: -------------------------------------------------------------------------------- 1 | .ui.grid 2 | .five.wide.column 3 | .six.wide.column 4 | = render 'form', song: @song -------------------------------------------------------------------------------- /app/views/songs/show.html.slim: -------------------------------------------------------------------------------- 1 | - id = @song.s_id 2 | - user = @song.user 3 | - cache @song do 4 | .ui.grid 5 | .four.wide.column 6 | .eight.wide.column 7 | .ui.one.column.grid.songs-list id="song-#{@song.id}" 8 | .sixteen.wide.column.song-info 9 | 10 | .song-container.ui.grid 11 | .ui.five.wide.column 12 | .album-pic 13 | .outer-pic.ui.circular.image.dimmer-image 14 | .ui.dimmer 15 | .content 16 | .center 17 | a.playBtn data-xiami_id="#{@song.s_id}" 18 | i.ui.red.huge.play.icon 19 | img src="#{@song.pic}" 20 | 21 | .ui.eleven.wide.column 22 | .song-panel 23 | h3.header 24 | i.orange.sound.icon 25 | = link_to @song.title.html_safe, @song 26 | p 27 | i.user.green.icon 28 | a = @song.artist 29 | p.song-toolbar 30 | = link_to user 31 | = image_tag(@song.user.avatar_url(:small), class: "ui mini-avatar avatar image run-popup", data: { content: "#{user.name}", position: "bottom center" } ) 32 | span.song-tools-divider • 33 | 34 | i.teal.calendar.icon 35 | = @song.created_at.strftime("%F") 36 | span.song-tools-divider • 37 | 38 | span.item id="like-song-#{@song.id}" 39 | - @likeable = Likeship.likeable(@song) 40 | = likeable_likes_tag @likeable 41 | 42 | - if current_user == user 43 | span.song-tools-divider • 44 | = link_to edit_song_path(@song) do 45 | i.green.icon.pencil 46 | span.song-tools-divider • 47 | 48 | = link_to song_path(@song), method: :delete, data: { confirm: "#{t('form.are_you_sure')}" } 49 | i.red.icon.trash 50 | = render 'share_buttons', song: @song 51 | 52 | .ui.sixteen.wide.column 53 | .song-content 54 | = parse_text(@song.content) 55 | .song-footer 56 | 57 | .ui.horizontal.icon.divider 58 | i.circular.red.chat.icon 59 | 60 | #comments 61 | .column 62 | .ui.comments 63 | - if @comments.any? 64 | = render @comments 65 | - else 66 | .ui.center.aligned.basic.segment =t('comments.no_comments_yet') 67 | 68 | = paginate @comments 69 | 70 | .ui.horizontal.icon.divider 71 | i.circular.green.pencil.icon 72 | - if logged_in? 73 | = render 'comments/form' 74 | - else 75 | .ui.center.aligned.basic.segment 76 | = link_to :login 77 | .ui.animated.fade.blue.button 78 | .visible.content =t('users.login_first') 79 | .hidden.content =t('users.login') 80 | audio id="player" data-xiami_id="-1" -------------------------------------------------------------------------------- /app/views/songs/update.js.erb: -------------------------------------------------------------------------------- 1 | <% if @song.errors.empty? %> 2 | Turbolinks.visit('<%= song_path(@song) %>') 3 | <% else %> 4 | $('#song-errors').html("<%= j render 'shared/error_messages', object: @song %>"); 5 | $('#error_explanation').transition('tada'); 6 | <% end %> -------------------------------------------------------------------------------- /app/views/user_mailer/activation_needed_email.html.erb: -------------------------------------------------------------------------------- 1 |

    2 | <%= t('mailer.activation.welcome') %>, <%= @user.name %> 3 |

    4 | 5 |

    6 | <%= t('mailer.activation.successfully_registered') %>, 7 | <%= t('mailer.activation.your_username') %>: <%= @user.name %>.
    8 | <%= t('mailer.activation.please_follow') %><%= link_to "#{t('mailer.activation.this_link')}", @url, { style: "color: #08c; text-decoration: none;" } %><%= t('mailer.activation.complete') %>.
    9 |

    10 | -------------------------------------------------------------------------------- /app/views/user_mailer/activation_needed_email.text.erb: -------------------------------------------------------------------------------- 1 | <%= t('mailer.activation.welcome') %>, <%= @user.name %> 2 | =============================================== 3 | 4 | <%= t('mailer.activation.successfully_registered') %>, 5 | <%= t('mailer.activation.your_username') %>: <%= @user.name %>. 6 | 7 | <%= t('mailer.activation.please_follow') %><%= t('mailer.activation.this_link') %><%= t('mailer.activation.complete') %>: <%= @url %> -------------------------------------------------------------------------------- /app/views/user_mailer/activation_success_email.html.erb: -------------------------------------------------------------------------------- 1 |

    2 | <%= t('mailer.activation.congratz') %>, <%= @user.name %> 3 |

    4 | 5 |

    6 | <%= t('mailer.subject.done') %>, 7 | <%= t('mailer.activation.your_username') %>: <%= @user.name %>.
    8 | <%= t('mailer.activation.you_can') %><%= link_to "#{t('mailer.activation.access')}", @url, { style: "color: #08c; text-decoration: none;" } %>, <%= t('mailer.activation.enjoy') %>!
    9 |

    -------------------------------------------------------------------------------- /app/views/user_mailer/activation_success_email.text.erb: -------------------------------------------------------------------------------- 1 | <%= t('mailer.activation.congratz') %>, <%= @user.name %> 2 | =============================================== 3 | 4 | <%= t('mailer.subject.done') %>, 5 | <%= t('mailer.activation.your_username') %>: <%= @user.name %>. 6 | <%= t('mailer.activation.you_can') %><%= t('mailer.activation.access') %>:<%= @url %>, <%= t('mailer.activation.enjoy') %>! 7 | -------------------------------------------------------------------------------- /app/views/user_mailer/reset_password_email.html.erb: -------------------------------------------------------------------------------- 1 |

    2 | <%= t('mailer.reset_password.header') %> 3 |

    4 | 5 |

    6 | <%= t('mailer.reset_password.body') %><%= link_to "#{t('mailer.activation.this_link')}", @url, { style: "color: #08c; text-decoration: none;" } %> 7 |

    -------------------------------------------------------------------------------- /app/views/user_mailer/reset_password_email.text.erb: -------------------------------------------------------------------------------- 1 | <%= t('mailer.reset_password.header') %> 2 | =============================================== 3 | <%= t('mailer.reset_password.body') %><%= t('mailer.activation.this_link') %>: <%= @url %> -------------------------------------------------------------------------------- /app/views/users/_user_panel.html.slim: -------------------------------------------------------------------------------- 1 | .ui.large.vertical.labeld.menu 2 | .ui.header.item.center.teal.label 3 | = link_to current_user 4 | = image_tag current_user.avatar_url(:large), class: "ui circular image thumb user-avatar run-popup", data: { content: "#{t('.check_profile')}", variation: "large"} 5 | = link_to notifications_path, class: "item" 6 | - count = current_user.notifications.unread.count 7 | - if count > 0 8 | #unread-count.ui.red.label = count 9 | - else 10 | #unread-count.ui.label = count 11 | | #{t('notifications.index.notifications')} 12 | = link_to songs_path, class: "item" 13 | i.home.green.icon 14 | | #{t('layouts.users_form.home')} 15 | = link_to :collect, class: "item" 16 | i.star.yellow.icon 17 | | #{t('.likes')} 18 | = link_to edit_user_path(current_user), class: "item" 19 | i.settings.blue.icon 20 | | #{t('.settings')} 21 | = link_to :logout, method: :delete, class: "item" 22 | i.sign.out.red.icon 23 | | #{t('users.logout')} -------------------------------------------------------------------------------- /app/views/users/create.js.erb: -------------------------------------------------------------------------------- 1 | $('.ui.form').removeClass('loading') 2 | <% if @user.errors.empty? %> 3 | Turbolinks.visit('<%= root_path %>'); 4 | <% else %> 5 | $('#signup-errors').html("<%= j render 'shared/error_messages', object: @user %>"); 6 | $('#error_explanation').transition('tada'); 7 | <% end %> -------------------------------------------------------------------------------- /app/views/users/edit.html.slim: -------------------------------------------------------------------------------- 1 | .ui.grid 2 | .six.wide.column 3 | .four.wide.column 4 | .ui.form 5 | #user-edit-errors 6 | = render 'shared/error_messages', object: @user 7 | = form_for @user, remote: true do |f| 8 | .ui.center.aligned.field.basic.segment 9 | = image_tag(current_user.avatar_url(:large), class: "ui image circular thumb user-avatar user-avatar-upload run-popup", data: { content: "#{t('.upload_avatar')}", position: "top center" }) 10 | = f.file_field :avatar, class: "upload-avatar" 11 | .inline.fields.ui.center.aligned.basic.segment 12 | - I18n.available_locales.each do |locale| 13 | .field 14 | .ui.radio.checkbox.checked 15 | = f.radio_button(:locale, "#{locale}") 16 | = f.label t("locales.#{locale}") 17 | .field 18 | = f.text_field :email, placeholder: "#{t('activerecord.attributes.user.email')}", disabled: "disabled" 19 | .field 20 | = f.text_field :bio, placeholder: "#{t('activerecord.attributes.user.bio')}" 21 | .ui.buttons.session-buttons 22 | = link_to "#{t('.my_page')}", user_path(@user), class: "ui button orange" 23 | .or 24 | = f.submit "#{t('form.save')}", class: "ui blue button" -------------------------------------------------------------------------------- /app/views/users/favorite_songs.js.erb: -------------------------------------------------------------------------------- 1 | $('.status-panel').html("<%= j render 'songs/play_list', songs: @songs %>") 2 | -------------------------------------------------------------------------------- /app/views/users/new.html.slim: -------------------------------------------------------------------------------- 1 | .signup-form 2 | #signup-errors 3 | = render 'shared/error_messages', object: @user 4 | = form_for @user, remote: true do |f| 5 | .ui.form 6 | .field 7 | .ui.left.icon.large.input 8 | = f.text_field :name, placeholder: "#{t('activerecord.attributes.user.name')}" 9 | i.user.icon 10 | .field 11 | .ui.left.icon.large.input 12 | = f.text_field :email, placeholder: "#{t('activerecord.attributes.user.email')}" 13 | i.mail.icon 14 | .field 15 | .ui.left.icon.large.input 16 | = f.password_field :password, placeholder: "#{t('activerecord.attributes.user.password')}" 17 | i.lock.icon 18 | .field 19 | .ui.left.icon.large.input 20 | = f.password_field :password_confirmation, placeholder: "#{t('activerecord.attributes.user.password_confirmation')}" 21 | i.lock.icon 22 | = f.submit "#{t('users.signup')}", class: "ui fluid green large button", data: { disable_with: "#{t('form.submitting')}" } 23 | -------------------------------------------------------------------------------- /app/views/users/recent_comments.js.erb: -------------------------------------------------------------------------------- 1 | $('.status-panel').html("<%= j render 'comments/recent_comments', comments: @comments %>") -------------------------------------------------------------------------------- /app/views/users/show.html.slim: -------------------------------------------------------------------------------- 1 | #canvas-bg 2 | canvas#particles 3 | .ui.grid 4 | .three.wide.column 5 | .ten.wide.column 6 | #user-show.ui.grid 7 | .one.wide.column 8 | .fourteen.wide.column 9 | .ui.grid 10 | .one.wide.column 11 | .ui.five.wide.column.center.aligned.info-column 12 | = image_tag(@user.avatar_url(:large), class: "ui image circular lg run-popup") 13 | .center 14 | h3 = @user.name 15 | .center.bio 16 | = @user.bio 17 | .ui.divider 18 | - class_name = "ui left aligned basic segment" unless I18n.locale.to_s == "zh-CN" 19 | .user-status class="#{class_name}" 20 | .user-created-at 21 | p #{t('.created_at')}: #{time_ago_in_words(@user.created_at)} #{t('.ago')} 22 | .user-songs-count 23 | p #{t('.total_songs')}: #{t('.songs_count', count: @user.songs.count)} 24 | .user-comments-count 25 | p #{t('.total_comments')}: #{t('.comments_count', count: @user.comments.count)} 26 | .nine.wide.column.content-column 27 | .ui.secondary.large.menu data-loading="#{t('.loading')}" 28 | = link_to "#{t('.songs')}", user_songs_url+"?id=#{@user.name}", remote: true, class: "active item" 29 | = link_to "#{t('users.user_panel.likes')}", favorite_url+"?id=#{@user.name}", remote: true, class: "item" 30 | = link_to "#{t('comments.comments')}", recent_comments_url+"?id=#{@user.name}", remote: true, class: "item" 31 | .status-panel 32 | = render 'songs/play_list', songs: @songs 33 | 34 | audio id="player" data-xiami_id="-1" 35 | 36 | javascript: 37 | $(function() { 38 | if($("#particles").size()>0){ 39 | (function() { 40 | var width, height, largeHeader, canvas, ctx, circles, target, animateHeader = true; 41 | width = $('#canvas-bg').width(); 42 | height= $('#canvas-bg').height(); 43 | target = {x: 0, y: height}; 44 | largeHeader = document.getElementById('canvas-bg'); 45 | largeHeader.style.height = height+'px'; 46 | canvas = document.getElementById('particles'); 47 | canvas.width = width; 48 | canvas.height = height; 49 | ctx = canvas.getContext('2d'); 50 | 51 | circles = []; 52 | for(var x = 0; x < width*0.5; x++) { 53 | var c = new Circle(); 54 | circles.push(c); 55 | } 56 | animate(); 57 | window.addEventListener('resize',function(){ 58 | width = window.innerWidth; 59 | canvas.width = width; 60 | }); 61 | function animate() { 62 | if(animateHeader) { 63 | ctx.clearRect(0,0,width,height); 64 | for(var i in circles) { 65 | circles[i].draw(); 66 | } 67 | } 68 | requestAnimationFrame(animate); 69 | } 70 | function Circle() { 71 | var _this = this; 72 | 73 | (function() { 74 | _this.pos = {}; 75 | init(); 76 | })(); 77 | 78 | function init() { 79 | _this.pos.x = Math.random()*width; 80 | _this.pos.y = height+Math.random()*100; 81 | _this.alpha = 0.1+Math.random()*0.3; 82 | _this.scale = 0.1+Math.random()*0.3; 83 | _this.velocity = Math.random(); 84 | } 85 | 86 | this.draw = function() { 87 | if(_this.alpha <= 0) { 88 | init(); 89 | } 90 | 91 | _this.pos.x -=0.1; 92 | _this.pos.y -= _this.velocity; 93 | _this.alpha -= 0.0005; 94 | ctx.beginPath(); 95 | ctx.arc(_this.pos.x, _this.pos.y, _this.scale*10, 0, 2 * Math.PI, false); 96 | ctx.fillStyle = 'rgba(255,255,255,'+ _this.alpha+')'; 97 | ctx.fill(); 98 | }; 99 | } 100 | })(); 101 | }}) -------------------------------------------------------------------------------- /app/views/users/update.js.erb: -------------------------------------------------------------------------------- 1 | <% if @user.errors.empty? %> 2 | Turbolinks.visit('<%= user_path(@user) %>'); 3 | <% else %> 4 | $('#user-edit-errors').html("<%= j render 'shared/error_messages', object: @user %>") 5 | $('#error_explanation').transition('tada'); 6 | <% end %> -------------------------------------------------------------------------------- /app/views/users/user_songs.js.erb: -------------------------------------------------------------------------------- 1 | $('.status-panel').html("<%= j render 'songs/play_list', songs: @songs %>") -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Ting 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | config.i18n.default_locale = "en" 22 | config.i18n.available_locales = [:en, 'zh-CN'] 23 | I18n.enforce_available_locales = false 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = true 15 | config.cache_store = [:dalli_store,"127.0.0.1", { namespace: "ting", compress: true }] 16 | 17 | # Don't care if the mailer can't send. 18 | config.action_mailer.raise_delivery_errors = false 19 | 20 | # Print deprecation notices to the Rails logger. 21 | config.active_support.deprecation = :log 22 | 23 | # Raise an error on page load if there are pending migrations. 24 | config.active_record.migration_error = :page_load 25 | 26 | # Debug mode disables concatenation and preprocessing of assets. 27 | # This option may cause significant delays in view rendering with a large 28 | # number of complex assets. 29 | config.assets.debug = true 30 | 31 | # Adds additional error checking when serving assets at runtime. 32 | # Checks for improperly declared sprockets dependencies. 33 | # Raises helpful error messages. 34 | config.assets.raise_runtime_errors = true 35 | config.action_mailer.default_url_options = { host: 'localhost:3000' } 36 | config.action_mailer.delivery_method = :smtp 37 | config.action_mailer.smtp_settings = { 38 | address: 'smtp.gmail.com', 39 | port: 587, 40 | domain: 'gmail.com', 41 | user_name: 'your_email', 42 | password: 'your_email_password', 43 | authentication: 'plain', 44 | enable_starttls_auto: true 45 | } 46 | # Raises error for missing translations 47 | # config.action_view.raise_on_missing_translations = true 48 | end 49 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 40 | 41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 42 | # config.force_ssl = true 43 | 44 | # Set to :debug to see everything in the log. 45 | config.log_level = :info 46 | 47 | # Prepend all log lines with the following tags. 48 | # config.log_tags = [ :subdomain, :uuid ] 49 | 50 | # Use a different logger for distributed setups. 51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 52 | 53 | # Use a different cache store in production. 54 | config.cache_store = :dalli_store 55 | 56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 57 | # config.action_controller.asset_host = "http://assets.example.com" 58 | 59 | # Ignore bad email addresses and do not raise email delivery errors. 60 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 61 | # config.action_mailer.raise_delivery_errors = false 62 | 63 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 64 | # the I18n.default_locale when a translation cannot be found). 65 | config.i18n.fallbacks = true 66 | 67 | # Send deprecation notices to registered listeners. 68 | config.active_support.deprecation = :notify 69 | 70 | # Disable automatic flushing of the log to improve performance. 71 | # config.autoflush_log = false 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | config.assets.raise_runtime_errors = true 79 | config.action_mailer.default_url_options = { host: 'tinger.heroku.com' } 80 | config.action_mailer.delivery_method = :smtp 81 | config.action_mailer.smtp_settings = { 82 | address: 'smtp.gmail.com', 83 | port: 587, 84 | domain: 'gmail.com', 85 | user_name: 'your_email', 86 | password: 'your_email_password', 87 | authentication: 'plain', 88 | enable_starttls_auto: true 89 | } 90 | end 91 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | config.active_support.test_order = :sorted 38 | 39 | config.active_record.raise_in_transactional_callbacks = true 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Precompile additional assets. 7 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 8 | # Rails.application.config.assets.precompile += %w( search.js ) 9 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/kaminari_config.rb: -------------------------------------------------------------------------------- 1 | Kaminari.configure do |config| 2 | config.default_per_page = 10 3 | # config.max_per_page = nil 4 | config.window = 1 5 | config.outer_window = 1 6 | # config.left = 0 7 | # config.right = 0 8 | # config.page_method_name = :page 9 | # config.param_name = :page 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_ting_session' 4 | -------------------------------------------------------------------------------- /config/initializers/sorcery.rb: -------------------------------------------------------------------------------- 1 | # The first thing you need to configure is which modules you need in your app. 2 | # The default is nothing which will include only core features (password encryption, login/logout). 3 | # Available submodules are: :user_activation, :http_basic_auth, :remember_me, 4 | # :reset_password, :session_timeout, :brute_force_protection, :activity_logging, :external 5 | Rails.application.config.sorcery.submodules = [:remember_me, :reset_password, :user_activation] 6 | 7 | # Here you can configure each submodule's features. 8 | Rails.application.config.sorcery.configure do |config| 9 | # -- core -- 10 | # What controller action to call for non-authenticated users. You can also 11 | # override the 'not_authenticated' method of course. 12 | # Default: `:not_authenticated` 13 | # 14 | # config.not_authenticated_action = 15 | 16 | 17 | # When a non logged in user tries to enter a page that requires login, save 18 | # the URL he wanted to reach, and send him there after login, using 'redirect_back_or_to'. 19 | # Default: `true` 20 | # 21 | # config.save_return_to_url = 22 | 23 | 24 | # Set domain option for cookies; Useful for remember_me submodule. 25 | # Default: `nil` 26 | # 27 | # config.cookie_domain = 28 | 29 | 30 | # -- session timeout -- 31 | # How long in seconds to keep the session alive. 32 | # Default: `3600` 33 | # 34 | # config.session_timeout = 35 | 36 | 37 | # Use the last action as the beginning of session timeout. 38 | # Default: `false` 39 | # 40 | # config.session_timeout_from_last_action = 41 | 42 | 43 | # -- http_basic_auth -- 44 | # What realm to display for which controller name. For example {"My App" => "Application"} 45 | # Default: `{"application" => "Application"}` 46 | # 47 | # config.controller_to_realm_map = 48 | 49 | 50 | # -- activity logging -- 51 | # will register the time of last user login, every login. 52 | # Default: `true` 53 | # 54 | # config.register_login_time = 55 | 56 | 57 | # will register the time of last user logout, every logout. 58 | # Default: `true` 59 | # 60 | # config.register_logout_time = 61 | 62 | 63 | # will register the time of last user action, every action. 64 | # Default: `true` 65 | # 66 | # config.register_last_activity_time = 67 | 68 | 69 | # -- external -- 70 | # What providers are supported by this app, i.e. [:twitter, :facebook, :github, :linkedin, :xing, :google, :liveid] . 71 | # Default: `[]` 72 | # 73 | # config.external_providers = 74 | 75 | 76 | # You can change it by your local ca_file. i.e. '/etc/pki/tls/certs/ca-bundle.crt' 77 | # Path to ca_file. By default use a internal ca-bundle.crt. 78 | # Default: `'path/to/ca_file'` 79 | # 80 | # config.ca_file = 81 | 82 | 83 | # For information about LinkedIn API: 84 | # - user info fields go to https://developer.linkedin.com/documents/profile-fields 85 | # - access permissions go to https://developer.linkedin.com/documents/authentication#granting 86 | # 87 | # config.linkedin.key = "" 88 | # config.linkedin.secret = "" 89 | # config.linkedin.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=linkedin" 90 | # config.linkedin.user_info_fields = ['first-name', 'last-name'] 91 | # config.linkedin.user_info_mapping = {first_name: "firstName", last_name: "lastName"} 92 | # config.linkedin.access_permissions = ['r_basicprofile'] 93 | # 94 | # 95 | # For information about XING API: 96 | # - user info fields go to https://dev.xing.com/docs/get/users/me 97 | # 98 | # config.xing.key = "" 99 | # config.xing.secret = "" 100 | # config.xing.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=xing" 101 | # config.xing.user_info_mapping = {first_name: "first_name", last_name: "last_name"} 102 | # 103 | # 104 | # Twitter wil not accept any requests nor redirect uri containing localhost, 105 | # make sure you use 0.0.0.0:3000 to access your app in development 106 | # 107 | # config.twitter.key = "" 108 | # config.twitter.secret = "" 109 | # config.twitter.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=twitter" 110 | # config.twitter.user_info_mapping = {:email => "screen_name"} 111 | # 112 | # config.facebook.key = "" 113 | # config.facebook.secret = "" 114 | # config.facebook.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=facebook" 115 | # config.facebook.user_info_mapping = {:email => "name"} 116 | # config.facebook.access_permissions = ["email", "publish_stream"] 117 | # 118 | # config.github.key = "" 119 | # config.github.secret = "" 120 | # config.github.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=github" 121 | # config.github.user_info_mapping = {:email => "name"} 122 | # 123 | # config.google.key = "" 124 | # config.google.secret = "" 125 | # config.google.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=google" 126 | # config.google.user_info_mapping = {:email => "email", :username => "name"} 127 | # 128 | # config.vk.key = "" 129 | # config.vk.secret = "" 130 | # config.vk.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=vk" 131 | # config.vk.user_info_mapping = {:login => "domain", :name => "full_name"} 132 | # 133 | # To use liveid in development mode you have to replace mydomain.com with 134 | # a valid domain even in development. To use a valid domain in development 135 | # simply add your domain in your /etc/hosts file in front of 127.0.0.1 136 | # 137 | # config.liveid.key = "" 138 | # config.liveid.secret = "" 139 | # config.liveid.callback_url = "http://mydomain.com:3000/oauth/callback?provider=liveid" 140 | # config.liveid.user_info_mapping = {:username => "name"} 141 | 142 | 143 | # --- user config --- 144 | config.user_config do |user| 145 | # -- core -- 146 | # specify username attributes, for example: [:username, :email]. 147 | # Default: `[:email]` 148 | # 149 | user.username_attribute_names = [:email] 150 | 151 | 152 | # change *virtual* password attribute, the one which is used until an encrypted one is generated. 153 | # Default: `:password` 154 | # 155 | # user.password_attribute_name = 156 | 157 | 158 | # downcase the username before trying to authenticate, default is false 159 | # Default: `false` 160 | # 161 | # user.downcase_username_before_authenticating = 162 | 163 | 164 | # change default email attribute. 165 | # Default: `:email` 166 | # 167 | # user.email_attribute_name = 168 | 169 | 170 | # change default crypted_password attribute. 171 | # Default: `:crypted_password` 172 | # 173 | # user.crypted_password_attribute_name = 174 | 175 | 176 | # what pattern to use to join the password with the salt 177 | # Default: `""` 178 | # 179 | # user.salt_join_token = 180 | 181 | 182 | # change default salt attribute. 183 | # Default: `:salt` 184 | # 185 | # user.salt_attribute_name = 186 | 187 | 188 | # how many times to apply encryption to the password. 189 | # Default: `nil` 190 | # 191 | # user.stretches = 192 | 193 | 194 | # encryption key used to encrypt reversible encryptions such as AES256. 195 | # WARNING: If used for users' passwords, changing this key will leave passwords undecryptable! 196 | # Default: `nil` 197 | # 198 | # user.encryption_key = 199 | 200 | 201 | # use an external encryption class. 202 | # Default: `nil` 203 | # 204 | # user.custom_encryption_provider = 205 | 206 | 207 | # encryption algorithm name. See 'encryption_algorithm=' for available options. 208 | # Default: `:bcrypt` 209 | # 210 | # user.encryption_algorithm = 211 | 212 | 213 | # make this configuration inheritable for subclasses. Useful for ActiveRecord's STI. 214 | # Default: `false` 215 | # 216 | # user.subclasses_inherit_config = 217 | 218 | 219 | # -- remember_me -- 220 | # allow the remember_me cookie to settable through AJAX 221 | # Default: `true` 222 | # 223 | # user.remember_me_httponly = 224 | 225 | # How long in seconds the session length will be 226 | # Default: `604800` 227 | # 228 | # user.remember_me_for = 229 | 230 | 231 | # -- user_activation -- 232 | # the attribute name to hold activation state (active/pending). 233 | # Default: `:activation_state` 234 | # 235 | # user.activation_state_attribute_name = 236 | 237 | 238 | # the attribute name to hold activation code (sent by email). 239 | # Default: `:activation_token` 240 | # 241 | # user.activation_token_attribute_name = 242 | 243 | 244 | # the attribute name to hold activation code expiration date. 245 | # Default: `:activation_token_expires_at` 246 | # 247 | # user.activation_token_expires_at_attribute_name = 248 | 249 | 250 | # how many seconds before the activation code expires. nil for never expires. 251 | # Default: `nil` 252 | # 253 | # user.activation_token_expiration_period = 254 | 255 | 256 | # your mailer class. Required. 257 | # Default: `nil` 258 | # 259 | user.user_activation_mailer = UserMailer 260 | 261 | 262 | # when true sorcery will not automatically 263 | # email activation details and allow you to 264 | # manually handle how and when email is sent. 265 | # Default: `false` 266 | # 267 | # user.activation_mailer_disabled = 268 | 269 | 270 | # activation needed email method on your mailer class. 271 | # Default: `:activation_needed_email` 272 | # 273 | # user.activation_needed_email_method_name = 274 | 275 | 276 | # activation success email method on your mailer class. 277 | # Default: `:activation_success_email` 278 | # 279 | # user.activation_success_email_method_name = 280 | 281 | 282 | # do you want to prevent or allow users that did not activate by email to login? 283 | # Default: `true` 284 | # 285 | # user.prevent_non_active_users_to_login = 286 | 287 | 288 | # -- reset_password -- 289 | # reset password code attribute name. 290 | # Default: `:reset_password_token` 291 | # 292 | # user.reset_password_token_attribute_name = 293 | 294 | 295 | # expires at attribute name. 296 | # Default: `:reset_password_token_expires_at` 297 | # 298 | # user.reset_password_token_expires_at_attribute_name = 299 | 300 | 301 | # when was email sent, used for hammering protection. 302 | # Default: `:reset_password_email_sent_at` 303 | # 304 | # user.reset_password_email_sent_at_attribute_name = 305 | 306 | 307 | # mailer class. Needed. 308 | # Default: `nil` 309 | # 310 | user.reset_password_mailer = UserMailer 311 | 312 | 313 | # reset password email method on your mailer class. 314 | # Default: `:reset_password_email` 315 | # 316 | # user.reset_password_email_method_name = 317 | 318 | 319 | # when true sorcery will not automatically 320 | # email password reset details and allow you to 321 | # manually handle how and when email is sent 322 | # Default: `false` 323 | # 324 | # user.reset_password_mailer_disabled = 325 | 326 | 327 | # how many seconds before the reset request expires. nil for never expires. 328 | # Default: `nil` 329 | # 330 | # user.reset_password_expiration_period = 331 | 332 | 333 | # hammering protection, how long to wait before allowing another email to be sent. 334 | # Default: `5 * 60` 335 | # 336 | # user.reset_password_time_between_emails = 337 | 338 | 339 | # -- brute_force_protection -- 340 | # Failed logins attribute name. 341 | # Default: `:failed_logins_count` 342 | # 343 | # user.failed_logins_count_attribute_name = 344 | 345 | 346 | # This field indicates whether user is banned and when it will be active again. 347 | # Default: `:lock_expires_at` 348 | # 349 | # user.lock_expires_at_attribute_name = 350 | 351 | 352 | # How many failed logins allowed. 353 | # Default: `50` 354 | # 355 | # user.consecutive_login_retries_amount_limit = 356 | 357 | 358 | # How long the user should be banned. in seconds. 0 for permanent. 359 | # Default: `60 * 60` 360 | # 361 | # user.login_lock_time_period = 362 | 363 | # Unlock token attribute name 364 | # Default: `:unlock_token` 365 | # 366 | # user.unlock_token_attribute_name = 367 | 368 | # Unlock token mailer method 369 | # Default: `:send_unlock_token_email` 370 | # 371 | # user.unlock_token_email_method_name = 372 | 373 | # when true sorcery will not automatically 374 | # send email with unlock token 375 | # Default: `false` 376 | # 377 | # user.unlock_token_mailer_disabled = true 378 | 379 | # Unlock token mailer class 380 | # Default: `nil` 381 | # 382 | # user.unlock_token_mailer = UserMailer 383 | 384 | # -- activity logging -- 385 | # Last login attribute name. 386 | # Default: `:last_login_at` 387 | # 388 | # user.last_login_at_attribute_name = 389 | 390 | 391 | # Last logout attribute name. 392 | # Default: `:last_logout_at` 393 | # 394 | # user.last_logout_at_attribute_name = 395 | 396 | 397 | # Last activity attribute name. 398 | # Default: `:last_activity_at` 399 | # 400 | # user.last_activity_at_attribute_name = 401 | 402 | 403 | # How long since last activity is he user defined logged out? 404 | # Default: `10 * 60` 405 | # 406 | # user.activity_timeout = 407 | 408 | 409 | # -- external -- 410 | # Class which holds the various external provider data for this user. 411 | # Default: `nil` 412 | # 413 | # user.authentications_class = 414 | 415 | 416 | # User's identifier in authentications class. 417 | # Default: `:user_id` 418 | # 419 | # user.authentications_user_id_attribute_name = 420 | 421 | 422 | # Provider's identifier in authentications class. 423 | # Default: `:provider` 424 | # 425 | # user.provider_attribute_name = 426 | 427 | 428 | # User's external unique identifier in authentications class. 429 | # Default: `:uid` 430 | # 431 | # user.provider_uid_attribute_name = 432 | end 433 | 434 | # This line must come after the 'user config' block. 435 | # Define which model authenticates with sorcery. 436 | config.user_class = "User" 437 | end 438 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | locales: 3 | en: "English" 4 | zh-CN: "简体中文" 5 | layouts: 6 | users_form: 7 | home: Home 8 | navbar: 9 | total_users: Total users 10 | users: 11 | signup: Sign up 12 | login: Login 13 | logout: Logout 14 | login_first: Login First 15 | has_already_logged_in: User has already logged in 16 | user_panel: 17 | likes: Likes 18 | settings: Settings 19 | check_profile: Check your profile 20 | edit: 21 | upload_avatar: Click and upload avatar 22 | my_page: My Page 23 | show: 24 | loading: Loading... 25 | created_at: Created at 26 | songs: Songs 27 | total_songs: Total Songs 28 | total_comments: Total Comments 29 | ago: ago 30 | users_count: "%{count}" 31 | songs_count: "%{count}" 32 | comments_count: "%{count}" 33 | no_user: The user does not exist 34 | create: 35 | successfully: Register successfully 36 | update: 37 | successfully: Profile is successfully saved 38 | faild: Profile save faild 39 | activate: 40 | activated: User was successfully activated 41 | sessions: 42 | new: 43 | remember_me: Remember me 44 | forget_password: Forget password 45 | invalid_email_password_combination: Invalid Email Password combination 46 | inactivation: User is inactivation 47 | create: 48 | successfully: Logged in successfully 49 | destroy: 50 | successfully: Logout successfully 51 | songs: 52 | form: 53 | title: Titile 54 | singer: Singer(Auto fetch) 55 | content: Content 56 | cant_be_blank: can't be blank 57 | passed: "Check passed, you can publish the song after filling the content" 58 | play_list: 59 | no_songs_yet: No songs yet 60 | songs: 61 | share_a_song: Share a song 62 | recommend_songs: Recommend songs 63 | loaded: All songs is loaded 64 | create: 65 | success: Publishing success 66 | faild: Publishing faild 67 | update: 68 | successfully: Successfully updated 69 | destroy: 70 | successfully: Song was successfully deleted 71 | cant_fetch: The information of song can't be fetch 72 | fetch_faild: " can't be fetch" 73 | comments: 74 | comments: Comments 75 | no_comments_yet: No comments yet 76 | notifications: 77 | index: 78 | notifications: Notifications 79 | empty_notifications: Empty notifications 80 | clearing: Clearing... 81 | notification: 82 | comment: 83 | add_a_comment_at: add a comment at 84 | mention: 85 | mention_you_at: mention you at 86 | password_resets: 87 | create: 88 | hint: You will receive an email with instructions about how to reset your password in a few minutes 89 | update: 90 | successfully_updated_password: Successfully updated password 91 | form: 92 | submit: Submit 93 | submitting: Submitting... 94 | save: Save 95 | are_you_sure: Are you sure? 96 | reset_password: Reset password 97 | mailer: 98 | subject: 99 | welcome: Welcome to the Ting 100 | reset_password: Reset Password 101 | done: Your account is confirmed 102 | activation: 103 | welcome: Welcome 104 | successfully_registered: You are successfully registered 105 | your_username: Your username is 106 | please_follow: Please follow 107 | this_link: " this link " 108 | complete: to complete the confirmation 109 | congratz: Congratulations 110 | you_can: Now you can 111 | access: " access to the site" 112 | enjoy: Enjoy it 113 | reset_password: 114 | header: "Hello, You have requested to reset your password" 115 | body: "To choose a new password, just follow" 116 | shared: 117 | error_messages: 118 | the_form_contains: The form contains 119 | permission_denied: Permission denied 120 | activerecord: 121 | errors: 122 | models: 123 | song: 124 | attributes: 125 | title: 126 | fetch: " can't be fetch" 127 | artist: 128 | fetch: " can't be fetch" 129 | attributes: 130 | user: 131 | name: "Name" 132 | email: "Email" 133 | password: "Password" 134 | password_confirmation: "Password confirmation" 135 | bio: "Bio" 136 | song: 137 | s_id: "Xiami ID " 138 | title: "Song title" 139 | artist: "Singer" 140 | content: "Content" 141 | pic: "Picture link" 142 | comment: 143 | content: "Content" -------------------------------------------------------------------------------- /config/locales/kaminari.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | views: 3 | pagination: 4 | first: "« First" 5 | last: "Last »" 6 | previous: "‹ Prev" 7 | next: "Next ›" 8 | truncate: "…" 9 | helpers: 10 | page_entries_info: 11 | one_page: 12 | display_entries: 13 | zero: "No %{entry_name} found" 14 | one: "Displaying 1 %{entry_name}" 15 | other: "Displaying all %{count} %{entry_name}" 16 | more_pages: 17 | display_entries: "Displaying %{entry_name} %{first} - %{last} of %{total} in total" -------------------------------------------------------------------------------- /config/locales/kaminari.zh-CN.yml: -------------------------------------------------------------------------------- 1 | zh-CN: 2 | views: 3 | pagination: 4 | first: "« 第一页" 5 | last: "最后一页 »" 6 | previous: "‹ 前一页" 7 | next: "下一页 ›" 8 | truncate: "…" 9 | helpers: 10 | page_entries_info: 11 | one_page: 12 | display_entries: 13 | zero: "%{entry_name} 未找到" 14 | one: "displaying 1 %{entry_name}" 15 | other: "displaying all %{count} %{entry_name}" 16 | more_pages: 17 | display_entries: "displaying %{entry_name} %{first} - %{last} of %{total} in total" -------------------------------------------------------------------------------- /config/locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | zh-CN: 2 | locales: 3 | en: "English" 4 | zh-CN: "简体中文" 5 | layouts: 6 | users_form: 7 | home: 主页 8 | navbar: 9 | total_users: 注册用户 10 | users: 11 | signup: 注册 12 | login: 登录 13 | logout: 退出 14 | login_first: 请先登录 15 | has_already_logged_in: 用户已经登录 16 | user_panel: 17 | likes: 喜欢 18 | settings: 设置 19 | check_profile: 查看个人信息 20 | edit: 21 | upload_avatar: 点击上传头像 22 | my_page: 个人主页 23 | show: 24 | loading: 加载中... 25 | created_at: 创建时间 26 | songs: 歌曲 27 | total_songs: 分享歌曲 28 | total_comments: 发表评论 29 | ago: "" 30 | users_count: "%{count} 位" 31 | songs_count: "%{count} 首" 32 | comments_count: "%{count} 条" 33 | no_user: 该用户不存在 34 | create: 35 | successfully: 注册成功 36 | update: 37 | successfully: 资料保存成功 38 | faild: 资料保存失败 39 | activate: 40 | activated: 验证成功 41 | sessions: 42 | new: 43 | remember_me: 记住我 44 | forget_password: 忘记密码 45 | invalid_email_password_combination: 邮箱或密码无效 46 | inactivation: 用户尚未通过验证 47 | create: 48 | successfully: 登录成功 49 | destroy: 50 | successfully: 成功退出 51 | songs: 52 | form: 53 | title: 歌曲名 54 | singer: 歌手信息(自动抓取) 55 | content: 分享内容 56 | cant_be_blank: 不能为空 57 | passed: "检测通过, 填写分享内容后即可发布" 58 | play_list: 59 | no_songs_yet: 没有相关歌曲 60 | songs: 61 | share_a_song: 分享歌曲 62 | recommend_songs: "你还没有喜欢过任何歌曲, 下面歌曲由系统自动推荐" 63 | loaded: 全部歌曲加载完毕 64 | create: 65 | success: 成功发布 66 | faild: 发布失败 67 | update: 68 | successfully: 更新成功 69 | destroy: 70 | successfully: 删除成功 71 | cant_fetch: 未获取到该歌曲的相关信息 72 | fetch_faild: 抓取失败 73 | comments: 74 | comments: 所有评论 75 | no_comments_yet: 暂无评论 76 | notifications: 77 | index: 78 | notifications: 通知 79 | empty_notifications: 清空通知 80 | clearing: 正在清空... 81 | notification: 82 | comment: 83 | add_a_comment_at: 添加评论于 84 | mention: 85 | mention_you_at: 提及您于 86 | password_resets: 87 | create: 88 | hint: 您将在几分钟内收到重置密码的指令 89 | update: 90 | successfully_updated_password: 密码更新成功 91 | form: 92 | submit: 提交 93 | submitting: 提交中... 94 | save: 保存 95 | are_you_sure: 确认删除? 96 | reset_password: 重置密码 97 | mailer: 98 | subject: 99 | welcome: 欢迎来到 Ting 100 | reset_password: 重置密码 101 | done: 你的账号已经完成激活 102 | activation: 103 | welcome: 欢迎您 104 | successfully_registered: 您已经成功注册 105 | your_username: 您的用户名是 106 | please_follow: 请使用 107 | this_link: 该链接 108 | complete: 完成验证 109 | congratz: 恭喜您 110 | you_can: 你现在可以 111 | access: 进入网站 112 | enjoy: 尽情享受音乐带来的美妙 113 | reset_password: 114 | header: "您好, 您正在请求重置密码" 115 | body: "重置密码请使用" 116 | shared: 117 | error_messages: 118 | the_form_contains: 表单包含 119 | permission_denied: 无权访问 120 | activerecord: 121 | errors: 122 | models: 123 | song: 124 | attributes: 125 | title: 126 | fetch: 抓取失败 127 | artist: 128 | fetch: 抓取失败 129 | attributes: 130 | user: 131 | name: 名称 132 | email: 邮箱 133 | password: 密码 134 | password_confirmation: 确认密码 135 | bio: 个人说明 136 | song: 137 | s_id: "虾米 ID " 138 | title: 歌曲名称 139 | artist: 歌手 140 | content: 分享内容 141 | pic: 图片链接 142 | comment: 143 | content: 评论内容 -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | root 'songs#index' 4 | resources :sessions, only: [ :new, :create, :destroy ] 5 | resources :users, only: [ :new, :create, :update, :edit, :show ] do 6 | member do 7 | get :activate 8 | end 9 | end 10 | resources :songs do 11 | resources :comments, only: [ :create, :destroy ] 12 | end 13 | resource :likeship, only: [ :create, :destroy ] 14 | resources :notifications, only: [ :index, :destroy ] do 15 | collection do 16 | post :count 17 | delete :clear 18 | end 19 | end 20 | resources :password_resets, only: [ :new, :create, :edit, :update] 21 | get 'login', to: 'sessions#new', as: :login 22 | get 'signup', to: 'users#new', as: :signup 23 | delete 'logout', to: 'sessions#destroy', as: :logout 24 | get 'collect', to: 'songs#collect', as: :collect 25 | get 'user_songs', to: 'users#user_songs', as: :user_songs 26 | get 'favorite_songs', to: 'users#favorite_songs', as: :favorite 27 | get 'recent_comments', to: 'users#recent_comments', as: :recent_comments 28 | get 'language', to: 'users#language', as: :set_locale 29 | # The priority is based upon order of creation: first created -> highest priority. 30 | # See how all your routes lay out with "rake routes". 31 | 32 | # You can have the root of your site routed with "root" 33 | # root 'welcome#index' 34 | 35 | # Example of regular route: 36 | # get 'products/:id' => 'catalog#view' 37 | 38 | # Example of named route that can be invoked with purchase_url(id: product.id) 39 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 40 | 41 | # Example resource route (maps HTTP verbs to controller actions automatically): 42 | # resources :products 43 | 44 | # Example resource route with options: 45 | # resources :products do 46 | # member do 47 | # get 'short' 48 | # post 'toggle' 49 | # end 50 | # 51 | # collection do 52 | # get 'sold' 53 | # end 54 | # end 55 | 56 | # Example resource route with sub-resources: 57 | # resources :products do 58 | # resources :comments, :sales 59 | # resource :seller 60 | # end 61 | 62 | # Example resource route with more complex sub-resources: 63 | # resources :products do 64 | # resources :comments 65 | # resources :sales do 66 | # get 'recent', on: :collection 67 | # end 68 | # end 69 | 70 | # Example resource route with concerns: 71 | # concern :toggleable do 72 | # post 'toggle' 73 | # end 74 | # resources :posts, concerns: :toggleable 75 | # resources :photos, concerns: :toggleable 76 | 77 | # Example resource route within a namespace: 78 | # namespace :admin do 79 | # # Directs /admin/products/* to Admin::ProductsController 80 | # # (app/controllers/admin/products_controller.rb) 81 | # resources :products 82 | # end 83 | end 84 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: fad025dab989623db1cee16e26e670938583c91f05350fe016ba360adb5049adef3e4834780e13ee55bdbdf791099feee04830139476c736694118572f0a3bf0 15 | 16 | test: 17 | secret_key_base: fad025dab989623db1cee16e26e670938583c91f05350fe016ba360adb5049adef3e4834780e13ee55bdbdf791099feee04830139476c736694118572f0a3bf0 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> -------------------------------------------------------------------------------- /db/migrate/20141206123135_sorcery_core.rb: -------------------------------------------------------------------------------- 1 | class SorceryCore < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :email, :null => false 6 | t.string :crypted_password, :null => false 7 | t.string :salt, :null => false 8 | t.string :avatar 9 | t.string :bio 10 | t.string :locale 11 | 12 | t.timestamps 13 | end 14 | 15 | add_index :users, :email, unique: true 16 | end 17 | end -------------------------------------------------------------------------------- /db/migrate/20141206123136_sorcery_remember_me.rb: -------------------------------------------------------------------------------- 1 | class SorceryRememberMe < ActiveRecord::Migration 2 | def change 3 | add_column :users, :remember_me_token, :string, :default => nil 4 | add_column :users, :remember_me_token_expires_at, :datetime, :default => nil 5 | 6 | add_index :users, :remember_me_token 7 | end 8 | end -------------------------------------------------------------------------------- /db/migrate/20141206123137_sorcery_reset_password.rb: -------------------------------------------------------------------------------- 1 | class SorceryResetPassword < ActiveRecord::Migration 2 | def change 3 | add_column :users, :reset_password_token, :string, :default => nil 4 | add_column :users, :reset_password_token_expires_at, :datetime, :default => nil 5 | add_column :users, :reset_password_email_sent_at, :datetime, :default => nil 6 | 7 | add_index :users, :reset_password_token 8 | end 9 | end -------------------------------------------------------------------------------- /db/migrate/20141208045147_create_songs.rb: -------------------------------------------------------------------------------- 1 | class CreateSongs < ActiveRecord::Migration 2 | def change 3 | create_table :songs do |t| 4 | t.integer :s_id 5 | t.string :title 6 | t.string :artist 7 | t.string :pic 8 | t.text :content 9 | t.integer :user_id 10 | t.integer :comments_count, default: 0 11 | 12 | t.timestamps 13 | end 14 | add_index :songs, [:user_id, :created_at] 15 | end 16 | end -------------------------------------------------------------------------------- /db/migrate/20141212155258_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration 2 | def change 3 | create_table :comments do |t| 4 | t.text :content 5 | t.integer :song_id 6 | t.integer :user_id 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20141213092401_create_likeships.rb: -------------------------------------------------------------------------------- 1 | class CreateLikeships < ActiveRecord::Migration 2 | def change 3 | create_table :likeships do |t| 4 | t.integer :user_id 5 | t.integer :likeable_id 6 | t.string :likeable_type 7 | 8 | t.timestamps 9 | end 10 | add_index :likeships, [:user_id, :likeable_id, :likeable_type], unique: true 11 | end 12 | end -------------------------------------------------------------------------------- /db/migrate/20141214004426_create_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateNotifications < ActiveRecord::Migration 2 | def change 3 | create_table :notifications do |t| 4 | t.belongs_to :user, index: true 5 | t.belongs_to :subject, polymorphic: true, index: true 6 | t.string :name 7 | t.boolean :read, default: false 8 | 9 | t.timestamps 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /db/migrate/20141223012634_sorcery_user_activation.rb: -------------------------------------------------------------------------------- 1 | class SorceryUserActivation < ActiveRecord::Migration 2 | def change 3 | add_column :users, :activation_state, :string, :default => nil 4 | add_column :users, :activation_token, :string, :default => nil 5 | add_column :users, :activation_token_expires_at, :datetime, :default => nil 6 | 7 | add_index :users, :activation_token 8 | end 9 | end -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20141229142842) do 15 | 16 | create_table "comments", force: true do |t| 17 | t.text "content" 18 | t.integer "song_id" 19 | t.integer "user_id" 20 | t.datetime "created_at" 21 | t.datetime "updated_at" 22 | end 23 | 24 | create_table "likeships", force: true do |t| 25 | t.integer "user_id" 26 | t.integer "likeable_id" 27 | t.string "likeable_type" 28 | t.datetime "created_at" 29 | t.datetime "updated_at" 30 | end 31 | 32 | add_index "likeships", ["user_id", "likeable_id", "likeable_type"], name: "index_likeships_on_user_id_and_likeable_id_and_likeable_type", unique: true 33 | 34 | create_table "notifications", force: true do |t| 35 | t.integer "user_id" 36 | t.integer "subject_id" 37 | t.string "subject_type" 38 | t.string "name" 39 | t.boolean "read", default: false 40 | t.datetime "created_at" 41 | t.datetime "updated_at" 42 | end 43 | 44 | add_index "notifications", ["subject_id", "subject_type"], name: "index_notifications_on_subject_id_and_subject_type" 45 | add_index "notifications", ["user_id"], name: "index_notifications_on_user_id" 46 | 47 | create_table "songs", force: true do |t| 48 | t.integer "s_id" 49 | t.string "title" 50 | t.string "artist" 51 | t.string "pic" 52 | t.text "content" 53 | t.integer "user_id" 54 | t.integer "comments_count", default: 0 55 | t.datetime "created_at" 56 | t.datetime "updated_at" 57 | end 58 | 59 | add_index "songs", ["user_id", "created_at"], name: "index_songs_on_user_id_and_created_at" 60 | 61 | create_table "users", force: true do |t| 62 | t.string "name" 63 | t.string "email", null: false 64 | t.string "crypted_password", null: false 65 | t.string "salt", null: false 66 | t.string "avatar" 67 | t.string "bio" 68 | t.string "douban" 69 | t.datetime "created_at" 70 | t.datetime "updated_at" 71 | t.string "remember_me_token" 72 | t.datetime "remember_me_token_expires_at" 73 | t.string "reset_password_token" 74 | t.datetime "reset_password_token_expires_at" 75 | t.datetime "reset_password_email_sent_at" 76 | t.string "activation_state" 77 | t.string "activation_token" 78 | t.datetime "activation_token_expires_at" 79 | t.string "locale" 80 | end 81 | 82 | add_index "users", ["activation_token"], name: "index_users_on_activation_token" 83 | add_index "users", ["email"], name: "index_users_on_email", unique: true 84 | add_index "users", ["remember_me_token"], name: "index_users_on_remember_me_token" 85 | add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token" 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/lib/tasks/.keep -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/test/controllers/.keep -------------------------------------------------------------------------------- /test/controllers/comments_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CommentsControllerTest < ActionController::TestCase 4 | def setup 5 | @comment = comments(:one) 6 | @other_comment = comments(:two) 7 | songs(:one) 8 | end 9 | 10 | test "should be valid" do 11 | assert @comment.valid? 12 | end 13 | 14 | test "should redierct create when not logged in" do 15 | assert_no_difference "Comment.count" do 16 | post :create, comment: @comment.attributes, song_id: @comment.song_id 17 | end 18 | assert_redirected_to login_url 19 | end 20 | 21 | test "should redierct destroy when not logged in" do 22 | assert_no_difference "Comment.count" do 23 | delete :destroy, id: @comment, song_id: @comment.song_id 24 | end 25 | assert_redirected_to login_url 26 | end 27 | 28 | test "should redirect destroy for wrong comment" do 29 | login_user(users(:paul)) 30 | assert_no_difference 'Comment.count' do 31 | assert_raise(ActiveRecord::RecordNotFound) do 32 | delete :destroy, id: @other_comment, song_id: @comment.song_id 33 | end 34 | end 35 | end 36 | 37 | test "should redirect destroy when comment successfully destroyed" do 38 | login_user(@comment.user) 39 | assert_difference 'Comment.count', -1 do 40 | delete :destroy, id: @comment, song_id: @comment.song_id 41 | end 42 | assert_redirected_to @comment.song 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/controllers/likeships_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class LikeshipsControllerTest < ActionController::TestCase 4 | def setup 5 | @song = songs(:one) 6 | @paul_likeship = likeships(:one) 7 | @paul_likeship.id = @song.id 8 | @foo_likeship = likeships(:two) 9 | end 10 | 11 | test "should redirect create when not logged in" do 12 | assert_no_difference 'Likeship.count' do 13 | post :create, likeship: @paul_likeship.attributes 14 | end 15 | assert_redirected_to login_url 16 | end 17 | 18 | test "should redirect destroy when not logged in" do 19 | assert_no_difference "Likeship.count" do 20 | unlike!(@paul_likeship) 21 | end 22 | assert_redirected_to login_url 23 | end 24 | 25 | test "should create when user logged in" do 26 | login_user(users(:paul)) 27 | assert_difference "Likeship.count", +1 do 28 | like!(@paul_likeship) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/controllers/notifications_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NotificationsControllerTest < ActionController::TestCase 4 | test "should redirect index when not logged in" do 5 | get :index 6 | assert_redirected_to login_url 7 | end 8 | 9 | test "should update attribute after current user get index" do 10 | login_user(users(:paul)) 11 | get :index 12 | assert_response :success 13 | assert_equal true, notifications(:one).read 14 | end 15 | 16 | test "should redirect destroy for wrong user" do 17 | login_user(users(:paul)) 18 | assert_raise(ActiveRecord::RecordNotFound) do 19 | xhr :delete, :destroy, id: notifications(:two) 20 | end 21 | end 22 | 23 | test "should destroy for correct user" do 24 | login_user(users(:paul)) 25 | assert_difference "Notification.count", -1 do 26 | xhr :delete, :destroy, id: notifications(:one) 27 | end 28 | end 29 | 30 | test "should destroy all notifications" do 31 | user = users(:paul) 32 | login_user(user) 33 | xhr :delete, :clear 34 | assert_equal 0, user.notifications.count 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/controllers/password_resets_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PasswordResetsControllerTest < ActionController::TestCase 4 | 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SessionsControllerTest < ActionController::TestCase 4 | test "should get new" do 5 | get :new 6 | assert_response :success 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/songs_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SongsControllerTest < ActionController::TestCase 4 | 5 | def setup 6 | @song = songs(:one) 7 | @other_song = songs(:two) 8 | end 9 | 10 | test "shoud redirect create when user not loged in" do 11 | assert_no_difference 'Song.count' do 12 | post :create, song: { s_id: 1, content: "lalala" } 13 | end 14 | assert_redirected_to login_url 15 | end 16 | 17 | test "should redirect destroy when user not loged in" do 18 | assert_no_difference "Song.count" do 19 | delete :destroy, id: @song 20 | end 21 | assert_redirected_to login_url 22 | end 23 | 24 | test "should redirect collect when user not loged in" do 25 | get :collect 26 | assert_redirected_to login_url 27 | end 28 | 29 | test "should redirect edit for wrong song" do 30 | login_user(users(:paul)) 31 | get :edit, id: @other_song 32 | assert_not flash.empty? 33 | assert_redirected_to root_path 34 | end 35 | 36 | test "should redirect update for wrong song" do 37 | login_user(users(:paul)) 38 | patch :update, id: @other_song, song: { content: "@other_song.content" } 39 | assert_not flash.empty? 40 | assert_redirected_to root_path 41 | end 42 | 43 | test "should destroy for wrong song" do 44 | login_user(users(:paul)) 45 | assert_no_difference "Song.count" do 46 | delete :destroy, id: @other_song 47 | end 48 | assert_redirected_to root_path 49 | end 50 | 51 | end -------------------------------------------------------------------------------- /test/controllers/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerTest < ActionController::TestCase 4 | 5 | def setup 6 | @user = users(:paul) 7 | @other_user = users(:foo) 8 | end 9 | 10 | test "new" do 11 | get :new 12 | assert_response :success 13 | end 14 | 15 | test "should redirect edit when not logined in" do 16 | get :edit, id: @user 17 | assert_not flash.empty? 18 | assert_redirected_to login_url 19 | end 20 | 21 | test "should redirect update when not logined in" do 22 | patch :update, id: @user, user: { name: @user.name, email: @user.email, bio: @user.bio } 23 | assert_not flash.empty? 24 | assert_redirected_to login_url 25 | end 26 | 27 | test "should redirect edit when user successfully update profile" do 28 | login_user(@user) 29 | patch :update, id: @user, user: { name: @user.name, email: @user.email, bio: @user.bio } 30 | assert_not flash.empty? 31 | assert_redirected_to root_url 32 | end 33 | 34 | test "should redirect new when user already logined in" do 35 | login_user(@user) 36 | get :new 37 | assert_not flash.empty? 38 | assert_redirected_to root_url 39 | end 40 | 41 | test 'should redirect edit when user logined in as wrong user' do 42 | login_user(@other_user) 43 | get :edit, id: @user 44 | assert_not flash.empty? 45 | assert_redirected_to root_url 46 | end 47 | 48 | test "should redirect update when user logined in as wrong user" do 49 | login_user(@other_user) 50 | patch :update, id: @user, user: { name: @user.name, email: @user.email, bio: @user.bio } 51 | assert_not flash.empty? 52 | assert_redirected_to root_url 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/test/fixtures/.keep -------------------------------------------------------------------------------- /test/fixtures/comments.yml: -------------------------------------------------------------------------------- 1 | one: 2 | content: "comment one" 3 | user: paul 4 | song: one 5 | 6 | two: 7 | content: "comment two" 8 | user: foo 9 | song: two -------------------------------------------------------------------------------- /test/fixtures/likeships.yml: -------------------------------------------------------------------------------- 1 | one: 2 | likeable_type: Song 3 | likeable_id: 1 4 | two: 5 | likeable_type: Song 6 | likeable_id: 1 -------------------------------------------------------------------------------- /test/fixtures/notifications.yml: -------------------------------------------------------------------------------- 1 | one: 2 | user: paul 3 | name: "mention" 4 | subject: one (Comment) 5 | two: 6 | user: foo 7 | name: "comment" 8 | subject: one (Comment) -------------------------------------------------------------------------------- /test/fixtures/songs.yml: -------------------------------------------------------------------------------- 1 | one: 2 | s_id: 1 3 | title: "Hello World" 4 | artist: "paul" 5 | pic: "http://example.com/dog.png" 6 | content: "Good music" 7 | user: paul 8 | 9 | two: 10 | s_id: 2 11 | title: "Cool" 12 | artist: "foo" 13 | pic: "http://example.com/dog.png" 14 | content: "I love this music" 15 | user: foo 16 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | paul: 2 | name: paul 3 | email: paul@example.com 4 | salt: <%= salt = "asdasdastr4325234324sdfds" %> 5 | crypted_password: <%= Sorcery::CryptoProviders::BCrypt.encrypt("secret", salt) %> 6 | bio: "Fxxk GFW" 7 | 8 | foo: 9 | name: foo 10 | email: foo@example.com 11 | salt: <%= salt = "asdasdastr4325234324sdfds" %> 12 | crypted_password: <%= Sorcery::CryptoProviders::BCrypt.encrypt("secret", salt) %> 13 | bio: "Fxxk GFW" -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/test/helpers/.keep -------------------------------------------------------------------------------- /test/helpers/comments_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CommentsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/helpers/likeships_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class LikeshipsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/helpers/notifications_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NotificationsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/helpers/password_resets_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PasswordResetsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/helpers/sessions_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SessionsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/helpers/songs_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SongsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/helpers/users_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/test/mailers/.keep -------------------------------------------------------------------------------- /test/mailers/previews/user_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/user_mailer 2 | class UserMailerPreview < ActionMailer::Preview 3 | # def reset_password_email 4 | # @user = User.first 5 | # UserMailer.reset_password_email(@user) 6 | # end 7 | 8 | # http://localhost:3000/rails/mailers/user_mailer/activation_needed_email 9 | def activation_needed_email 10 | @user = User.first 11 | UserMailer.activation_needed_email(@user) 12 | end 13 | 14 | # http://localhost:3000/rails/mailers/user_mailer/activation_success_email 15 | def activation_success_email 16 | @user = User.first 17 | UserMailer.activation_success_email(@user) 18 | end 19 | 20 | def reset_password_email 21 | @user = User.first 22 | UserMailer.reset_password_email(@user) 23 | end 24 | end -------------------------------------------------------------------------------- /test/mailers/user_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserMailerTest < ActionMailer::TestCase 4 | 5 | end 6 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/test/models/.keep -------------------------------------------------------------------------------- /test/models/comment_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CommentTest < ActiveSupport::TestCase 4 | def setup 5 | @comment = comments(:one) 6 | end 7 | 8 | test "should be valid" do 9 | assert @comment.valid? 10 | end 11 | 12 | test "user id should be present" do 13 | @comment.user_id = nil 14 | assert_not @comment.valid? 15 | end 16 | 17 | test "song id should be present" do 18 | @comment.song_id = nil 19 | assert_not @comment.valid? 20 | end 21 | 22 | test "content should be present" do 23 | @comment.content = " " 24 | assert_not @comment.valid? 25 | end 26 | 27 | test 'should notify at_users' do 28 | count = Notification.count 29 | user = users(:foo) 30 | @comment.content = "@#{@comment.song.user.name}" 31 | @comment.user = user 32 | @comment.dup.save! 33 | assert_equal count + 1, Notification.count 34 | end 35 | 36 | test 'should notify musician' do 37 | count = Notification.count 38 | user = users(:foo) 39 | @comment.user = user 40 | @comment.dup.save! 41 | assert_equal count + 1, Notification.count 42 | end 43 | 44 | test 'should not notify musician' do 45 | count = Notification.count 46 | @comment.dup.save! 47 | assert_equal count, Notification.count 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/models/likeship_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class LikeshipTest < ActiveSupport::TestCase 4 | def setup 5 | @user = users(:paul) 6 | @like_song = @user.likeships.new(likeable_type: "Song", likeable_id: 1) 7 | @like_comment = @user.likeships.new(likeable_type: "Comment", likeable_id: 2) 8 | end 9 | 10 | test "should be valid" do 11 | assert @like_song.valid? 12 | assert @like_comment.valid? 13 | end 14 | 15 | test "user id should be present" do 16 | @like_song.user_id = nil 17 | assert_not @like_song.valid? 18 | end 19 | 20 | test "likeable type should be present" do 21 | @like_song.likeable_type = nil 22 | assert_not @like_song.valid? 23 | end 24 | 25 | test "likeable id should be present" do 26 | @like_song.likeable_id = nil 27 | assert_not @like_song.valid? 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/models/notification_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NotificationTest < ActiveSupport::TestCase 4 | test "should decrease notification count" do 5 | assert_difference "Notification.count", -1 do 6 | notifications(:one).destroy 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /test/models/song_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SongTest < ActiveSupport::TestCase 4 | def setup 5 | @user = users(:paul) 6 | @song = @user.songs.build(s_id: 3, 7 | title: "梦的列车", 8 | artist: "Alex", 9 | pic: "http://img.xiami.net/images/album/img1/1/11383201444.jpg", 10 | content: "Good music") 11 | end 12 | 13 | test "shuold be valid" do 14 | assert @song.valid? 15 | end 16 | 17 | test "s_id should not be blank" do 18 | assert_raise(OpenURI::HTTPError) do 19 | @song.s_id = " " 20 | assert_not @song.valid? 21 | end 22 | end 23 | 24 | test "content should not be blank" do 25 | @song.content = " " 26 | assert_not @song.valid? 27 | end 28 | 29 | test "s_id should greater than 0" do 30 | @song.s_id = 0 31 | assert_not @song.valid? 32 | end 33 | 34 | test "content should have maximum length" do 35 | @song.content = "a" * 10001 36 | assert_not @song.valid? 37 | end 38 | 39 | test "associated comments should be destroyed" do 40 | comment = comments(:one) 41 | assert_difference 'Comment.count', -1 do 42 | comment.song.destroy 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | 5 | def setup 6 | @user = User.new(name: "Aufree", email: "user@example.com", 7 | password: "foobar", password_confirmation: "foobar") 8 | end 9 | 10 | test "should be valid" do 11 | assert @user.valid? 12 | end 13 | 14 | test "name should be present" do 15 | @user.name = " " 16 | assert_not @user.valid? 17 | end 18 | 19 | test "email should be present" do 20 | @user.email = " " 21 | assert_not @user.valid? 22 | end 23 | 24 | test "name should not be too long" do 25 | @user.name = "a" * 51 26 | assert_not @user.valid? 27 | end 28 | 29 | test "bio should not be too long" do 30 | @user.bio = "a" * 141 31 | assert_not @user.valid? 32 | end 33 | 34 | test "name validation should accept valid format" do 35 | invalid_names = %w["au free", "张三"] 36 | invalid_names.each do |invalid_name| 37 | @user.name = invalid_name 38 | assert_not @user.valid? 39 | end 40 | end 41 | 42 | test "email validation should accept valid addresses" do 43 | valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org 44 | first.last@foo.jp alice+bob@baz.cn] 45 | valid_addresses.each do |valid_address| 46 | @user.email = valid_address 47 | assert @user.valid?, "#{valid_address.inspect} should be valid" 48 | end 49 | end 50 | 51 | test "email validation should reject invalid addresses" do 52 | invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. 53 | foo@bar_baz.com foo@bar+baz.com] 54 | invalid_addresses.each do |invalid_address| 55 | @user.email = invalid_address 56 | assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" 57 | end 58 | end 59 | 60 | test "user name should be unique" do 61 | duplicate_user = @user.dup 62 | duplicate_user.name = @user.name.upcase 63 | @user.save 64 | assert_not duplicate_user.valid? 65 | end 66 | 67 | test "email address should be unique" do 68 | duplicate_user = @user.dup 69 | duplicate_user.email = @user.email.upcase 70 | @user.save 71 | assert_not duplicate_user.valid? 72 | end 73 | 74 | test "password should have a minimum length" do 75 | @user.password = @user.password_confirmation = "a" * 5 76 | assert_not @user.valid? 77 | end 78 | 79 | test "password should have a maximum length" do 80 | @user.password = @user.password_confirmation = "a" * 17 81 | assert_not @user.valid? 82 | end 83 | 84 | test "bio should have a maximum length" do 85 | @user.bio = "a" * 141 86 | assert_not @user.valid? 87 | end 88 | 89 | # test "associated songs should be destroyed" do 90 | # @user.save 91 | # @user.songs.create!(s_id: 100, title: "lorem ipsum", artist: "k", content: "cool") 92 | # assert_difference 'Song.count', -1 do 93 | # @user.destroy 94 | # end 95 | # end 96 | 97 | test "associated comments should be destroyed" do 98 | comment = comments(:one) 99 | assert_difference 'Comment.count', -1 do 100 | comment.user.destroy 101 | end 102 | end 103 | 104 | end 105 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | require 'minitest/reporters' 5 | Minitest::Reporters.use! 6 | include Sorcery::TestHelpers::Rails::Controller 7 | 8 | class ActiveSupport::TestCase 9 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 10 | fixtures :all 11 | 12 | def like!(likeship) 13 | post :create, 14 | likeable_type: likeship.likeable_type, 15 | likeable_id: likeship.id 16 | end 17 | 18 | def unlike!(likeship) 19 | delete :destroy, 20 | likeable_type: likeship.likeable_type, 21 | likeable_id: likeship.id 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aufree/ting/66631ffdecf0d4752243a7a2d57cf3e9bc160a62/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------