Merge branch 'outfit_thumbnails'

This commit is contained in:
Emi Matchu 2012-07-31 11:21:28 -04:00
commit 38a9e620c4
48 changed files with 2258 additions and 1228 deletions

View file

@ -41,6 +41,13 @@ gem 'newrelic_rpm'
gem 'neopets', :git => 'git://github.com/matchu/neopets.git'
gem "mini_magick", "~> 3.4"
gem "fog", "~> 1.1.2"
gem "carrierwave", "~> 0.5.8"
gem "parallel", "~> 0.5.17"
group :development_async do
# async wrappers
gem 'eventmachine', :git => 'git://github.com/eventmachine/eventmachine.git'

View file

@ -86,6 +86,8 @@ GEM
arel (2.0.10)
bcrypt-ruby (2.1.4)
builder (2.1.2)
carrierwave (0.5.8)
activesupport (~> 3.0)
character-encodings (0.4.1)
chronic (0.6.7)
closure-compiler (1.1.4)
@ -100,11 +102,23 @@ GEM
eventmachine
erubis (2.6.6)
abstract (>= 1.0.0)
excon (0.9.6)
factory_girl (2.3.2)
activesupport
factory_girl_rails (1.4.0)
factory_girl (~> 2.3.0)
railties (>= 3.0.0)
fog (1.1.2)
builder
excon (~> 0.9.0)
formatador (~> 0.2.0)
mime-types
multi_json (~> 1.0.3)
net-scp (~> 1.0.4)
net-ssh (>= 2.1.3)
nokogiri (~> 1.5.0)
ruby-hmac
formatador (0.2.1)
haml (3.0.25)
hoptoad_notifier (2.4.11)
activesupport
@ -122,13 +136,20 @@ GEM
treetop (~> 1.4.8)
memcache-client (1.8.5)
mime-types (1.17.2)
mini_magick (3.4)
subexec (~> 0.2.1)
msgpack (0.4.6)
multi_json (1.0.4)
mysql2 (0.2.6)
net-scp (1.0.4)
net-ssh (>= 1.99.1)
net-ssh (2.3.0)
newrelic_rpm (3.3.3)
nokogiri (1.5.3)
open4 (1.3.0)
openneo-auth-signatory (0.1.0)
ruby-hmac
parallel (0.5.17)
polyglot (0.3.3)
rack (1.2.5)
rack-fiber_pool (0.9.2)
@ -189,6 +210,7 @@ GEM
sinatra (1.2.8)
rack (~> 1.1)
tilt (>= 1.2.2, < 2.0)
subexec (0.2.1)
swf_converter (0.0.3)
thor (0.14.6)
tilt (1.3.3)
@ -213,6 +235,7 @@ PLATFORMS
DEPENDENCIES
RocketAMF!
addressable
carrierwave (~> 0.5.8)
character-encodings (~> 0.4.1)
compass (~> 0.10.1)
devise (~> 1.1.5)
@ -221,10 +244,12 @@ DEPENDENCIES
em-synchrony!
eventmachine!
factory_girl_rails (~> 1.0)
fog (~> 1.1.2)
haml (~> 3.0.18)
hoptoad_notifier
jammit (~> 0.5.3)
memcache-client (~> 1.8.5)
mini_magick (~> 3.4)
msgpack (~> 0.4.3)
mysql2 (< 0.3)
mysqlplus!
@ -232,6 +257,7 @@ DEPENDENCIES
newrelic_rpm
nokogiri (~> 1.5.2)
openneo-auth-signatory (~> 0.1.0)
parallel (~> 0.5.17)
rack-fiber_pool
rails (= 3.0.5)
rdiscount (~> 1.6.5)

View file

@ -2,18 +2,11 @@ class OutfitsController < ApplicationController
before_filter :find_authorized_outfit, :only => [:update, :destroy]
def create
Rails.logger.debug "Signed in?: #{user_signed_in?}"
Rails.logger.debug "User 1: #{current_user.inspect}"
@outfit = Outfit.build_for_user(current_user, params[:outfit])
Rails.logger.debug "User 2: #{current_user.inspect}"
if @outfit.save
Rails.logger.debug "User 3: #{current_user.inspect}"
render :json => @outfit.id
Rails.logger.debug "User 4: #{current_user.inspect}"
render :json => @outfit
else
Rails.logger.debug "User 5: #{current_user.inspect}"
render_outfit_errors
Rails.logger.debug "User 6: #{current_user.inspect}"
end
end
@ -82,7 +75,7 @@ class OutfitsController < ApplicationController
def update
if @outfit.update_attributes(params[:outfit])
render :json => true
render :json => @outfit
else
render_outfit_errors
end

View file

@ -1,4 +1,12 @@
module ApplicationHelper
def absolute_url(path_or_url)
if path_or_url.include?('://') # already an absolute URL
path_or_url
else # a relative path
request.protocol + request.host_with_port + path_or_url
end
end
def add_body_class(class_name)
@body_class ||= ''
@body_class << " #{class_name}"
@ -100,6 +108,23 @@ module ApplicationHelper
def origin_tag(value)
hidden_field_tag 'origin', value, :id => nil
end
def open_graph(properties)
if @open_graph
@open_graph.merge! properties
else
@open_graph = properties
end
end
def open_graph_tags
if @open_graph
@open_graph.inject('') do |output, property|
key, value = property
output + tag(:meta, :property => "og:#{key}", :content => value)
end.html_safe
end
end
def return_to_field_tag
hidden_field_tag :return_to, request.fullpath

View file

@ -12,10 +12,23 @@ class Outfit < ActiveRecord::Base
attr_accessible :name, :pet_state_id, :starred, :worn_and_unworn_item_ids
scope :wardrobe_order, order('starred DESC', :name)
mount_uploader :image, OutfitImageUploader
before_save :update_enqueued_image
after_commit :enqueue_image!
def as_json(more_options={})
serializable_hash :only => [:id, :name, :pet_state_id, :starred],
:methods => [:color_id, :species_id, :worn_and_unworn_item_ids]
:methods => [:color_id, :species_id, :worn_and_unworn_item_ids,
:image_versions, :image_enqueued, :image_layers_hash]
end
def image_versions
{}.tap do |versions|
versions[:large] = image.url
image.versions.each { |name, version| versions[name] = version.url }
end
end
def closet_item_ids
@ -64,9 +77,45 @@ class Outfit < ActiveRecord::Base
end
self.item_outfit_relationships = new_rels
end
def worn_item_ids
worn_and_unworn_item_ids[:worn]
# Returns the array of SwfAssets representing each layer of the output image,
# ordered from bottom to top. Careful: this method is memoized, so if the
# image layers change after its first call we'll get bad results.
def image_layers
@image_layers ||= visible_assets_with_images.sort { |a, b| a.zone.depth <=> b.zone.depth }
end
# Creates and writes the thumbnail images for this outfit iff the new image
# would be different than the current one. (Writes to file in development,
# S3 in production.) If the image is updated, updates the image layers hash
# and runs #save! on the record, so any other changes will also be saved.
def write_image!
if image_layers_dirty?
Tempfile.open(['outfit_image', '.png']) do |image|
create_image! image
self.image_layers_hash = generate_image_layers_hash
self.image = image
self.image_enqueued = false
save!
end
end
self.image
end
# Enqueue an image write iff the new image would be different than the
# current one.
def enqueue_image!
Resque.enqueue(OutfitImageUpdate, id)
end
def update_enqueued_image
self.image_enqueued = (image_layers_dirty?)
true
end
def s3_key(size)
URI.encode("#{id}/#{size.join 'x'}.png")
end
def self.build_for_user(user, params)
@ -82,5 +131,87 @@ class Outfit < ActiveRecord::Base
outfit.attributes = params
end
end
protected
# Creates a 600x600 PNG image of this outfit, writing to the given output
# file.
def create_image!(output)
unless image_layers.empty?
temp_image_files = Parallel.map(image_layers, :in_threads => 8) do |swf_asset|
image_file = Tempfile.open(['outfit_layer', '.png'])
write_temp_swf_asset_image!(swf_asset, image_file)
image_file.close
image_file
end
# Here we do some awkwardness to get the exact ImageMagick command we
# want, though it's still less awkward than handling the command
# ourselves. Give all of the temporary images as input, flatten them and
# write them to the output path.
command = MiniMagick::CommandBuilder.new('convert')
temp_image_files.each { |image_file| command.push image_file.path }
command.layers 'flatten'
command.push output.path
# Though the above command really is sufficient, we still need a dummy
# image to handle execution.
output_image = MiniMagick::Image.new(output.path)
output_image.run(command)
temp_image_files.each(&:unlink)
else
output.close
end
end
def visible_assets
biology_assets = pet_state.swf_assets
object_assets = SwfAsset.object_assets.
fitting_body_id(pet_state.pet_type.body_id).for_item_ids(worn_item_ids)
# Now for fun with bitmasks! Rather than building a bunch of integer arrays
# here, we instead go low-level and use bit-level operations. Build the
# bitmask by parsing the binary string (reversing it to get the lower zone
# numbers on the right), then OR them all together to get the mask
# representing all the restricted zones. (Note to self: why not just store
# in this format in the first place?)
restrictors = biology_assets + worn_items
restricted_zones_mask = restrictors.inject(0) do |mask, restrictor|
mask | restrictor.zones_restrict.reverse.to_i(2)
end
# Now, check each asset's zone is not restricted in the bitmask using
# bitwise operations: shift 1 to the zone_id position, then AND it with
# the restricted zones mask. If we get 0, then the bit for that zone ID was
# not turned on, so the zone is not restricted and this asset is visible.
all_assets = biology_assets + object_assets
all_assets.select { |a| (1 << (a.zone_id - 1)) & restricted_zones_mask == 0 }
end
def visible_assets_with_images
visible_assets.select(&:has_image?)
end
# Generate 8-char hex digest representing visible image layers for this outfit.
# Hash function should be decently collision-resistant.
def generate_image_layers_hash
@generated_image_layers_hash ||=
Digest::MD5.hexdigest(image_layers.map(&:id).join(',')).first(8)
end
def image_layers_dirty?
generate_image_layers_hash != self.image_layers_hash
end
IMAGE_BASE_SIZE = [600, 600]
def write_temp_swf_asset_image!(swf_asset, file)
key = swf_asset.s3_key(IMAGE_BASE_SIZE)
bucket = SwfAsset::IMAGE_BUCKET
data = bucket.get(key)
file.binmode # write in binary mode
file.truncate(0) # clear the file
file.write data # write the new data
end
end

View file

@ -0,0 +1,15 @@
class OutfitImageUpdate
@queue = :outfit_image_updates
def self.perform(id)
Outfit.find(id).write_image!
end
# Represents an outfit image update for an outfit that existed before this
# feature was built. Its queue has a lower priority, so new outfits will
# be updated before retroactively converted outfits.
class Retroactive < OutfitImageUpdate
@queue = :retroactive_outfit_image_updates
end
end

