diff --git a/.gitignore b/.gitignore index 196d3a76..3f798c97 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ coverage/ .idea/ /public/assets +/public/system # damn vim backup files *~ diff --git a/Gemfile b/Gemfile index dd1e8e80..e281b595 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,8 @@ gem 'font-kit-rails' gem 'nprogress-rails' gem 'font-awesome-rails', '~> 4.2.0.0' gem 'rails-assets-growl' +gem "paperclip", "~> 4.2" +gem 'delayed_paperclip' gem 'ruby-progressbar' diff --git a/Gemfile.lock b/Gemfile.lock index 2ea048b5..cc81ddf5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,7 +67,11 @@ GEM xpath (~> 2.0) celluloid (0.16.0) timers (~> 4.0.0) + climate_control (0.0.3) + activesupport (>= 3.0) cliver (0.3.2) + cocaine (0.5.5) + climate_control (>= 0.0.3, < 1.0) coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) @@ -80,6 +84,8 @@ GEM crass (1.0.1) daemons (1.1.9) database_cleaner (1.3.0) + delayed_paperclip (2.9.0) + paperclip (>= 3.3) devise (3.4.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -167,6 +173,11 @@ GEM multi_json (~> 1.3) omniauth-oauth (~> 1.0) orm_adapter (0.5.0) + paperclip (4.2.1) + activemodel (>= 3.0.0) + activesupport (>= 3.0.0) + cocaine (~> 0.5.3) + mime-types pg (0.17.1) poltergeist (1.5.1) capybara (~> 2.1) @@ -339,6 +350,7 @@ DEPENDENCIES capybara coffee-rails (~> 4.1.0) database_cleaner + delayed_paperclip devise factory_girl_rails faker @@ -354,6 +366,7 @@ DEPENDENCIES nprogress-rails omniauth omniauth-twitter + paperclip (~> 4.2) pg poltergeist questiongenerator! diff --git a/app/assets/javascripts/application.js.erb.coffee b/app/assets/javascripts/application.js.erb.coffee index f37ea0b7..ef98ffe4 100644 --- a/app/assets/javascripts/application.js.erb.coffee +++ b/app/assets/javascripts/application.js.erb.coffee @@ -7,6 +7,7 @@ #= require nprogress-turbolinks #= require growl #= require cheet +#= require jquery.guillotine #= require_tree . NProgress.configure diff --git a/app/assets/javascripts/settings.coffee b/app/assets/javascripts/settings.coffee index 4f6517bb..5742e5b9 100644 --- a/app/assets/javascripts/settings.coffee +++ b/app/assets/javascripts/settings.coffee @@ -2,4 +2,50 @@ ($ document).on "submit", "form#edit_user", (evt) -> if ($ "input#user_current_password").val().length == 0 evt.preventDefault() - $("button[data-target=#modal-passwd]").trigger 'click' \ No newline at end of file + $("button[data-target=#modal-passwd]").trigger 'click' + + +# Profile pic +($ document).on 'change', 'input#user_profile_picture[type=file]', -> + input = ($ this)[0] + + ($ '#profile-picture-crop-controls').slideUp 400, -> + if input.files and input.files[0] + fr = new FileReader() + ($ fr).on 'load', (e) -> + cropper = ($ '#profile-picture-cropper') + preview = ($ '#profile-picture-preview') + + updateVars = (data, action) -> + ($ '#crop_x').val Math.floor(data.x / data.scale) + ($ '#crop_y').val Math.floor(data.y / data.scale) + ($ '#crop_w').val Math.floor(data.w / data.scale) + ($ '#crop_h').val Math.floor(data.h / data.scale) +# rx = 100 / data.w +# ry = 100 / data.h +# ($ '#profile-picture-preview').css +# width: Math.round(rx * preview[0].naturalWidth) + 'px' +# height: Math.round(ry * preview[0].naturalHeight) + 'px' +# marginLeft: '-' + Math.round(rx * data.x) + 'px' +# marginTop: '-' + Math.round(ry * data.y) + 'px' + + cropper.on 'load', -> + side = if cropper[0].naturalWidth > cropper[0].naturalHeight + cropper[0].naturalHeight + else + cropper[0].naturalWidth + + cropper.guillotine + width: side + height: side + onChange: updateVars + + updateVars cropper.guillotine('getData'), 'drag' # just because + + ($ '#cropper-zoom-out').click -> cropper.guillotine 'zoomOut' + ($ '#cropper-zoom-in').click -> cropper.guillotine 'zoomIn' + ($ '#profile-picture-crop-controls').slideDown() + + cropper.attr 'src', e.target.result + + fr.readAsDataURL(input.files[0]) \ No newline at end of file diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index f929ab0d..e88e57cc 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -1,6 +1,7 @@ /* *= require rails_bootstrap_forms *= require growl + *= require jquery.guillotine *= require_self */ diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index cc560d54..8124e1e8 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -1,4 +1,6 @@ class UserController < ApplicationController + before_filter :authenticate_user!, only: %w(edit update) + def show @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! @answers = @user.answers.reverse_order.paginate(page: params[:page]) @@ -9,14 +11,17 @@ class UserController < ApplicationController end def edit - authenticate_user! end def update - authenticate_user! - user_attributes = params.require(:user).permit(:display_name, :motivation_header, :website, :location, :bio) - unless current_user.update_attributes(user_attributes) - flash[:error] = 'fork it' + user_attributes = params.require(:user).permit(:display_name, :profile_picture, :motivation_header, :website, + :location, :bio, :crop_x, :crop_y, :crop_w, :crop_h) + if current_user.update_attributes(user_attributes) + text = 'Your profile has been updated!' + text += ' It might take a few minutes until your new profile picture is shown everywhere.' if user_attributes[:profile_picture] + flash[:success] = text + else + flash[:error] = 'An error occurred. ;_;' end redirect_to edit_user_profile_path end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index cf7e294a..5873f7db 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -59,11 +59,13 @@ module ApplicationHelper ((!current_user.nil?) && ((current_user == user) || current_user.mod?)) ? true : false end + # @deprecated Use {User#profile_picture.url} instead. def gravatar_url(user) + return user.profile_picture.url :medium # return '/cage.png' - return '//www.gravatar.com/avatar' if user.nil? - return "//www.gravatar.com/avatar/#{Digest::MD5.hexdigest(user)}" if user.is_a? String - "//www.gravatar.com/avatar/#{Digest::MD5.hexdigest(user.email)}" + #return '//www.gravatar.com/avatar' if user.nil? + #return "//www.gravatar.com/avatar/#{Digest::MD5.hexdigest(user)}" if user.is_a? String + #"//www.gravatar.com/avatar/#{Digest::MD5.hexdigest(user.email)}" end def ios_web_app? diff --git a/app/models/user.rb b/app/models/user.rb index 4a805a4d..05048789 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,6 +40,12 @@ class User < ActiveRecord::Base # validates :website, format: { with: WEBSITE_REGEX } + has_attached_file :profile_picture, styles: { large: "500x500#", medium: "256x256#", small: "80x80#" }, + default_url: "/images/:style/no_avatar.png", use_timestamp: false, + processors: [:cropper] + validates_attachment_content_type :profile_picture, :content_type => /\Aimage\/.*\Z/ + process_in_background :profile_picture + before_save do self.display_name = 'WRYYYYYYYY' if display_name == 'Dio Brando' self.website = if website.match %r{\Ahttps?://} @@ -150,4 +156,8 @@ class User < ActiveRecord::Base def report_comment(report, content) ModerationComment.create!(user: self, report: report, content: content) end + + def cropping? + !crop_x.blank? && !crop_y.blank? && !crop_w.blank? && !crop_h.blank? + end end diff --git a/app/views/user/_account.html.haml b/app/views/user/_account.html.haml index e7f0f04d..be98fc87 100644 --- a/app/views/user/_account.html.haml +++ b/app/views/user/_account.html.haml @@ -1,11 +1,6 @@ .container.j2-page = render 'user/settings_tabs' .col-md-9.col-xs-12.col-sm-9 - .alert.alert-info - We currently only support avatars using - = succeed ',' do - %a{href: "https://en.gravatar.com"} Gravatar - after you set yours up, use the E-Mail you are using for it on here as well, we will directly use this image then! = render 'layouts/messages' .panel.panel-default .panel-body diff --git a/app/views/user/edit.html.haml b/app/views/user/edit.html.haml index a39e380f..bd2540c7 100644 --- a/app/views/user/edit.html.haml +++ b/app/views/user/edit.html.haml @@ -4,10 +4,27 @@ = render 'layouts/messages' .panel.panel-default .panel-body - = bootstrap_form_for(current_user, url: {action: "edit"}, method: "patch") do |f| + = bootstrap_form_for(current_user, url: {action: "edit"}, :html => { :multipart => true }, method: "patch") do |f| = f.text_field :display_name, label: "Your name" + .media + .pull-left + %img.img-rounded.profile--img{src: current_user.profile_picture.url(:medium)} + .media-body + = f.file_field :profile_picture + + .row#profile-picture-crop-controls{style: 'display: none;'} + .col-sm-10.col-md-8 + %strong Adjust your new image + %img#profile-picture-cropper{src: current_user.profile_picture.url(:medium)} + .col-sm-2.col-md-4 + .btn-group + %button#cropper-zoom-out.btn.btn-inverse{type: :button} + %i.fa.fa-search-minus + %button#cropper-zoom-in.btn.btn-inverse{type: :button} + %i.fa.fa-search-plus + = f.text_field :motivation_header, label: "Motivation header", placeholder: 'Ask me anything!' = f.text_field :website, label: "Website", placeholder: 'http://bad-dragon.com' @@ -16,4 +33,7 @@ = f.text_area :bio, label: "Bio", placeholder: 'In Bio war ich nie gut x--DD' + - for attrib in %i(crop_x crop_y crop_w crop_h) + = f.hidden_field attrib, id: attrib + = f.submit "Save settings", class: 'btn btn-primary' \ No newline at end of file diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 6762520e..525b6171 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -6,4 +6,5 @@ staging: production: :concurrency: 25 :queues: - - share \ No newline at end of file + - share + - paperclip \ No newline at end of file diff --git a/db/migrate/20141229085904_add_attachment_profile_picture_to_users.rb b/db/migrate/20141229085904_add_attachment_profile_picture_to_users.rb new file mode 100644 index 00000000..5191d385 --- /dev/null +++ b/db/migrate/20141229085904_add_attachment_profile_picture_to_users.rb @@ -0,0 +1,11 @@ +class AddAttachmentProfilePictureToUsers < ActiveRecord::Migration + def self.up + change_table :users do |t| + t.attachment :profile_picture + end + end + + def self.down + remove_attachment :users, :profile_picture + end +end diff --git a/db/migrate/20141229105013_add_profile_picture_processing_to_users.rb b/db/migrate/20141229105013_add_profile_picture_processing_to_users.rb new file mode 100644 index 00000000..a5f7e85d --- /dev/null +++ b/db/migrate/20141229105013_add_profile_picture_processing_to_users.rb @@ -0,0 +1,5 @@ +class AddProfilePictureProcessingToUsers < ActiveRecord::Migration + def change + add_column :users, :profile_picture_processing, :boolean + end +end diff --git a/db/migrate/20141229133149_add_crop_values_to_users.rb b/db/migrate/20141229133149_add_crop_values_to_users.rb new file mode 100644 index 00000000..f3580dcf --- /dev/null +++ b/db/migrate/20141229133149_add_crop_values_to_users.rb @@ -0,0 +1,10 @@ +class AddCropValuesToUsers < ActiveRecord::Migration + def change + # this is a ugly hack and will stay until I find a way to pass parameters + # to the paperclip Sidekiq worker. oh well. + add_column :users, :crop_x, :integer + add_column :users, :crop_y, :integer + add_column :users, :crop_w, :integer + add_column :users, :crop_h, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index a939558f..a5bcd502 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20141228202825) do +ActiveRecord::Schema.define(version: 20141229133149) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -133,12 +133,12 @@ ActiveRecord::Schema.define(version: 20141228202825) do add_index "smiles", ["user_id"], name: "index_smiles_on_user_id", using: :btree create_table "users", force: true do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0, null: false + t.integer "sign_in_count", default: 0, null: false t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" @@ -146,19 +146,28 @@ ActiveRecord::Schema.define(version: 20141228202825) do t.datetime "created_at" t.datetime "updated_at" t.string "screen_name" - t.integer "friend_count", default: 0, null: false - t.integer "follower_count", default: 0, null: false - t.integer "asked_count", default: 0, null: false - t.integer "answered_count", default: 0, null: false - t.integer "commented_count", default: 0, null: false + t.integer "friend_count", default: 0, null: false + t.integer "follower_count", default: 0, null: false + t.integer "asked_count", default: 0, null: false + t.integer "answered_count", default: 0, null: false + t.integer "commented_count", default: 0, null: false t.string "display_name" - t.integer "smiled_count", default: 0, null: false - t.boolean "admin", default: false, null: false - t.string "motivation_header", default: "", null: false - t.string "website", default: "", null: false - t.string "location", default: "", null: false - t.text "bio", default: "", null: false - t.boolean "moderator", default: false, null: false + t.integer "smiled_count", default: 0, null: false + t.boolean "admin", default: false, null: false + t.string "motivation_header", default: "", null: false + t.string "website", default: "", null: false + t.string "location", default: "", null: false + t.text "bio", default: "", null: false + t.boolean "moderator", default: false, null: false + t.string "profile_picture_file_name" + t.string "profile_picture_content_type" + t.integer "profile_picture_file_size" + t.datetime "profile_picture_updated_at" + t.boolean "profile_picture_processing" + t.integer "crop_x" + t.integer "crop_y" + t.integer "crop_w" + t.integer "crop_h" end add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree diff --git a/lib/assets/javascripts/jquery.guillotine.js b/lib/assets/javascripts/jquery.guillotine.js new file mode 100644 index 00000000..3d6cb0b1 --- /dev/null +++ b/lib/assets/javascripts/jquery.guillotine.js @@ -0,0 +1,417 @@ +// Generated by CoffeeScript 1.8.0 + +/* + * jQuery Guillotine Plugin v1.3.0 + * http://matiasgagliano.github.com/guillotine/ + * + * Copyright 2014, Matías Gagliano. + * Dual licensed under the MIT or GPLv3 licenses. + * http://opensource.org/licenses/MIT + * http://opensource.org/licenses/GPL-3.0 + * + */ + +(function() { + "use strict"; + var $, Guillotine, canTransform, defaults, events, getPointerPosition, hardwareAccelerate, isTouch, pluginName, scope, touchRegExp, validEvent, whitelist, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + $ = jQuery; + + pluginName = 'guillotine'; + + scope = 'guillotine'; + + events = { + start: "touchstart." + scope + " mousedown." + scope + " pointerdown." + scope, + move: "touchmove." + scope + " mousemove." + scope + " pointermove." + scope, + stop: "touchend." + scope + " mouseup." + scope + " pointerup." + scope + }; + + defaults = { + width: 400, + height: 300, + zoomStep: 0.1, + init: null, + eventOnChange: null, + onChange: null + }; + + touchRegExp = /touch/i; + + isTouch = function(e) { + return touchRegExp.test(e.type); + }; + + validEvent = function(e) { + if (isTouch(e)) { + return e.originalEvent.changedTouches.length === 1; + } else { + return e.which === 1; + } + }; + + getPointerPosition = function(e) { + if (isTouch(e)) { + e = e.originalEvent.touches[0]; + } + return { + x: e.pageX, + y: e.pageY + }; + }; + + canTransform = function() { + var hasTransform, helper, prefix, prefixes, prop, test, tests, value, _i, _len; + hasTransform = false; + prefixes = 'webkit,Moz,O,ms,Khtml'.split(','); + tests = { + transform: 'transform' + }; + for (_i = 0, _len = prefixes.length; _i < _len; _i++) { + prefix = prefixes[_i]; + tests[prefix + 'Transform'] = "-" + (prefix.toLowerCase()) + "-transform"; + } + helper = document.createElement('img'); + document.body.insertBefore(helper, null); + for (test in tests) { + prop = tests[test]; + if (helper.style[test] === void 0) { + continue; + } + helper.style[test] = 'rotate(90deg)'; + value = window.getComputedStyle(helper).getPropertyValue(prop); + if ((value != null) && value.length && value !== 'none') { + hasTransform = true; + break; + } + } + document.body.removeChild(helper); + canTransform = hasTransform ? (function() { + return true; + }) : (function() { + return false; + }); + return canTransform(); + }; + + hardwareAccelerate = function(el) { + return $(el).css({ + '-webkit-perspective': 1000, + 'perspective': 1000, + '-webkit-backface-visibility': 'hidden', + 'backface-visibility': 'hidden' + }); + }; + + Guillotine = (function() { + function Guillotine(element, options) { + this._drag = __bind(this._drag, this); + this._unbind = __bind(this._unbind, this); + this._start = __bind(this._start, this); + var _ref; + this.op = $.extend(true, {}, defaults, options, $(element).data(pluginName)); + this.enabled = true; + this.zoomInFactor = 1 + this.op.zoomStep; + this.zoomOutFactor = 1 / this.zoomInFactor; + _ref = [0, 0, 0, 0, 0], this.width = _ref[0], this.height = _ref[1], this.left = _ref[2], this.top = _ref[3], this.angle = _ref[4]; + this.data = { + scale: 1, + angle: 0, + x: 0, + y: 0, + w: this.op.width, + h: this.op.height + }; + this._wrap(element); + if (this.op.init != null) { + this._init(); + } + if (this.width < 1 || this.height < 1) { + this._fit() && this._center(); + } + hardwareAccelerate(this.$el); + this.$el.on(events.start, this._start); + } + + Guillotine.prototype._wrap = function(element) { + var canvas, el, guillotine, height, img, paddingTop, width, _ref, _ref1, _ref2; + el = $(element); + if (el.prop('tagName') === 'IMG') { + img = document.createElement('img'); + img.src = el.attr('src'); + _ref = [img.width, img.height], width = _ref[0], height = _ref[1]; + } else { + _ref1 = [el.width(), el.height()], width = _ref1[0], height = _ref1[1]; + } + _ref2 = [width / this.op.width, height / this.op.height], this.width = _ref2[0], this.height = _ref2[1]; + canvas = $('
').addClass('guillotine-canvas'); + canvas.css({ + width: this.width * 100 + '%', + height: this.height * 100 + '%', + top: 0, + left: 0 + }); + canvas = el.wrap(canvas).parent(); + paddingTop = this.op.height / this.op.width * 100 + '%'; + guillotine = $('
').addClass('guillotine-window'); + guillotine.css({ + width: '100%', + height: 'auto', + 'padding-top': paddingTop + }); + guillotine = canvas.wrap(guillotine).parent(); + this.$el = el; + this.el = el[0]; + this.$canvas = canvas; + this.canvas = canvas[0]; + this.$gllt = guillotine; + this.gllt = guillotine[0]; + return this.$document = $(element.ownerDocument); + }; + + Guillotine.prototype._unwrap = function() { + this.$el.removeAttr('style'); + this.$el.insertBefore(this.gllt); + return this.$gllt.remove(); + }; + + Guillotine.prototype._init = function() { + var angle, o, scale; + o = this.op.init; + if ((scale = parseFloat(o.scale))) { + this._zoom(scale); + } + if ((angle = parseInt(o.angle))) { + this._rotate(angle); + } + return this._offset(parseInt(o.x) / this.op.width || 0, parseInt(o.y) / this.op.height || 0); + }; + + Guillotine.prototype._start = function(e) { + if (!(this.enabled && validEvent(e))) { + return; + } + e.preventDefault(); + e.stopImmediatePropagation(); + this.p = getPointerPosition(e); + return this._bind(); + }; + + Guillotine.prototype._bind = function() { + this.$document.on(events.move, this._drag); + return this.$document.on(events.stop, this._unbind); + }; + + Guillotine.prototype._unbind = function(e) { + this.$document.off(events.move, this._drag); + this.$document.off(events.stop, this._unbind); + if (e != null) { + return this._trigger('drag'); + } + }; + + Guillotine.prototype._trigger = function(action) { + if (this.op.eventOnChange != null) { + this.$el.trigger(this.op.eventOnChange, [this.data, action]); + } + if (typeof this.op.onChange === 'function') { + return this.op.onChange.call(this.el, this.data, action); + } + }; + + Guillotine.prototype._drag = function(e) { + var dx, dy, left, p, top; + e.preventDefault(); + e.stopImmediatePropagation(); + p = getPointerPosition(e); + dx = p.x - this.p.x; + dy = p.y - this.p.y; + this.p = p; + left = dx === 0 ? null : this.left - dx / this.gllt.clientWidth; + top = dy === 0 ? null : this.top - dy / this.gllt.clientHeight; + return this._offset(left, top); + }; + + Guillotine.prototype._offset = function(left, top) { + if (left || left === 0) { + if (left < 0) { + left = 0; + } + if (left > this.width - 1) { + left = this.width - 1; + } + this.canvas.style.left = (-left * 100).toFixed(2) + '%'; + this.left = left; + this.data.x = Math.round(left * this.op.width); + } + if (top || top === 0) { + if (top < 0) { + top = 0; + } + if (top > this.height - 1) { + top = this.height - 1; + } + this.canvas.style.top = (-top * 100).toFixed(2) + '%'; + this.top = top; + return this.data.y = Math.round(top * this.op.height); + } + }; + + Guillotine.prototype._zoom = function(factor) { + var h, left, top, w, _ref; + if (factor <= 0 || factor === 1) { + return; + } + _ref = [this.width, this.height], w = _ref[0], h = _ref[1]; + if (w * factor > 1 && h * factor > 1) { + this.width *= factor; + this.height *= factor; + this.canvas.style.width = (this.width * 100).toFixed(2) + '%'; + this.canvas.style.height = (this.height * 100).toFixed(2) + '%'; + this.data.scale *= factor; + } else { + this._fit(); + factor = this.width / w; + } + left = (this.left + 0.5) * factor - 0.5; + top = (this.top + 0.5) * factor - 0.5; + return this._offset(left, top); + }; + + Guillotine.prototype._fit = function() { + var prevWidth, relativeRatio; + prevWidth = this.width; + relativeRatio = this.height / this.width; + if (relativeRatio > 1) { + this.width = 1; + this.height = relativeRatio; + } else { + this.width = 1 / relativeRatio; + this.height = 1; + } + this.canvas.style.width = (this.width * 100).toFixed(2) + '%'; + this.canvas.style.height = (this.height * 100).toFixed(2) + '%'; + return this.data.scale *= this.width / prevWidth; + }; + + Guillotine.prototype._center = function() { + return this._offset((this.width - 1) / 2, (this.height - 1) / 2); + }; + + Guillotine.prototype._rotate = function(angle) { + var canvasRatio, glltRatio, h, w, _ref, _ref1, _ref2; + if (!canTransform()) { + return; + } + if (!(angle !== 0 && angle % 90 === 0)) { + return; + } + this.angle = (this.angle + angle) % 360; + if (this.angle < 0) { + this.angle = 360 + this.angle; + } + if (angle % 180 !== 0) { + glltRatio = this.op.height / this.op.width; + _ref = [this.height * glltRatio, this.width / glltRatio], this.width = _ref[0], this.height = _ref[1]; + if (this.width >= 1 && this.height >= 1) { + this.canvas.style.width = this.width * 100 + '%'; + this.canvas.style.height = this.height * 100 + '%'; + } else { + this._fit(); + } + } + _ref1 = [1, 1], w = _ref1[0], h = _ref1[1]; + if (this.angle % 180 !== 0) { + canvasRatio = this.height / this.width * glltRatio; + _ref2 = [canvasRatio, 1 / canvasRatio], w = _ref2[0], h = _ref2[1]; + } + this.el.style.width = w * 100 + '%'; + this.el.style.height = h * 100 + '%'; + this.el.style.left = (1 - w) / 2 * 100 + '%'; + this.el.style.top = (1 - h) / 2 * 100 + '%'; + this.$el.css({ + transform: "rotate(" + this.angle + "deg)" + }); + this._center(); + return this.data.angle = this.angle; + }; + + Guillotine.prototype.rotateLeft = function() { + return this.enabled && (this._rotate(-90), this._trigger('rotateLeft')); + }; + + Guillotine.prototype.rotateRight = function() { + return this.enabled && (this._rotate(90), this._trigger('rotateRight')); + }; + + Guillotine.prototype.center = function() { + return this.enabled && (this._center(), this._trigger('center')); + }; + + Guillotine.prototype.fit = function() { + return this.enabled && (this._fit(), this._center(), this._trigger('fit')); + }; + + Guillotine.prototype.zoomIn = function() { + return this.enabled && (this._zoom(this.zoomInFactor), this._trigger('zoomIn')); + }; + + Guillotine.prototype.zoomOut = function() { + return this.enabled && (this._zoom(this.zoomOutFactor), this._trigger('zoomOut')); + }; + + Guillotine.prototype.getData = function() { + return this.data; + }; + + Guillotine.prototype.enable = function() { + return this.enabled = true; + }; + + Guillotine.prototype.disable = function() { + return this.enabled = false; + }; + + Guillotine.prototype.remove = function() { + this._unbind(); + this._unwrap(); + this.disable(); + this.$el.off(events.start, this._start); + return this.$el.removeData(pluginName + 'Instance'); + }; + + return Guillotine; + + })(); + + whitelist = ['rotateLeft', 'rotateRight', 'center', 'fit', 'zoomIn', 'zoomOut', 'instance', 'getData', 'enable', 'disable', 'remove']; + + $.fn[pluginName] = function(options) { + if (typeof options !== 'string') { + return this.each(function() { + var guillotine; + if (!$.data(this, pluginName + 'Instance')) { + guillotine = new Guillotine(this, options); + return $.data(this, pluginName + 'Instance', guillotine); + } + }); + } else if (__indexOf.call(whitelist, options) >= 0) { + if (options === 'instance') { + return $.data(this[0], pluginName + 'Instance'); + } + if (options === 'getData') { + return $.data(this[0], pluginName + 'Instance')[options](); + } + return this.each(function() { + var guillotine; + guillotine = $.data(this, pluginName + 'Instance'); + if (guillotine) { + return guillotine[options](); + } + }); + } + }; + +}).call(this); diff --git a/lib/assets/stylesheets/jquery.guillotine.css b/lib/assets/stylesheets/jquery.guillotine.css new file mode 100644 index 00000000..62df7e9b --- /dev/null +++ b/lib/assets/stylesheets/jquery.guillotine.css @@ -0,0 +1,28 @@ +.guillotine-window { + display: block; + position: relative; + overflow: hidden; +} + +.guillotine-canvas { + position: absolute; + top: 0; + left: 0; + text-align: center; + margin: 0 !important; + padding: 0 !important; + border: none !important; +} + +.guillotine-canvas > * { + position: absolute; + top: 0; + left: 0; + max-width: none; + max-height: none; + width: 100%; + height: 100%; + margin: 0 !important; + padding: 0 !important; + border: none !important; +} diff --git a/lib/paperclip_processors/cropper.rb b/lib/paperclip_processors/cropper.rb new file mode 100644 index 00000000..f2677029 --- /dev/null +++ b/lib/paperclip_processors/cropper.rb @@ -0,0 +1,22 @@ +module Paperclip + class Cropper < Thumbnail + def transformation_command + if crop_command + x = super + i = x.index '-crop' + 2.times { x.delete_at i } if i + crop_command + x + else + super + end + end + + def crop_command + target = @attachment.instance + if target.cropping? + ['-crop', "'#{target.crop_w.to_i}x#{target.crop_h.to_i}+#{target.crop_x.to_i}+#{target.crop_y.to_i}'"] + end + end + end +end + diff --git a/public/images/large/no_avatar.png b/public/images/large/no_avatar.png new file mode 100644 index 00000000..709109fa Binary files /dev/null and b/public/images/large/no_avatar.png differ diff --git a/public/images/medium/no_avatar.png b/public/images/medium/no_avatar.png new file mode 100644 index 00000000..b4aa610f Binary files /dev/null and b/public/images/medium/no_avatar.png differ diff --git a/public/images/original/no_avatar.png b/public/images/original/no_avatar.png new file mode 100644 index 00000000..929a21e8 Binary files /dev/null and b/public/images/original/no_avatar.png differ diff --git a/public/images/small/no_avatar.png b/public/images/small/no_avatar.png new file mode 100644 index 00000000..b7603d67 Binary files /dev/null and b/public/images/small/no_avatar.png differ