View file

@ -0,0 +1,39 @@
require 'carrierwave/processing/mime_types'
class OutfitImageUploader < CarrierWave::Uploader::Base
include CarrierWave::MimeTypes
include CarrierWave::MiniMagick
# Settings for S3 storage. Will only be used on production.
fog_directory 'impress-outfit-images'
fog_attributes 'Cache-Control' => "max-age=#{15.minutes}",
'Content-Type' => 'image/png'
process :set_content_type
version :medium do
process :resize_to_fill => [300, 300]
end
version :small, :from_version => :medium do
process :resize_to_fill => [150, 150]
end
def filename
"preview.png"
end
def store_dir
"outfits/#{partition_dir}"
end
# 123006789 => "123/006/789"
def partition_dir
partitions.map { |partition| "%03d" % partition }.join('/')
end
# 123006789 => [123, 6, 789]
def partitions
[6, 3, 0].map { |n| model.id / 10**n % 1000 }
end
end

View file

@ -15,8 +15,14 @@ class SwfAsset < ActiveRecord::Base
set_inheritance_column 'inheritance_type'
IMAGE_SIZES = {
:small => [150, 150],
:medium => [300, 300],
:large => [600, 600]
}
include SwfConverter
converts_swfs :size => [600, 600], :output_sizes => [[150, 150], [300, 300], [600, 600]]
converts_swfs :size => IMAGE_SIZES[:large], :output_sizes => IMAGE_SIZES.values
def local_swf_path
LOCAL_ASSET_DIR.join(local_path_within_outfit_swfs)
@ -74,6 +80,21 @@ class SwfAsset < ActiveRecord::Base
end
end
end
def image_version
converted_at.to_i
end
def image_url(size=IMAGE_SIZES[:large])
host = ASSET_HOSTS[:swf_asset_images]
size_key = size.join('x')
"http://#{host}/#{s3_path}/#{size_key}.png?#{image_version}"
end
def images
IMAGE_SIZES.values.map { |size| {:size => size, :url => image_url(size)} }
end
def convert_swf_if_not_converted!
if needs_conversion?
@ -161,7 +182,7 @@ class SwfAsset < ActiveRecord::Base
:zones_restrict => zones_restrict,
:is_body_specific => body_specific?,
:has_image => has_image?,
:s3_path => s3_path
:images => images
}
if options[:for] == 'wardrobe'
json[:local_path] = local_url

View file

@ -3,12 +3,13 @@
@import "partials/context_button"
@import "partials/icon"
@import "partials/outfit"
@import star
$object-padding: 6px
$nc-icon-size: 16px
$preview-dimension: 400px
$preview-dimension: 380px
$sidebar-margin: 20px
$sidebar-width: 400px
$sidebar-unit-horizontal-padding: 24px
@ -21,88 +22,23 @@ $outfit-header-padding: 24px
$outfit-content-width: $sidebar-unit-inner-width - $outfit-thumbnail-size - $outfit-thumbnail-margin - 32px
$outfit-content-inner-width: $outfit-content-width - $outfit-header-padding
=user-select($select)
select: unquote($select)
+experimental(user-select, $select, -moz, -webkit, not -o, not -ms, -khtml, official)
=active-mode
color: $text-color
font-weight: bold
=outfit
+outfit-star-shifted
padding: .25em 0
//.outfit-thumbnail
float: left
height: $outfit-thumbnail-size
margin-right: $outfit-thumbnail-margin
overflow: hidden
position: relative
width: $outfit-thumbnail-size
img
height: $outfit-thumbnail-original-size
left: -$outfit-thumbnail-original-size / 4
position: absolute
top: -$outfit-thumbnail-original-size / 4
width: $outfit-thumbnail-original-size
.outfit-delete
+reset-awesome-button
+opacity(.5)
font-size: 150%
float: right
line-height: 1
margin-top: -.125em
padding: .125em .25em
&:hover
+opacity(1)
background: $module-bg-color
header
display: block
padding-left: $outfit-header-padding
h4
cursor: pointer
display: inline
&:hover
text-decoration: underline
h4, .outfit-rename-field
font-size: 115%
.outfit-rename-button, .outfit-rename-form
display: none
.outfit-rename-button
+opacity(.75)
font-size: 75%
margin-left: 1em
.outfit-url
+opacity(.5)
background: transparent
border-width: 0
width: $outfit-content-inner-width
&:hover
+opacity(1)
border-width: 1px
.outfit-delete-confirmation
display: none
font-size: 75%
span
color: red
a
margin: 0 .25em
&.active
background: $module-bg-color
&.confirming-deletion
.outfit-delete
visibility: hidden
.outfit-url
display: none
.outfit-delete-confirmation
display: block
&.renaming
h4
display: none
.outfit-rename-form
display: inline
&:hover
.outfit-rename-button
display: none
&:hover
.outfit-rename-button
display: inline
=sidebar-navbar-unselected
background: transparent
border-bottom: 1px solid $soft-border-color
font-weight: normal
=sidebar-navbar-selected
background: white
border-bottom-color: white
font-weight: bold
=sidebar-view-child
margin:
@ -144,7 +80,7 @@ body.outfits-edit
position: left center
repeat: no-repeat
padding-left: 20px
#save-outfit, #save-outfit-not-signed-in, #save-current-outfit, #save-outfit-copy, #save-outfit-finish
#save-outfit, #save-outfit-not-signed-in, #save-current-outfit, #save-outfit-finish
+loud-awesome-button-color
#current-outfit-permalink, #shared-outfit-permalink
display: none
@ -207,11 +143,14 @@ body.outfits-edit
&.image-active
#preview-mode-image
+active-mode
#report-broken-image
#preview-mode-note, #report-broken-image
display: block
&.can-download
#preview-download-image
display: inline-block
// Phasing out the image download section. Not confident enough yet to
// *remove* it, depending on user feedback, but that's a TODO for down
// the road if hiding goes well.
// &.can-download
// #preview-download-image
// display: inline-block
#preview-mode-toggle
+border-radius(.5em)
border: 1px solid $module-border-color
@ -249,34 +188,27 @@ body.outfits-edit
em
font-style: normal
text-decoration: underline
#report-broken-image
#preview-mode-note, #report-broken-image
display: none
#preview-sidebar
+border-radius(10px)
border: 1px solid $soft-border-color
float: left
height: $preview-dimension
margin-left: $sidebar-margin
margin-bottom: 1em
overflow: auto
width: $container_width - $preview-dimension - $sidebar-margin - 2px
width: $container_width - $preview-dimension - $sidebar-margin
&.viewing-outfits
#preview-closet
display: none
#preview-outfits
display: block
&.viewing-saving-outfit
height: auto
max-height: 100%
&.sharing
#preview-closet
display: none
#preview-saving-outfit
#preview-sharing
display: block
.sidebar-view
h2
margin:
bottom: .25em
left: $sidebar-unit-horizontal-padding
margin: 1.5em 0
#preview-closet
h2
margin-bottom: 0
@ -378,7 +310,6 @@ body.outfits-edit
width: 100%
#preview-sidebar
float: right
height: 100%
margin: 0
position: relative
width: $sidebar-width
@ -461,19 +392,283 @@ body.outfits-edit
#preview-outfits
display: none
text-align: left
$outfit-inner-size: 110px
$outfit-margin: 1px
$outfit-outer-size: $outfit-inner-size + ($outfit-margin * 2)
> ul
+outfits-list
+sidebar-view-child
background: image-url("loading.gif") no-repeat center top
display: block
display: none
font-family: $main-font
list-style: none
margin:
bottom: 1em
margin: 0 auto 1em
min-height: 16px
> li
+outfit
width: $outfit-outer-size * 3
&.loaded
background: transparent
> li
height: $outfit-inner-size
margin: $outfit-margin
width: $outfit-inner-size
$outfit-header-h-padding: 4px
$outfit-header-v-padding: 2px
$outfit-header-inner-width: $outfit-inner-size - (2 * $outfit-header-h-padding)
$outfit-header-inner-height: 12px
$outfit-header-outer-height: $outfit-header-inner-height + (2 * $outfit-header-v-padding)
header, footer, .outfit-delete-confirmation
font-size: $outfit-header-inner-height
padding: $outfit-header-v-padding $outfit-header-h-padding
width: $outfit-header-inner-width
header
+opacity(0.75)
bottom: 0
cursor: pointer
footer, .outfit-delete-confirmation
display: none
.outfit-delete-confirmation
+outfit-banner
+outfit-banner-background(rgb(255, 50, 50))
text-align: center
top: 0
span
font-weight: bold
$outfit-thumbnail-size: 150px
$outfit-thumbnail-h-offset: ($outfit-inner-size - $outfit-thumbnail-size) / 2
$outfit-thumbnail-v-offset: $outfit-thumbnail-h-offset - ($outfit-header-outer-height / 4)
.outfit-thumbnail-wrapper
+opacity(.5)
background:
image: url(/images/outfits/small_default.png)
position: center center
size: $outfit-inner-size $outfit-inner-size
cursor: pointer
height: $outfit-thumbnail-size
left: $outfit-thumbnail-h-offset
position: absolute
top: $outfit-thumbnail-v-offset
width: $outfit-thumbnail-size
z-index: 1
.outfit-thumbnail
display: none
.outfit-star
bottom: 0
margin-right: 4px
.outfit-delete
float: right
.outfit-rename-button
float: left
.outfit-rename-button, .outfit-delete
font-size: 85%
text-decoration: none
&:hover
text-decoration: underline
.outfit-rename-form
display: none
input
background: transparent
border: 1px solid white
width: 6em
&:hover
header
+opacity(1)
.outfit-thumbnail
+opacity(0.75)
footer
display: block
&.active
header
+opacity(1)
font-weight: bold
.outfit-thumbnail
+opacity(1)
&.confirming-deletion
footer
display: none
.outfit-delete-confirmation
display: block
&.renaming
.outfit-name
display: none
.outfit-rename-form
display: inline
&.thumbnail-available
background: transparent
.outfit-thumbnail-wrapper
background-image: none
.outfit-thumbnail
display: block
&.loading
.outfit-star
background-image: image-url("loading_outfit_pane.gif")
#preview-outfits-not-logged-in
text-align: center
overflow-x: hidden
img
border:
color: $module-border-color
style: solid
width: 1px 0
figure
display: block
margin: 0 0 1em 0
padding: 0
figcaption
display: block
font-weight: bold
p
+sidebar-view-child
font-size: 85%
#preview-outfits-log-in
+awesome-button
+loud-awesome-button-color
#preview-sharing
display: none
#preview-sharing-urls
+sidebar-view-child
display: none
margin:
bottom: 1em
top: 1em
li
display: block
padding: .25em 0
width: 100%
label
display: block
font-weight: bold
input
display: block
width: 100%
#preview-sharing-url-formats
+sidebar-view-child
+user-select(none)
// remove whitespace between inline-block elements
display: none
font-size: 0
text-align: center
li
+inline-block
border: 1px solid $module-border-color
border-left-width: 0
border-right-color: $soft-border-color
color: $soft-text-color
cursor: pointer
font-size: 12px
padding: 0 2em
&.active
background: $module-bg-color
color: inherit
font-weight: bold
&:first-child
+border-top-left-radius(5px)
+border-bottom-left-radius(5px)
border-left-width: 1px
&:last-child
+border-top-right-radius(5px)
+border-bottom-right-radius(5px)
border-right-color: $module-border-color
#preview-sharing-thumbnail-wrapper
border: 1px solid $soft-border-color
display: block
height: 150px
margin: 1em auto 0
position: relative
width: 150px
#preview-sharing-thumbnail-loading
height: 100%
left: 0
position: absolute
top: 0
width: 100%
span
color: $soft-text-color
font-size: 85%
margin-top: -0.75em
position: absolute
text-align: center
top: 50%
width: 100%
#preview-sharing-thumbnail, #preview-sharing-thumbnail-generating
display: none
#preview-sharing-beta-note
+sidebar-view-child
+warning
font-size: 85%
margin-top: 1em
text-align: center
&.urls-loaded
#preview-sharing-thumbnail-saving
display: none
#preview-sharing-urls, #preview-sharing-url-formats, #preview-sharing-thumbnail-generating
display: block
&.urls-loaded.thumbnail-loaded
#preview-sharing-thumbnail-loading
display: none
#preview-sharing-thumbnail
display: block
&.urls-loaded.thumbnail-available
#preview-sharing-thumbnail-loading
+opacity(0.85)
#preview-sharing-thumbnail
display: block
.preview-sidebar-nav
float: right
@ -481,6 +676,49 @@ body.outfits-edit
margin:
right: $sidebar-unit-horizontal-padding
top: 1em
$sidebar-border-radius: 10px
$sidebar-navbar-inner-width: $sidebar-width - 2px
$sidebar-navbar-child-outer-width: floor($sidebar-navbar-inner-width / 3)
#preview-sidebar
#preview-sidebar-navbar-closet
+sidebar-navbar-selected
&.viewing-outfits, &.sharing
#preview-sidebar-navbar-closet
+sidebar-navbar-unselected
&.viewing-outfits #preview-sidebar-navbar-outfits, &.sharing #preview-sidebar-navbar-sharing
+sidebar-navbar-selected
#preview-sidebar-navbar
+border-radius($sidebar-border-radius $sidebar-border-radius 0 0)
+clearfix
+header-text
background: $module-bg-color
border: 1px solid $soft-border-color
border-bottom: 0
font-size: 150%
> div
+sidebar-navbar-unselected
cursor: pointer
float: left
border-left: 1px solid $soft-border-color
padding: .5em 0
text-align: center
width: $sidebar-navbar-child-outer-width
&:first-child
border-left: 0
#preview-sidebar-content
+border-radius(0 0 $sidebar-border-radius $sidebar-border-radius)
border: 1px solid $soft-border-color
border-top: 0
height: 300px
overflow: auto
#save-success, #save-error, #outfit-not-found, #preview-sidebar-donation-request
+sidebar-view-child
@ -507,25 +745,10 @@ body.outfits-edit
+opacity(.5)
display: none
#new-outfit
+outfit
+sidebar-view-child
display: none
h4
display: inline
&:hover
text-decoration: none
.outfit-star
margin-top: .5em
#new-outfit-name
font: inherit
line-height: 1
#preview-saving-outfit
display: none
padding-bottom: 1em
#pet-type-form, #pet-state-form, #preview-swf, #preview-search-form
position: relative
@ -541,10 +764,10 @@ body.outfits-edit
display: none
form#save-outfit-form
+outfit
+outfit-star-shifted
display: none
margin-right: 0
padding: 0
padding: 0
.outfit-star, input, button
+inline-block
@ -572,8 +795,11 @@ body.outfits-edit
display: none
#save-current-outfit, #save-outfit-copy
display: inline-block
#current-outfit-permalink
display: inline-block
// Phasing out permalink. Shared outfit links have been straight-up
// removed, but this may stay depending on user feedback. Otherwise,
// removing it is TODO down the road.
// #current-outfit-permalink
// display: inline-block
&.saving-outfit
#save-outfit-form
display: block
@ -581,6 +807,10 @@ body.outfits-edit
display: none
.preview-search-form-your-items
+inline-block
#preview-outfits-not-logged-in
display: none
#preview-outfits-list
display: block
&.user-not-signed-in
#save-outfit-not-signed-in

View file

@ -1,27 +1,55 @@
@import "partials/outfit"
@import star
$outfit-inner-height: 150px
$outfit-inner-width: 150px
$outfit-banner-h-padding: 4px
$outfit-banner-v-padding: 2px
$outfit-banner-inner-width: $outfit-inner-width - (2 * $outfit-banner-h-padding)
body.outfits-index
#outfits
list-style: none
li
+outfit-star
clear: left
float: left
margin-bottom: .5em
h4
float: left
width: 12em
.outfit-edit-link, form
float: left
font-size: 85%
margin-left: 1em
.outfit-edit-link
+awesome-button
+outfits-list
> li
height: $outfit-inner-height
margin: 2px
width: $outfit-inner-width
header, footer
padding: $outfit-banner-v-padding $outfit-banner-h-padding
width: $outfit-banner-inner-width
footer
display: none
.outfit-edit-link
float: left
text-decoration: none
form
float: right
.outfit-delete-button
margin: 0
padding: 0
.outfit-edit-link, .outfit-delete-button
&:hover
text-decoration: underline
.outfit-star
cursor: auto
.outfit-name
text-decoration: none
&:hover
text-decoration: underline
&:hover
footer
display: block
.outfit-delete-button
margin: 0
+reset-awesome-button

View file

@ -15,8 +15,6 @@
background-image: image-url("star.png")
&.loading .outfit-star
background-image: image-url("loading.gif")
&.loading.active .outfit-star
background-image: image-url("loading_current_outfit.gif")
=outfit-star-shifted
+outfit-star

View file

@ -0,0 +1,37 @@
=outfit
+inline-block
+outfit-star
overflow: hidden
position: relative
header, footer
+outfit-banner
+outfit-banner-background(black)
header
bottom: 0
footer
top: 0
a
color: white
=outfits-list
// remove whitespace between inline-block elements
font-size: 0
list-style: none
> li
+outfit
font-size: 14px
=outfit-banner
color: white
left: 0
position: absolute
z-index: 2
=outfit-banner-background($color)
background: $color
background: rgba($color, 0.75)

View file

@ -12,7 +12,7 @@
%ul#report-assets
- @swf_assets.each do |swf_asset|
%li
= link_to image_tag(swf_asset.s3_url([150, 150])), swf_asset.url
= link_to image_tag(swf_asset.image_url([150, 150])), swf_asset.url
- unless swf_asset.image_pending_repair?
= form_tag(:action => :create) do
= hidden_field_tag 'swf_asset_remote_id', swf_asset.remote_id

View file

@ -14,6 +14,7 @@
= yield :stylesheets
= stylesheet_link_tag "compiled/screen"
= yield :meta
= open_graph_tags
= csrf_meta_tag
= signed_in_meta_tag
%body{:class => body_class}

View file

@ -1,6 +1,14 @@
= outfit_li_for(outfit) do
.outfit-star
%h4= link_to outfit.name, outfit
= link_to_edit_outfit 'Edit', outfit, :class => 'outfit-edit-link'
= button_to('Delete', outfit, :method => 'delete', :class => 'outfit-delete-button', :confirm => "Are you sure you want to delete the outfit #{outfit.name}?")
- if outfit.image?
= link_to image_tag(outfit.image.small.url), outfit
%header
.outfit-star
= link_to outfit.name, outfit, :class => 'outfit-name'
%footer
= link_to_edit_outfit 'edit', outfit, :class => 'outfit-edit-link'
= button_to 'delete', outfit, :method => 'delete', :class => 'outfit-delete-button', :confirm => "Are you sure you want to delete the outfit #{outfit.name}?"

View file

@ -17,14 +17,10 @@
#save-outfit-wrapper
%a#current-outfit-permalink{:target => '_blank'}
= image_tag 'link_go.png', :alt => 'Permalink', :title => 'Permalink to current outfit'
%a#shared-outfit-permalink{:target => '_blank'}
= image_tag 'link_go.png', :alt => 'Permalink', :title => 'Permalink to shared outfit'
%input#shared-outfit-url.outfit-url{:type => 'text'}
%button#share-outfit Share outfit
%button#save-outfit Save outfit
%button#save-outfit-not-signed-in Log in to save
%button#save-outfit-copy Save as…
%button#save-current-outfit Save &quot;<span>current outfit</span>&quot;
%button#save-outfit-copy Save a copy
%form#save-outfit-form
.outfit-star
%input#save-outfit-name{:type => 'text', :placeholder => 'Outfit name'}
@ -62,30 +58,67 @@
%em donate
at least $5 to help upgrade the server. Thanks!
#preview-sidebar
#outfit-not-found Outfit not found
#save-success Outfit successfully saved
#save-error
#preview-closet.sidebar-view
%a#preview-sidebar-nav-outfits.preview-sidebar-nav{:href => '#'} Your outfits
%h2 Closet
%ul
%p#fullscreen-copyright
Images © 2000-2010 Neopets, Inc. All Rights Reserved.
Used With Permission
#preview-outfits.sidebar-view
%a#preview-sidebar-nav-closet.preview-sidebar-nav{:href => "#"} &larr; Back to Closet
%h2 Your outfits
%ul
#preview-saving-outfit.sidebar-view
%a#preview-sidebar-nav-cancel-save.preview-sidebar-nav{:href => '#'} &larr; Cancel
%h2 Saving new outfit
#new-outfit
%form#new-outfit-form
%header
.outfit-star
%h4
%input#new-outfit-name{:type => 'text', :placeholder => 'Outfit name'}
%button{:type => 'submit'} Save
%nav#preview-sidebar-navbar
#preview-sidebar-navbar-closet Closet
#preview-sidebar-navbar-sharing Sharing
#preview-sidebar-navbar-outfits Outfits
#preview-sidebar-content
#outfit-not-found Outfit not found
#save-success Outfit successfully saved
#save-error
#preview-closet.sidebar-view
%ul
%p#fullscreen-copyright
Images © 2000-2010 Neopets, Inc. All Rights Reserved.
Used With Permission
#preview-outfits.sidebar-view
%ul#preview-outfits-list
#preview-outfits-not-logged-in
%figure
= image_tag 'outfits_welcome.png'
%figcaption Ready to become a pro designer?
:markdown
We know how hard it can be to keep track of your ideas,
especially if you end up having a lot of them.
**But Dress to Impress makes it easy.**
Once you have an idea for an outfit, you can **build it,
save it, and view it again later**, either to update your
design or finally make your dream a reality.
**Thousands of users have already saved tens of thousands of
outfits &mdash; will you be next?**
= link_to 'Log in to save this outfit', login_path_with_return_to, :id => 'preview-outfits-log-in'
#preview-sharing.sidebar-view
#preview-sharing-thumbnail-wrapper
#preview-sharing-thumbnail-loading
= image_tag 'outfits/small_loading.gif'
%span#preview-sharing-thumbnail-saving Saving&hellip;
%span#preview-sharing-thumbnail-generating Generating&hellip;
%img#preview-sharing-thumbnail
%p#preview-sharing-beta-note
We're currently beta testing outfit image generation. It might be
slow or not work at all, and we might have to take it down.
Still, we're really excited about this feature, and we hope you
are, too!
%ul#preview-sharing-urls
%li
%label{:for => 'preview-sharing-permalink-url'} Outfit page
%input#preview-sharing-permalink-url.outfit-url{:type => 'text'}
%li
%label{:for => 'preview-sharing-large-image-url'} Large image
%input#preview-sharing-large-image-url.outfit-url{:type => 'text'}
%li
%label{:for => 'preview-sharing-medium-image-url'} Medium image
%input#preview-sharing-medium-image-url.outfit-url{:type => 'text'}
%li
%label{:for => 'preview-sharing-small-image-url'} Small image
%input#preview-sharing-small-image-url.outfit-url{:type => 'text'}
%ul#preview-sharing-url-formats
%li.active{'data-format' => 'plain'} Plain
%li{'data-format' => 'html'} HTML
%li{'data-format' => 'bbcode'} BBCode
%form#preview-search-form
%header
%h2 Add an item
@ -132,18 +165,20 @@
%script#outfit-template{:type => 'text/x-jquery-tmpl'}
<li class="outfit-${id}{{if starred}} starred{{/if}}">
%header
%button.outfit-delete &times;
.outfit-star
%h4 ${name}
%a.outfit-rename-button{:href => '#'} rename
%span.outfit-name ${name}
%form.outfit-rename-form
%input.outfit-rename-field{:type => 'text'}
%input.outfit-url{:type => 'text', :value => "http://#{request.host}/outfits/${id}"}
%footer
%a.outfit-rename-button{:href => '#'} rename
%a.outfit-delete{:href => '#'} delete
.outfit-thumbnail-wrapper
%img.outfit-thumbnail
.outfit-delete-confirmation
%span Delete forever?
%span Delete?
%a.outfit-delete-confirmation-yes{:href => '#'} yes
\/
%a.outfit-delete-confirmation-no{:href => '#'} no, thanks
%a.outfit-delete-confirmation-no{:href => '#'} no
</li>
- content_for :javascripts do
= include_javascript_libraries :jquery, :swfobject, :jquery_tmpl

View file

@ -1,6 +1,11 @@
- title(@outfit.name || "Shared outfit")
- content_for :before_title, campaign_progress
- open_graph :type => 'openneo-impress:outfit', :title => yield(:title),
:url => outfit_url(@outfit)
- if @outfit.image?
- open_graph :image => absolute_url(@outfit.image.url)
= link_to_edit_outfit(@outfit, :class => 'button', :id => 'outfit-wardrobe-link') do
Edit
- unless user_signed_in? && @outfit.user == current_user

View file

@ -0,0 +1,3 @@
ASSET_HOSTS = {
:swf_asset_images => 'd1i4vx4g4uxw7j.cloudfront.net'
}

View file

@ -0,0 +1,20 @@
# By default, we'll have CarrierWave use S3 only on production. (Since each
# asset image has only One True Image no matter the environment, we'll override
# this to use S3 on all environments for those images only.)
CarrierWave.configure do |config|
if Rails.env.production?
s3_config = YAML.load_file Rails.root.join('config', 'aws_s3.yml')
access_key_id = s3_config['access_key_id']
secret_access_key = s3_config['secret_access_key']
config.storage = :fog
config.fog_credentials = {
:provider => 'AWS',
:aws_access_key_id => access_key_id,
:aws_secret_access_key => secret_access_key
}
else
config.storage = :file
end
end

View file

@ -0,0 +1,9 @@
class AddImageToOutfits < ActiveRecord::Migration
def self.up
add_column :outfits, :image, :string
end
def self.down
remove_column :outfits, :image
end
end

View file

@ -0,0 +1,9 @@
class AddImageLayersHashToOutfit < ActiveRecord::Migration
def self.up
add_column :outfits, :image_layers_hash, :string, :length => 8
end
def self.down
remove_column :outfits, :image_layers_hash
end
end

View file

@ -0,0 +1,9 @@
class AddImageEnqueuedToOutfits < ActiveRecord::Migration
def self.up
add_column :outfits, :image_enqueued, :boolean, :null => false, :default => false
end
def self.down
remove_column :outfits, :image_enqueued
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20120521164652) do
ActiveRecord::Schema.define(:version => 20120725232903) do
create_table "auth_servers", :force => true do |t|
t.string "short_name", :limit => 10, :null => false
@ -116,8 +116,10 @@ ActiveRecord::Schema.define(:version => 20120521164652) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
t.boolean "starred", :default => false, :null => false
t.boolean "starred", :default => false, :null => false
t.string "image"
t.string "image_layers_hash"
t.boolean "image_enqueued", :default => false, :null => false
end
add_index "outfits", ["pet_state_id"], :name => "index_outfits_on_pet_state_id"

11
lib/tasks/outfits.rake Normal file
View file

@ -0,0 +1,11 @@
namespace :outfits do
desc 'Retroactively enqueue image updates for outfits saved to user accounts'
task :retroactively_enqueue => :environment do
outfits = Outfit.select([:id]).where('image IS NULL AND user_id IS NOT NULL')
puts "Enqueuing #{outfits.count} outfits"
outfits.find_each do |outfit|
Resque.enqueue(OutfitImageUpdate::Retroactive, outfit.id)
end
puts "Successfully enqueued."
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -57,7 +57,7 @@ Partial.ItemSet = function ItemSet(wardrobe, selector) {
var item, no_assets, li, no_assets_message;
for(var i = 0, l = specific_items.length; i < l; i++) {
item = specific_items[i];
no_assets = item.couldNotLoadAssetsFitting(wardrobe.outfit.getPetType());
no_assets = item.couldNotLoadAssetsFitting(wardrobe.outfits.getPetType());
li = $('li.object-' + item.id).toggleClass('no-assets', no_assets);
(function (li) {
no_assets_message = li.find('span.no-assets-message');
@ -103,8 +103,8 @@ Partial.ItemSet = function ItemSet(wardrobe, selector) {
}
li.append(img).append(controls).append(info_link).append(item.name).appendTo(ul);
}
setClosetItems(wardrobe.outfit.getClosetItems());
setOutfitItems(wardrobe.outfit.getWornItems());
setClosetItems(wardrobe.outfits.getClosetItems());
setOutfitItems(wardrobe.outfits.getWornItems());
}
$('span.no-assets-message').live('mouseover', function () {
@ -117,9 +117,9 @@ Partial.ItemSet = function ItemSet(wardrobe, selector) {
no_assets_full_message.removeAttr('style');
});
wardrobe.outfit.bind('updateItemAssets', function () { setHasAssets(wardrobe.outfit.getWornItems()) });
wardrobe.outfit.bind('updateWornItems', setOutfitItems);
wardrobe.outfit.bind('updateClosetItems', setClosetItems);
wardrobe.outfits.bind('updateItemAssets', function () { setHasAssets(wardrobe.outfits.getWornItems()) });
wardrobe.outfits.bind('updateWornItems', setOutfitItems);
wardrobe.outfits.bind('updateClosetItems', setClosetItems);
}
Partial.ItemSet.CONTROL_SETS = {};
@ -151,12 +151,12 @@ Partial.ItemSet.setWardrobe = function (wardrobe) {
}
toggle_fn.closeted = {};
toggle_fn.closeted[true] = $.proxy(wardrobe.outfit, 'closetItem');
toggle_fn.closeted[false] = $.proxy(wardrobe.outfit, 'unclosetItem');
toggle_fn.closeted[true] = $.proxy(wardrobe.outfits, 'closetItem');
toggle_fn.closeted[false] = $.proxy(wardrobe.outfits, 'unclosetItem');
toggle_fn.worn = {};
toggle_fn.worn[true] = $.proxy(wardrobe.outfit, 'wearItem');
toggle_fn.worn[false] = $.proxy(wardrobe.outfit, 'unwearItem');
toggle_fn.worn[true] = $.proxy(wardrobe.outfits, 'wearItem');
toggle_fn.worn[false] = $.proxy(wardrobe.outfits, 'unwearItem');
Partial.ItemSet.setWardrobe = $.noop;
}
@ -164,14 +164,16 @@ Partial.ItemSet.setWardrobe = function (wardrobe) {
View.Closet = function (wardrobe) {
var item_set = new Partial.ItemSet(wardrobe, '#preview-closet ul');
wardrobe.outfit.bind('updateClosetItems', $.proxy(item_set, 'setItems'));
wardrobe.outfits.bind('updateClosetItems', $.proxy(item_set, 'setItems'));
}
View.Fullscreen = function (wardrobe) {
var full = $(document.body).hasClass('fullscreen'), win = $(window),
preview_el = $('#preview'), search_el = $('#preview-search-form'),
preview_swf = $('#preview-swf'), sidebar_el = $('#preview-sidebar'),
footer = $('#footer'), jwindow = $(window), overrideFull = false;
sidebar_content_el = $('#preview-sidebar-content'),
sidebar_navbar_el = $('#preview-sidebar-navbar'), footer = $('#footer'),
jwindow = $(window), overrideFull = false;
function fit() {
if(!overrideFull) {
@ -182,6 +184,7 @@ View.Fullscreen = function (wardrobe) {
if(!full) {
preview_swf.removeAttr('style').css('visibility', 'visible');
preview_el.removeAttr('style');
sidebar_content_el.removeAttr('style');
}
}
}
@ -213,6 +216,12 @@ View.Fullscreen = function (wardrobe) {
preview_swf.css(size.next);
preview_el.height(available.height);
// Now that preview is fit, we fit the sidebar's content element, which
// also has to deal with the constraint of its navbar's height.
var sidebar_content_height = available.height -
sidebar_navbar_el.outerHeight() - 1; // 1px bottom border
sidebar_content_el.height(sidebar_content_height);
}
}
$('#preview').data('fit', fit);
@ -273,32 +282,32 @@ View.Hash = function (wardrobe) {
}
if(new_data.color !== data.color || new_data.species !== data.species) {
wardrobe.outfit.setPetTypeByColorAndSpecies(new_data.color, new_data.species);
wardrobe.outfits.setPetTypeByColorAndSpecies(new_data.color, new_data.species);
}
if(new_data.closet) {
if(!arraysMatch(new_data.closet, data.closet)) {
wardrobe.outfit.setClosetItemsByIds(new_data.closet.slice(0));
wardrobe.outfits.setClosetItemsByIds(new_data.closet.slice(0));
}
} else if(new_data.objects && !arraysMatch(new_data.objects, data.closet)) {
wardrobe.outfit.setClosetItemsByIds(new_data.objects.slice(0));
wardrobe.outfits.setClosetItemsByIds(new_data.objects.slice(0));
} else {
wardrobe.outfit.setClosetItemsByIds([]);
wardrobe.outfits.setClosetItemsByIds([]);
}
if(new_data.objects) {
if(!arraysMatch(new_data.objects, data.objects)) {
wardrobe.outfit.setWornItemsByIds(new_data.objects.slice(0));
wardrobe.outfits.setWornItemsByIds(new_data.objects.slice(0));
}
} else {
wardrobe.outfit.setWornItemsByIds([]);
wardrobe.outfits.setWornItemsByIds([]);
}
if(new_data.name != data.name && new_data.name) {
wardrobe.base_pet.setName(new_data.name);
}
if(new_data.state != data.state) {
wardrobe.outfit.setPetStateById(new_data.state);
wardrobe.outfits.setPetStateById(new_data.state);
}
if(new_data.outfit != data.outfit) {
wardrobe.outfit.setId(new_data.outfit);
wardrobe.outfits.setId(new_data.outfit);
}
if(new_data.search != data.search || new_data.search_offset != data.search_offset) {
wardrobe.search.setItemsByQuery(new_data.search, {offset: new_data.search_offset});
@ -350,27 +359,27 @@ View.Hash = function (wardrobe) {
}
function singleOutfitResponse(event_name, response) {
wardrobe.outfit.bind(event_name, function () {
if(!wardrobe.outfit.in_transaction) response.apply(this, arguments);
wardrobe.outfits.bind(event_name, function () {
if(!wardrobe.outfits.in_transaction) response.apply(this, arguments);
});
}
singleOutfitResponse('updateClosetItems', function (items) {
var item_ids = items.map('id');
var item_ids = items.mapProperty('id');
if(!arraysMatch(item_ids, data.closet)) {
changeQuery({closet: item_ids});
}
});
singleOutfitResponse('updateWornItems', function (items) {
var item_ids = items.map('id'), changes = {};
var item_ids = items.mapProperty('id'), changes = {};
if(!arraysMatch(item_ids, data.objects)) {
changes.objects = item_ids;
}
if(arraysMatch(item_ids, data.closet) || arraysMatch(item_ids, data.objects)) {
changes.closet = undefined;
} else {
changes.closet = wardrobe.outfit.getClosetItems().map('id');
changes.closet = wardrobe.outfits.getClosetItems().mapProperty('id');
}
if(changes.objects || changes.closet) changeQuery(changes);
});
@ -390,7 +399,7 @@ View.Hash = function (wardrobe) {
});
singleOutfitResponse('updatePetState', function (pet_state) {
var pet_type = wardrobe.outfit.getPetType();
var pet_type = wardrobe.outfits.getPetType();
if(pet_state.id != data.state && pet_type && (data.state || pet_state.id != pet_type.pet_state_ids[0])) {
changeQuery({state: pet_state.id});
}
@ -402,7 +411,7 @@ View.Hash = function (wardrobe) {
}
});
wardrobe.outfit.bind('loadOutfit', function (outfit) {
wardrobe.outfits.bind('loadOutfit', function (outfit) {
changeQuery({
closet: outfit.getClosetItemIds(),
color: outfit.pet_type.color_id,
@ -413,7 +422,7 @@ View.Hash = function (wardrobe) {
});
});
wardrobe.outfit.bind('outfitNotFound', function (outfit) {
wardrobe.outfits.bind('outfitNotFound', function (outfit) {
var new_id = outfit ? outfit.id : undefined;
changeQuery({outfit: new_id});
});
@ -430,8 +439,6 @@ View.Hash = function (wardrobe) {
View.Outfits = function (wardrobe) {
var current_outfit_permalink_el = $('#current-outfit-permalink'),
shared_outfit_permalink_el = $('#shared-outfit-permalink'),
shared_outfit_url_el = $('#shared-outfit-url'),
new_outfit_form_el = $('#save-outfit-form'),
new_outfit_name_el = $('#save-outfit-name'),
outfits_el = $('#preview-outfits'),
@ -451,13 +458,6 @@ View.Outfits = function (wardrobe) {
return $('li.outfit-' + outfit.id);
}
function navLinkTo(callback) {
return function (e) {
e.preventDefault();
callback();
}
}
function navigateTo(will_be_viewing) {
var currently_viewing = sidebar_el.attr('class');
if(currently_viewing != will_be_viewing) previously_viewing = currently_viewing;
@ -476,13 +476,20 @@ View.Outfits = function (wardrobe) {
/* Nav */
function showCloset() {
sharing.onHide();
navigateTo('');
}
function showOutfits() {
wardrobe.user.loadOutfits();
sharing.onHide();
wardrobe.outfits.loadOutfits();
navigateTo('viewing-outfits');
}
function showSharing() {
sharing.onShow();
navigateTo('sharing');
}
function showNewOutfitForm() {
new_outfit_name_el.val('');
@ -495,9 +502,13 @@ View.Outfits = function (wardrobe) {
save_outfit_wrapper_el.removeClass('saving-outfit');
}
$('#preview-sidebar-nav-outfits').click(navLinkTo(showOutfits));
$('#preview-sidebar-nav-closet').click(navLinkTo(showCloset));
$('#preview-sidebar-navbar-closet').click(showCloset);
$('#preview-sidebar-navbar-sharing').click(function () {
sharing.startLoading();
wardrobe.outfits.share();
showSharing();
});
$('#preview-sidebar-navbar-outfits').click(showOutfits);
$('#save-outfit, #save-outfit-copy').click(showNewOutfitForm);
@ -508,40 +519,65 @@ View.Outfits = function (wardrobe) {
});
/* Outfits list */
var list_image_subscriptions = {};
function listSubscribeToImage(outfit) {
list_image_subscriptions[outfit.id] = wardrobe.image_subscriptions.subscribe(outfit);
}
function listUnsubscribeFromImage(outfit) {
if(outfit.id in list_image_subscriptions) {
if(list_image_subscriptions[outfit.id] !== null) {
wardrobe.image_subscriptions.unsubscribe(list_image_subscriptions[outfit.id]);
}
delete list_image_subscriptions[outfit.id];
}
}
$('#outfit-template').template('outfitTemplate');
wardrobe.user.bind('outfitsLoaded', function (outfits) {
wardrobe.outfits.bind('outfitsLoaded', function (outfits) {
var outfit_els = $.tmpl('outfitTemplate', outfits);
outfits_list_el.html('').append(outfit_els).addClass('loaded');
updateActiveOutfit();
for(var i = 0; i < outfits.length; i++) {
listSubscribeToImage(outfits[i]);
}
});
wardrobe.user.bind('addOutfit', function (outfit, i) {
wardrobe.outfits.bind('addOutfit', function (outfit, i) {
var next_child = outfits_list_el.children().not('.hiding').eq(i),
outfit_el = $.tmpl('outfitTemplate', outfit);
outfit_el = $.tmpl('outfitTemplate', outfit.clone());
if(next_child.length) {
outfit_el.insertBefore(next_child);
} else {
outfit_el.appendTo(outfits_list_el);
}
updateActiveOutfit();
outfit_el.hide().show('normal');
var naturalWidth = outfit_el.css('width');
log("Natural width is", naturalWidth, outfit_el.width());
outfit_el.width(0).animate({width: naturalWidth}, 'normal');
listSubscribeToImage(outfit);
});
wardrobe.user.bind('removeOutfit', function (outfit, i) {
wardrobe.outfits.bind('removeOutfit', function (outfit, i) {
var outfit_el = outfits_list_el.children().not('.hiding').eq(i);
outfit_el.addClass('hiding').stop(true).hide('normal', function () { outfit_el.remove() });
outfit_el.addClass('hiding').stop(true).animate({width: 0}, 'normal', function () { outfit_el.remove() });
listUnsubscribeFromImage(outfit);
});
$('#preview-outfits h4').live('click', function () {
wardrobe.outfit.load($(this).tmplItem().data.id);
$('#preview-outfits li header, #preview-outfits li .outfit-thumbnail-wrapper').live('click', function () {
wardrobe.outfits.load($(this).tmplItem().data.id);
});
$('a.outfit-rename-button').live('click', function (e) {
e.preventDefault();
var li = $(this).closest('li').addClass('renaming'),
name = li.find('h4').text();
name = li.find('span.outfit-name').text();
li.find('input.outfit-rename-field').val(name).focus();
});
@ -550,7 +586,7 @@ View.Outfits = function (wardrobe) {
li = el.closest('li').removeClass('renaming');
if(new_name != outfit.name) {
li.startLoading();
wardrobe.user.renameOutfit(outfit, new_name);
wardrobe.outfits.renameOutfit(outfit, new_name);
}
}
@ -568,7 +604,8 @@ View.Outfits = function (wardrobe) {
this.blur();
});
$('button.outfit-delete').live('click', function (e) {
$('a.outfit-delete').live('click', function (e) {
e.stopPropagation();
e.preventDefault();
$(this).closest('li').addClass('confirming-deletion');
});
@ -576,9 +613,9 @@ View.Outfits = function (wardrobe) {
$('a.outfit-delete-confirmation-yes').live('click', function (e) {
var outfit = $(this).tmplItem().data;
e.preventDefault();
wardrobe.user.destroyOutfit(outfit);
if(wardrobe.outfit.getOutfit().id == outfit.id) {
wardrobe.outfit.setId(null);
wardrobe.outfits.destroyOutfit(outfit);
if(wardrobe.outfits.getOutfit().id == outfit.id) {
wardrobe.outfits.setId(null);
}
});
@ -587,16 +624,25 @@ View.Outfits = function (wardrobe) {
$(this).closest('li').removeClass('confirming-deletion');
});
stars.live('click', function () {
stars.live('click', function (e) {
e.stopPropagation();
var el = $(this);
el.closest('li').startLoading();
wardrobe.user.toggleOutfitStar(el.tmplItem().data);
wardrobe.outfits.toggleOutfitStar(el.tmplItem().data);
});
function pathToUrl(path) {
var host = document.location.protocol + "//" + document.location.host;
if(document.location.port) host += ":" + document.location.port;
return host + path;
}
function generateOutfitPermalink(outfit) {
return pathToUrl("/outfits/" + outfit.id);
}
function setOutfitPermalink(outfit, outfit_permalink_el, outfit_url_el) {
var url = document.location.protocol + "//" + document.location.host;
if(document.location.port) url += ":" + document.location.port;
url += "/outfits/" + outfit.id;
var url = generateOutfitPermalink(outfit);
outfit_permalink_el.attr('href', url);
if(outfit_url_el) outfit_url_el.val(url);
}
@ -605,10 +651,6 @@ View.Outfits = function (wardrobe) {
setOutfitPermalink(outfit, current_outfit_permalink_el);
}
function setSharedOutfitPermalink(outfit) {
setOutfitPermalink(outfit, shared_outfit_permalink_el, shared_outfit_url_el);
}
function setActiveOutfit(outfit) {
outfits_list_el.find('li.active').removeClass('active');
if(outfit.id) {
@ -620,33 +662,210 @@ View.Outfits = function (wardrobe) {
}
function updateActiveOutfit() {
setActiveOutfit(wardrobe.outfit.getOutfit());
setActiveOutfit(wardrobe.outfits.getOutfit());
}
wardrobe.outfit.bind('setOutfit', setActiveOutfit);
wardrobe.outfit.bind('outfitNotFound', setActiveOutfit);
wardrobe.outfits.bind('setOutfit', setActiveOutfit);
wardrobe.outfits.bind('outfitNotFound', setActiveOutfit);
wardrobe.user.bind('outfitRenamed', function (outfit) {
if(outfit.id == wardrobe.outfit.getId()) {
wardrobe.outfits.bind('outfitRenamed', function (outfit) {
if(outfit.id == wardrobe.outfits.getId()) {
save_current_outfit_name_el.text(outfit.name);
}
});
function outfitElement(outfit) {
return outfits_el.find('li.outfit-' + outfit.id);
}
wardrobe.outfits.bind('saveSuccess', function (outfit) {
listSubscribeToImage(outfit);
});
wardrobe.image_subscriptions.bind('imageEnqueued', function (outfit) {
if(outfit.id in list_image_subscriptions) {
log("List sees imageEnqueued for", outfit);
outfitElement(outfit).removeClass('thumbnail-loaded');
}
});
wardrobe.image_subscriptions.bind('imageReady', function (outfit) {
if(outfit.id in list_image_subscriptions) {
log("List sees imageReady for", outfit);
listUnsubscribeFromImage(outfit);
var src = outfit.image_versions.small + '?' + (new Date()).getTime();
outfitElement(outfit).addClass('thumbnail-loaded').addClass('thumbnail-available').
find('img.outfit-thumbnail').attr('src', src);
}
});
/* Sharing */
var sharing = new function Sharing() {
var WRAPPER = $('#preview-sharing');
var sharing_url_els = {
permalink: $('#preview-sharing-permalink-url'),
large_image: $('#preview-sharing-large-image-url'),
medium_image: $('#preview-sharing-medium-image-url'),
small_image: $('#preview-sharing-small-image-url'),
};
var format_selector_els = $('#preview-sharing-url-formats li');
var thumbnail_el = $('#preview-sharing-thumbnail');
var formats = {
plain: {
image: function (url) { return url },
text: function (url) { return url }
},
html: {
image: function (url, permalink) {
return '<a href="' + permalink + '"><img src="' + url + '" /></a>';
},
text: function (url) {
return '<a href="' + url + '">Dress to Impress</a>';
}
},
bbcode: {
image: function (url, permalink) {
return '[URL=' + permalink + '][IMG]' + url + '[/IMG][/URL]';
},
text: function (url) {
return '[URL=' + url + ']Dress to Impress[/URL]';
}
}
};
var format = formats.plain;
var urls = {permalink: null, small_image: null, medium_image: null,
large_image: null};
format_selector_els.click(function () {
var selector_el = $(this);
format_selector_els.removeClass('active');
selector_el.addClass('active');
log("Setting sharing URL format:", selector_el.attr('data-format'));
format = formats[selector_el.attr('data-format')];
formatUrls();
});
var image_subscription = null;
function unsubscribeFromImage() {
wardrobe.image_subscriptions.unsubscribe(image_subscription);
image_subscription = null;
}
function subscribeToImage(outfit) {
image_subscription = wardrobe.image_subscriptions.subscribe(outfit);
}
function subscribeToImageIfVisible(outfit) {
if(outfit && sidebar_el.hasClass('sharing')) {
subscribeToImage(outfit);
}
}
var current_shared_outfit = {id: null};
this.setOutfit = function (outfit) {
// If outfit has no ID but we're already on the Sharing tab (e.g. user is
// on Sharing but goes back in history to a no-ID outfit), we can't
// exactly do anything with it but submit it for sharing.
if(!outfit.id) {
sharing.startLoading();
wardrobe.outfits.share(outfit);
return false;
}
// But if the outfit does have a valid ID, we're good to go. If it's the
// same as the currently shared outfit ID, then don't even change
// anything. If it's new, then change everything.
if(outfit.id != current_shared_outfit.id) {
// The current shared outfit needs to be a clone, or else modifications
// to the active outfit will show up here, too, and then our comparison
// to discover if this is a new outfit ID or not fails.
current_shared_outfit = outfit.clone();
urls.permalink = generateOutfitPermalink(outfit);
urls.small_image = pathToUrl(outfit.image_versions.small);
urls.medium_image = pathToUrl(outfit.image_versions.medium);
urls.large_image = pathToUrl(outfit.image_versions.large);
formatUrls();
WRAPPER.removeClass('thumbnail-available');
subscribeToImageIfVisible(current_shared_outfit);
}
WRAPPER.addClass('urls-loaded');
}
this.startLoading = function () {
WRAPPER.removeClass('urls-loaded');
}
this.onHide = function () {
unsubscribeFromImage();
}
this.onShow = function () {
subscribeToImageIfVisible(wardrobe.outfits.getOutfit());
}
function formatUrls() {
formatImageUrl('small_image');
formatImageUrl('medium_image');
formatImageUrl('large_image');
formatTextUrl('permalink');
}
function formatTextUrl(key) {
formatUrl(key, format.text(urls[key]));
}
function formatImageUrl(key) {
formatUrl(key, format.image(urls[key], urls.permalink));
}
function formatUrl(key, url) {
sharing_url_els[key].val(url);
}
wardrobe.image_subscriptions.bind('imageEnqueued', function (outfit) {
if(outfit.id == current_shared_outfit.id) {
log("Sharing thumbnail enqueued for outfit", outfit);
WRAPPER.removeClass('thumbnail-loaded');
}
});
wardrobe.image_subscriptions.bind('imageReady', function (outfit) {
if(outfit.id == current_shared_outfit.id) {
log("Sharing thumbnail ready for outfit", outfit);
var src = outfit.image_versions.small + '?' + outfit.image_layers_hash;
thumbnail_el.attr('src', src);
WRAPPER.addClass('thumbnail-loaded');
WRAPPER.addClass('thumbnail-available');
unsubscribeFromImage(outfit);
}
});
wardrobe.outfits.bind('updateSuccess', function (outfit) {
if(sidebar_el.hasClass('sharing')) {
subscribeToImage(outfit);
}
});
wardrobe.outfits.bind('setOutfit', function (outfit) {
log("Sharing sees the setOutfit signal, and will set", outfit);
sharing.setOutfit(outfit);
});
}
/* Saving */
save_current_outfit_el.click(function () {
wardrobe.outfit.update();
wardrobe.outfits.update();
});
new_outfit_form_el.submit(function (e) {
e.preventDefault();
new_outfit_form_el.startLoading();
wardrobe.outfit.create({starred: new_outfit_form_el.hasClass('starred'), name: new_outfit_name_el.val()});
});
$('#share-outfit').click(function () {
save_outfit_wrapper_el.startLoading();
wardrobe.outfit.share();
wardrobe.outfits.create({starred: new_outfit_form_el.hasClass('starred'), name: new_outfit_name_el.val()});
});
new_outfit_form_el.find('div.outfit-star').click(function () {
@ -664,32 +883,31 @@ View.Outfits = function (wardrobe) {
save_error_el.text(text).notify();
}
wardrobe.outfit.bind('saveSuccess', function (outfit) {
wardrobe.outfits.bind('saveSuccess', function (outfit) {
save_success_el.notify();
});
wardrobe.outfit.bind('createSuccess', function (outfit) {
wardrobe.user.addOutfit(outfit);
wardrobe.outfits.bind('createSuccess', function (outfit) {
showOutfits();
hideNewOutfitForm();
});
wardrobe.outfit.bind('updateSuccess', function (outfit) {
wardrobe.user.updateOutfit(outfit);
});
wardrobe.outfit.bind('shareSuccess', function (outfit) {
function shareComplete(outfit) {
save_outfit_wrapper_el.stopLoading().addClass('shared-outfit');
setSharedOutfitPermalink(outfit);
});
sharing.setOutfit(outfit);
showSharing();
}
wardrobe.outfits.bind('shareSuccess', shareComplete);
wardrobe.outfits.bind('shareSkipped', shareComplete);
function clearSharedOutfit() {
save_outfit_wrapper_el.removeClass('shared-outfit');
}
wardrobe.outfit.bind('updateClosetItems', clearSharedOutfit);
wardrobe.outfit.bind('updateWornItems', clearSharedOutfit);
wardrobe.outfit.bind('updatePetState', clearSharedOutfit);
wardrobe.outfits.bind('updateClosetItems', clearSharedOutfit);
wardrobe.outfits.bind('updateWornItems', clearSharedOutfit);
wardrobe.outfits.bind('updatePetState', clearSharedOutfit);
function saveFailure(outfit, response) {
var errors = response.errors;
@ -712,16 +930,16 @@ View.Outfits = function (wardrobe) {
liForOutfit(outfit).stopLoading();
}
wardrobe.outfit.bind('saveFailure', saveFailure);
wardrobe.user.bind('saveFailure', saveFailure)
wardrobe.outfit.bind('shareFailure', function (outfit, response) {
wardrobe.outfits.bind('saveFailure', saveFailure);
wardrobe.outfits.bind('saveFailure', saveFailure)
wardrobe.outfits.bind('shareFailure', function (outfit, response) {
save_outfit_wrapper_el.stopLoading();
saveFailure(outfit, response);
});
/* Error */
wardrobe.outfit.bind('outfitNotFound', function () {
wardrobe.outfits.bind('outfitNotFound', function () {
outfit_not_found_el.notify();
});
}
@ -733,7 +951,7 @@ View.PetStateForm = function (wardrobe) {
button_query = form_query + ' button';
$(button_query).live('click', function (e) {
e.preventDefault();
wardrobe.outfit.setPetStateById(+$(this).data('value'));
wardrobe.outfits.setPetStateById(+$(this).data('value'));
});
function updatePetState(pet_state) {
@ -743,7 +961,7 @@ View.PetStateForm = function (wardrobe) {
}
}
wardrobe.outfit.bind('petTypeLoaded', function (pet_type) {
wardrobe.outfits.bind('petTypeLoaded', function (pet_type) {
var ids = pet_type.pet_state_ids, i, id, li, button, label;
ul.children().remove();
if(ids.length == 1) {
@ -761,18 +979,18 @@ View.PetStateForm = function (wardrobe) {
button.appendTo(li);
li.appendTo(ul);
}
updatePetState(wardrobe.outfit.getPetState());
updatePetState(wardrobe.outfits.getPetState());
}
});
wardrobe.outfit.bind('updatePetState', updatePetState);
wardrobe.outfits.bind('updatePetState', updatePetState);
}
View.PetTypeForm = function (wardrobe) {
var form = $('#pet-type-form'), dropdowns = {}, loaded = false;
form.submit(function (e) {
e.preventDefault();
wardrobe.outfit.setPetTypeByColorAndSpecies(
wardrobe.outfits.setPetTypeByColorAndSpecies(
+dropdowns.color.val(), +dropdowns.species.val()
);
}).children('select').each(function () {
@ -803,12 +1021,12 @@ View.PetTypeForm = function (wardrobe) {
});
});
loaded = true;
updatePetType(wardrobe.outfit.getPetType());
updatePetType(wardrobe.outfits.getPetType());
});
wardrobe.outfit.bind('updatePetType', updatePetType);
wardrobe.outfits.bind('updatePetType', updatePetType);
wardrobe.outfit.bind('petTypeNotFound', function () {
wardrobe.outfits.bind('petTypeNotFound', function () {
$('#pet-type-not-found').show('normal').delay(3000).hide('fast');
});
}
@ -865,7 +1083,7 @@ View.ReportBrokenImage = function (wardrobe) {
var baseURL = link.attr('data-base-url');
function updateLink() {
var assets = wardrobe.outfit.getVisibleAssets();
var assets = wardrobe.outfits.getVisibleAssets();
var url = baseURL + "?";
for(var i = 0; i < assets.length; i++) {
@ -876,9 +1094,9 @@ View.ReportBrokenImage = function (wardrobe) {
link.attr('href', url);
}
wardrobe.outfit.bind('updateWornItems', updateLink);
wardrobe.outfit.bind('updateItemAssets', updateLink);
wardrobe.outfit.bind('updatePetState', updateLink);
wardrobe.outfits.bind('updateWornItems', updateLink);
wardrobe.outfits.bind('updateItemAssets', updateLink);
wardrobe.outfits.bind('updatePetState', updateLink);
}
View.Search = function (wardrobe) {

View file

@ -7,4 +7,4 @@ var main_wardrobe = new Wardrobe(), View = Wardrobe.getStandardView({
});
main_wardrobe.registerViews(View);
main_wardrobe.initialize();
main_wardrobe.outfit.loadData(INITIAL_OUTFIT_DATA);
main_wardrobe.outfits.loadData(INITIAL_OUTFIT_DATA);

View file

@ -7,9 +7,6 @@ function arraysMatch(array1, array2) {
return array1 == array2;
}
temp = [];
if ( (!array1[0]) || (!array2[0]) ) {
return false;
}
if (array1.length != array2.length) {
return false;
}
@ -28,7 +25,7 @@ function arraysMatch(array1, array2) {
return true;
}
Array.prototype.map = function (property) {
Array.prototype.mapProperty = function (property) {
return $.map(this, function (element) {
return element[property];
});
@ -78,9 +75,20 @@ function Wardrobe() {
function Asset(newData) {
var asset = this;
function size_key(size) {
return size[0] + 'x' + size[1];
}
this.image_urls_by_size_key = {};
var image;
for(var i = 0; i < newData.images.length; i++) {
image = newData.images[i];
this.image_urls_by_size_key[size_key(image.size)] = image.url;
}
this.imageURL = function (size) {
return Wardrobe.IMAGE_CONFIG.base_url + this.s3_path + "/" + size[0] + "x" + size[1] + ".png";
return this.image_urls_by_size_key[size_key(size)];
}
this.update = function (data) {
@ -267,16 +275,23 @@ function Wardrobe() {
worn_item_ids = new_ids.worn;
closet_item_ids = new_ids.unworn.concat(new_ids.worn);
}
function loadAttributes(data) {
outfit.color_id = data.color_id;
outfit.id = data.id;
outfit.name = data.name;
outfit.pet_state_id = data.pet_state_id;
outfit.starred = data.starred;
outfit.species_id = data.species_id;
outfit.image_versions = data.image_versions;
outfit.image_enqueued = data.image_enqueued;
outfit.image_layers_hash = data.image_layers_hash;
outfit.setWornAndUnwornItemIds(data.worn_and_unworn_item_ids);
new_record = false;
}
if(typeof data != 'undefined') {
this.color_id = data.color_id;
this.id = data.id;
this.name = data.name;
this.pet_state_id = data.pet_state_id;
this.starred = data.starred;
this.species_id = data.species_id;
this.setWornAndUnwornItemIds(data.worn_and_unworn_item_ids);
new_record = false;
loadAttributes(data);
}
this.closet_items = [];
@ -327,11 +342,11 @@ function Wardrobe() {
new_items = [], new_worn_item_ids = [];
if(added_item) {
// now that we've loaded, check for conflicts on the added item
item_zones = added_item.getAssetsFitting(outfit.pet_type).map('zone_id');
item_zones = added_item.getAssetsFitting(outfit.pet_type).mapProperty('zone_id');
item_zones_length = item_zones.length;
for(var i = 0; i < outfit.worn_items.length; i++) {
existing_item = outfit.worn_items[i];
existing_item_zones = existing_item.getAssetsFitting(outfit.pet_type).map('zone_id');
existing_item_zones = existing_item.getAssetsFitting(outfit.pet_type).mapProperty('zone_id');
passed = true;
if(existing_item != added_item) {
for(var j = 0; j < item_zones_length; j++) {
@ -369,23 +384,6 @@ function Wardrobe() {
}
}
function sendUpdate(outfit_data, success, failure) {
$.ajax({
url: '/outfits/' + outfit.id,
type: 'post',
data: {'_method': 'put', outfit: outfit_data},
success: function () {
Outfit.cache[outfit.id] = outfit;
success(outfit);
},
error: function (xhr) {
if(typeof failure !== 'undefined') {
failure(outfit, $.parseJSON(xhr.responseText));
}
}
});
}
this.closetItem = function (item, updateClosetItemsCallback) {
if(!hasItemInCloset(item)) {
this.closet_items.push(item);
@ -415,6 +413,14 @@ function Wardrobe() {
});
return visible_assets;
}
this.isIdenticalTo = function (other) {
return other && // other exists
this.constructor == other.constructor && // other is an outfit
this.getPetStateId() == other.getPetStateId() &&
arraysMatch(this.getWornItemIds(), other.getWornItemIds()) &&
arraysMatch(this.getClosetItemIds(), other.getClosetItemIds());
}
this.rename = function (new_name, success, failure) {
this.updateAttributes({name: new_name}, success, failure);
@ -522,6 +528,9 @@ function Wardrobe() {
new_outfit.id = outfit.id;
new_outfit.name = outfit.name;
new_outfit.starred = outfit.starred;
new_outfit.image_enqueued = outfit.image_enqueued;
new_outfit.image_versions = outfit.image_versions;
new_outfit.image_layers_hash = outfit.image_layers_hash;
return new_outfit;
}
@ -539,6 +548,13 @@ function Wardrobe() {
new_ids.unworn = base_ids.unworn.slice(0);
outfit.setWornAndUnwornItemIds(new_ids);
}
function updateFromSaveResponse(data) {
outfit.id = data.id;
outfit.image_versions = data.image_versions;
outfit.image_enqueued = data.image_enqueued;
outfit.image_layers_hash = data.image_layers_hash;
}
this.destroy = function (success) {
$.ajax({
@ -554,10 +570,11 @@ function Wardrobe() {
url: '/outfits',
type: 'post',
data: {outfit: getAttributes()},
dataType: 'json',
success: function (data) {
new_record = false;
outfit.id = data;
Outfit.cache[data] = outfit;
updateFromSaveResponse(data);
Outfit.cache[outfit.id] = outfit;
success(outfit);
},
error: function (xhr) {
@ -565,6 +582,32 @@ function Wardrobe() {
}
});
}
this.reload = function (success) {
Outfit.load(this.id, function (new_outfit) {
loadAttributes(new_outfit);
success(outfit);
});
}
function sendUpdate(outfit_data, success, failure) {
$.ajax({
url: '/outfits/' + outfit.id,
type: 'post',
data: {'_method': 'put', outfit: outfit_data},
dataType: 'json',
success: function (data) {
updateFromSaveResponse(data);
Outfit.cache[outfit.id] = outfit;
success(outfit);
},
error: function (xhr) {
if(typeof failure !== 'undefined') {
failure(outfit, $.parseJSON(xhr.responseText));
}
}
});
}
this.updateAttributes = function (attributes, success, failure) {
var outfit_data = {};
@ -583,19 +626,23 @@ function Wardrobe() {
if(typeof Outfit.cache[id] !== 'undefined') {
callback(Outfit.cache[id]);
} else {
$.ajax({
url: '/outfits/' + id + '.json',
success: function (data) {
var outfit = new Outfit(data);
Outfit.cache[id] = outfit;
callback(outfit);
},
error: function () {
callback(null);
}
});
Outfit.load(id, callback);
}
}
Outfit.load = function (id, callback) {
$.ajax({
url: '/outfits/' + id + '.json',
success: function (data) {
var outfit = new Outfit(data);
Outfit.cache[id] = outfit;
callback(outfit);
},
error: function () {
callback(null);
}
});
}
Outfit.loadForCurrentUser = function (success) {
var outfits = [];
@ -788,8 +835,13 @@ function Wardrobe() {
Controller.all = {};
Controller.all.Outfit = function OutfitController() {
var controller = this, outfit = new Outfit;
Controller.all.Outfits = function OutfitsController() {
// TODO: clean up the merge of outfits and user controller. Some is already
// done, but I'm sure there's tons of redundant code still lying around.
/* Current outfit management */
var controller = this, outfit = new Outfit, last_shared_outfit = null;
this.in_transaction = false;
@ -862,6 +914,7 @@ function Wardrobe() {
}
outfit.create(
function (outfit) {
insertOutfit(outfit);
controller.events.trigger('saveSuccess', outfit);
controller.events.trigger('createSuccess', outfit);
controller.events.trigger('setOutfit', outfit);
@ -920,12 +973,24 @@ function Wardrobe() {
}
this.share = function () {
var sharedOutfit = outfit.clone();
sharedOutfit.anonymous = true;
sharedOutfit.create(
controller.event('shareSuccess'),
controller.event('shareFailure')
);
if(outfit.id) {
// If this is a user-saved outfit (user is logged in), no need to
// re-share it. Skip to using the current outfit.
controller.events.trigger('shareSkipped', outfit);
} else if(outfit.isIdenticalTo(last_shared_outfit)) {
// If the outfit hasn't changed since last time we shared it, no need to
// re-share it. Skip to using the last shared outfit.
controller.events.trigger('shareSkipped', last_shared_outfit);
} else {
// Otherwise, this is a fresh outfit that needs to be shared. Try, and
// report success or failure.
last_shared_outfit = outfit.clone();
last_shared_outfit.anonymous = true;
last_shared_outfit.create(
controller.event('shareSuccess'),
controller.event('shareFailure')
);
}
}
this.unclosetItem = function (item) {
@ -943,6 +1008,7 @@ function Wardrobe() {
this.update = function () {
outfit.update(
function (outfit) {
updateUserOutfit(outfit);
controller.events.trigger('saveSuccess', outfit),
controller.events.trigger('updateSuccess', outfit)
},
@ -958,6 +1024,159 @@ function Wardrobe() {
controller.event('updateItemAssets')
);
}
/* User outfits management */
var outfits = [], outfits_loaded = false;
function compareOutfits(a, b) {
if(a.starred) {
if(!b.starred) return -1;
} else if(b.starred) {
return 1;
}
if(a.name < b.name) return -1;
else if(a.name == b.name) return 0;
else return 1;
}
function insertOutfit(outfit) {
for(var i = 0; i < outfits.length; i++) {
if(compareOutfits(outfit, outfits[i]) < 0) {
outfits.splice(i, 0, outfit);
controller.events.trigger('addOutfit', outfit, i);
return;
}
}
controller.events.trigger('addOutfit', outfit, outfits.length);
outfits.push(outfit);
}
function sortOutfits(outfits) {
outfits.sort(compareOutfits);
}
function yankOutfit(outfit) {
var i;
for(i = 0; i < outfits.length; i++) {
if(outfit.id == outfits[i].id) {
outfits.splice(i, 1);
break;
}
}
controller.events.trigger('removeOutfit', outfit, i);
}
this.destroyOutfit = function (outfit) {
outfit.destroy(function () {
yankOutfit(outfit);
});
}
this.loadOutfits = function () {
if(!outfits_loaded) {
Outfit.loadForCurrentUser(function (new_outfits) {
outfits = new_outfits;
outfits_loaded = true;
sortOutfits(outfits);
controller.events.trigger('outfitsLoaded', outfits);
});
}
}
this.renameOutfit = function (outfit, new_name) {
var old_name = outfit.name;
outfit.rename(new_name, function () {
yankOutfit(outfit);
insertOutfit(outfit);
controller.events.trigger('outfitRenamed', outfit);
}, function (outfit_copy, response) {
outfit.name = old_name;
controller.events.trigger('saveFailure', outfit_copy, response);
});
}
this.toggleOutfitStar = function (outfit) {
outfit.toggleStar(function () {
yankOutfit(outfit);
insertOutfit(outfit);
controller.events.trigger('outfitStarToggled', outfit);
});
}
function updateUserOutfit(outfit) {
for(var i = 0; i < outfits.length; i++) {
if(outfits[i].id == outfit.id) {
outfits[i] = outfit.clone();
break;
}
}
}
}
Controller.all.ImageSubscriptions = function ImagesSubscriptionsController() {
var outfitSubscriptionTotals = {};
var DELAY = 5000;
var controller = this;
function checkSubscription(outfit_id) {
Outfit.find(outfit_id, function (outfit) {
log("Checking image for", outfit);
outfit.reload(function () {
if(outfitSubscriptionTotals[outfit_id] > 0) {
if(outfit.image_enqueued) {
log("Outfit image still enqueued; will try again soon", outfit);
setTimeout(function () { checkSubscription(outfit_id) }, DELAY);
} else {
// Unsubscribe everyone from this outfit and fire ready events
delete outfitSubscriptionTotals[outfit_id];
controller.events.trigger('imageReady', outfit);
}
} else {
log("Outfit was unsubscribed", outfit);
delete outfitSubscriptionTotals[outfit_id];
}
});
});
}
this.subscribe = function (outfit) {
if(outfit.image_enqueued) {
if(outfit.id in outfitSubscriptionTotals) {
// The subscription is already running. Just mark that one more
// consumer is interested in it, and they'll all get a response soon.
outfitSubscriptionTotals[outfit.id] += 1;
} else {
// This is a new subscription! Let's start checking it.
outfitSubscriptionTotals[outfit.id] = 1;
checkSubscription(outfit.id);
}
// Regardless, trigger the enqueued event for the new consumer's sake.
controller.events.trigger('imageEnqueued', outfit);
} else {
// Otherwise, never bother checking: skip straight to the ready phase.
// Give it an instant timeout so that we're sure the consumer is ready
// for the event. (It can be tricky when the consumer assigns this
// return value somewhere to know if it cares about the event, so the
// event can't fire before the return.)
setTimeout(function () {
controller.events.trigger('imageReady', outfit)
}, 0);
}
return outfit;
}
this.unsubscribe = function (outfit) {
if(outfit && outfit.id in outfitSubscriptionTotals) {
if(outfitSubscriptionTotals[outfit.id] > 1) {
outfitSubscriptionTotals[outfit.id] -= 1;
} else {
delete outfitSubscriptionTotals[outfit.id];
}
}
}
}
Controller.all.BasePet = function BasePetController() {
@ -1028,96 +1247,6 @@ function Wardrobe() {
}
}
Controller.all.User = function UserController() {
var controller = this, outfits = [], outfits_loaded = false;
function compareOutfits(a, b) {
if(a.starred) {
if(!b.starred) return -1;
} else if(b.starred) {
return 1;
}
if(a.name < b.name) return -1;
else if(a.name == b.name) return 0;
else return 1;
}
function insertOutfit(outfit) {
for(var i = 0; i < outfits.length; i++) {
if(compareOutfits(outfit, outfits[i]) < 0) {
outfits.splice(i, 0, outfit);
controller.events.trigger('addOutfit', outfit, i);
return;
}
}
controller.events.trigger('addOutfit', outfit, outfits.length);
outfits.push(outfit);
}
function sortOutfits(outfits) {
outfits.sort(compareOutfits);
}
function yankOutfit(outfit) {
var i;
for(i = 0; i < outfits.length; i++) {
if(outfit.id == outfits[i].id) {
outfits.splice(i, 1);
break;
}
}
controller.events.trigger('removeOutfit', outfit, i);
}
this.addOutfit = insertOutfit;
this.destroyOutfit = function (outfit) {
outfit.destroy(function () {
yankOutfit(outfit);
});
}
this.loadOutfits = function () {
if(!outfits_loaded) {
Outfit.loadForCurrentUser(function (new_outfits) {
outfits = new_outfits;
outfits_loaded = true;
sortOutfits(outfits);
controller.events.trigger('outfitsLoaded', outfits);
});
}
}
this.renameOutfit = function (outfit, new_name) {
var old_name = outfit.name;
outfit.rename(new_name, function () {
yankOutfit(outfit);
insertOutfit(outfit);
controller.events.trigger('outfitRenamed', outfit);
}, function (outfit_copy, response) {
outfit.name = old_name;
controller.events.trigger('saveFailure', outfit_copy, response);
});
}
this.toggleOutfitStar = function (outfit) {
outfit.toggleStar(function () {
yankOutfit(outfit);
insertOutfit(outfit);
controller.events.trigger('outfitStarToggled', outfit);
});
}
this.updateOutfit = function (outfit) {
for(var i = 0; i < outfits.length; i++) {
if(outfits[i].id == outfit.id) {
outfits[i] = outfit.clone();
break;
}
}
}
}
var underscored_name;
for(var name in Controller.all) {
@ -1196,13 +1325,13 @@ Wardrobe.getStandardView = function (options) {
var outfit_events = ['updateWornItems', 'updateClosetItems', 'updateItemAssets', 'updatePetType', 'updatePetState'];
for(var i = 0; i < outfit_events.length; i++) {
(function (event) {
wardrobe.outfit.bind(event, function (obj) {
wardrobe.outfits.bind(event, function (obj) {
log(event, obj);
});
})(outfit_events[i]);
}
wardrobe.outfit.bind('petTypeNotFound', function (pet_type) {
wardrobe.outfits.bind('petTypeNotFound', function (pet_type) {
log(pet_type.toString() + ' not found');
});
}
@ -1246,7 +1375,7 @@ Wardrobe.getStandardView = function (options) {
var assets, assets_for_swf;
if(update_pending_flash) return false;
if(preview_swf && preview_swf.setAssets) {
assets = wardrobe.outfit.getVisibleAssets();
assets = wardrobe.outfits.getVisibleAssets();
preview_swf.setAssets(assets);
} else {
update_pending_flash = true;
@ -1303,10 +1432,11 @@ Wardrobe.getStandardView = function (options) {
// Get a copy of the visible assets, then sort them in ascending zone
// order.
var assets = wardrobe.outfit.getVisibleAssets().slice(0);
var assets = wardrobe.outfits.getVisibleAssets().slice(0);
assets.sort(function (a, b) {
return a.depth - b.depth;
});
console.log(assets.mapProperty('id'));return;
for(var i = 0; i < assets.length; i++) {
url += "," + encodeURIComponent(assets[i].imageURL(size));
@ -1316,7 +1446,7 @@ Wardrobe.getStandardView = function (options) {
}
this.updateAssets = function () {
var assets = wardrobe.outfit.getVisibleAssets(), asset,
var assets = wardrobe.outfits.getVisibleAssets(), asset,
availableAssets = [];
pendingAssets = {};
pendingAssetsCount = 0;
@ -1375,10 +1505,9 @@ Wardrobe.getStandardView = function (options) {
for(var i in sizes) {
if(!sizes.hasOwnProperty(i)) continue;
size = sizes[i];
size[2] = size[0] * size[1];
inserted = false;
for(var i in SIZES_SMALL_TO_LARGE) {
if(SIZES_SMALL_TO_LARGE[i][2] > size[2]) {
if(SIZES_SMALL_TO_LARGE[i][0] * SIZES_SMALL_TO_LARGE[i][1] > size[0] * size[1]) {
SIZES_SMALL_TO_LARGE.splice(i, 0, size);
inserted = true;
break;
@ -1476,9 +1605,9 @@ Wardrobe.getStandardView = function (options) {
preview.adapter.updateAssets();
}
wardrobe.outfit.bind('updateWornItems', updateAssets);
wardrobe.outfit.bind('updateItemAssets', updateAssets);
wardrobe.outfit.bind('updatePetState', updateAssets);
wardrobe.outfits.bind('updateWornItems', updateAssets);
wardrobe.outfits.bind('updateItemAssets', updateAssets);
wardrobe.outfits.bind('updatePetState', updateAssets);
function useAdapter(name) {
preview.adapter = new Adapter[name]();

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

File diff suppressed because it is too large Load diff

BIN
vendor/cache/carrierwave-0.5.8.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/excon-0.9.6.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/fog-1.1.2.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/formatador-0.2.1.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/mini_magick-3.4.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/multi_json-1.0.4.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/net-scp-1.0.4.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/net-ssh-2.3.0.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/parallel-0.5.17.gem vendored Normal file

Binary file not shown.

BIN
vendor/cache/subexec-0.2.1.gem vendored Normal file

Binary file not shown.