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 '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 group :development_async do
# async wrappers # async wrappers
gem 'eventmachine', :git => 'git://github.com/eventmachine/eventmachine.git' gem 'eventmachine', :git => 'git://github.com/eventmachine/eventmachine.git'

View file

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

View file

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

View file

@ -1,4 +1,12 @@
module ApplicationHelper 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) def add_body_class(class_name)
@body_class ||= '' @body_class ||= ''
@body_class << " #{class_name}" @body_class << " #{class_name}"
@ -100,6 +108,23 @@ module ApplicationHelper
def origin_tag(value) def origin_tag(value)
hidden_field_tag 'origin', value, :id => nil hidden_field_tag 'origin', value, :id => nil
end 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 def return_to_field_tag
hidden_field_tag :return_to, request.fullpath 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 attr_accessible :name, :pet_state_id, :starred, :worn_and_unworn_item_ids
scope :wardrobe_order, order('starred DESC', :name) 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={}) def as_json(more_options={})
serializable_hash :only => [:id, :name, :pet_state_id, :starred], 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 end
def closet_item_ids def closet_item_ids
@ -64,9 +77,45 @@ class Outfit < ActiveRecord::Base
end end
self.item_outfit_relationships = new_rels self.item_outfit_relationships = new_rels
end end
def worn_item_ids # Returns the array of SwfAssets representing each layer of the output image,
worn_and_unworn_item_ids[:worn] # 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 end
def self.build_for_user(user, params) def self.build_for_user(user, params)
@ -82,5 +131,87 @@ class Outfit < ActiveRecord::Base
outfit.attributes = params outfit.attributes = params
end end
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 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' set_inheritance_column 'inheritance_type'
IMAGE_SIZES = {
:small => [150, 150],
:medium => [300, 300],
:large => [600, 600]
}
include SwfConverter 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 def local_swf_path
LOCAL_ASSET_DIR.join(local_path_within_outfit_swfs) LOCAL_ASSET_DIR.join(local_path_within_outfit_swfs)
@ -74,6 +80,21 @@ class SwfAsset < ActiveRecord::Base
end end
end 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! def convert_swf_if_not_converted!
if needs_conversion? if needs_conversion?
@ -161,7 +182,7 @@ class SwfAsset < ActiveRecord::Base
:zones_restrict => zones_restrict, :zones_restrict => zones_restrict,
:is_body_specific => body_specific?, :is_body_specific => body_specific?,
:has_image => has_image?, :has_image => has_image?,
:s3_path => s3_path :images => images
} }
if options[:for] == 'wardrobe' if options[:for] == 'wardrobe'
json[:local_path] = local_url json[:local_path] = local_url

View file

@ -3,12 +3,13 @@
@import "partials/context_button" @import "partials/context_button"
@import "partials/icon" @import "partials/icon"
@import "partials/outfit"
@import star @import star
$object-padding: 6px $object-padding: 6px
$nc-icon-size: 16px $nc-icon-size: 16px
$preview-dimension: 400px $preview-dimension: 380px
$sidebar-margin: 20px $sidebar-margin: 20px
$sidebar-width: 400px $sidebar-width: 400px
$sidebar-unit-horizontal-padding: 24px $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-width: $sidebar-unit-inner-width - $outfit-thumbnail-size - $outfit-thumbnail-margin - 32px
$outfit-content-inner-width: $outfit-content-width - $outfit-header-padding $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 =active-mode
color: $text-color color: $text-color
font-weight: bold font-weight: bold
=outfit =sidebar-navbar-unselected
+outfit-star-shifted background: transparent
padding: .25em 0 border-bottom: 1px solid $soft-border-color
//.outfit-thumbnail font-weight: normal
float: left
height: $outfit-thumbnail-size =sidebar-navbar-selected
margin-right: $outfit-thumbnail-margin background: white
overflow: hidden border-bottom-color: white
position: relative font-weight: bold
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-view-child =sidebar-view-child
margin: margin:
@ -144,7 +80,7 @@ body.outfits-edit
position: left center position: left center
repeat: no-repeat repeat: no-repeat
padding-left: 20px 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 +loud-awesome-button-color
#current-outfit-permalink, #shared-outfit-permalink #current-outfit-permalink, #shared-outfit-permalink
display: none display: none
@ -207,11 +143,14 @@ body.outfits-edit
&.image-active &.image-active
#preview-mode-image #preview-mode-image
+active-mode +active-mode
#report-broken-image #preview-mode-note, #report-broken-image
display: block display: block
&.can-download // Phasing out the image download section. Not confident enough yet to
#preview-download-image // *remove* it, depending on user feedback, but that's a TODO for down
display: inline-block // the road if hiding goes well.
// &.can-download
// #preview-download-image
// display: inline-block
#preview-mode-toggle #preview-mode-toggle
+border-radius(.5em) +border-radius(.5em)
border: 1px solid $module-border-color border: 1px solid $module-border-color
@ -249,34 +188,27 @@ body.outfits-edit
em em
font-style: normal font-style: normal
text-decoration: underline text-decoration: underline
#report-broken-image #preview-mode-note, #report-broken-image
display: none display: none
#preview-sidebar #preview-sidebar
+border-radius(10px)
border: 1px solid $soft-border-color
float: left float: left
height: $preview-dimension height: $preview-dimension
margin-left: $sidebar-margin margin-left: $sidebar-margin
margin-bottom: 1em margin-bottom: 1em
overflow: auto width: $container_width - $preview-dimension - $sidebar-margin
width: $container_width - $preview-dimension - $sidebar-margin - 2px
&.viewing-outfits &.viewing-outfits
#preview-closet #preview-closet
display: none display: none
#preview-outfits #preview-outfits
display: block display: block
&.viewing-saving-outfit &.sharing
height: auto
max-height: 100%
#preview-closet #preview-closet
display: none display: none
#preview-saving-outfit #preview-sharing
display: block display: block
.sidebar-view .sidebar-view
h2 margin: 1.5em 0
margin:
bottom: .25em
left: $sidebar-unit-horizontal-padding
#preview-closet #preview-closet
h2 h2
margin-bottom: 0 margin-bottom: 0
@ -378,7 +310,6 @@ body.outfits-edit
width: 100% width: 100%
#preview-sidebar #preview-sidebar
float: right float: right
height: 100%
margin: 0 margin: 0
position: relative position: relative
width: $sidebar-width width: $sidebar-width
@ -461,19 +392,283 @@ body.outfits-edit
#preview-outfits #preview-outfits
display: none display: none
text-align: left text-align: left
$outfit-inner-size: 110px
$outfit-margin: 1px
$outfit-outer-size: $outfit-inner-size + ($outfit-margin * 2)
> ul > ul
+outfits-list
+sidebar-view-child +sidebar-view-child
background: image-url("loading.gif") no-repeat center top background: image-url("loading.gif") no-repeat center top
display: block display: none
font-family: $main-font font-family: $main-font
list-style: none margin: 0 auto 1em
margin:
bottom: 1em
min-height: 16px min-height: 16px
> li width: $outfit-outer-size * 3
+outfit
&.loaded &.loaded
background: transparent 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 .preview-sidebar-nav
float: right float: right
@ -481,6 +676,49 @@ body.outfits-edit
margin: margin:
right: $sidebar-unit-horizontal-padding right: $sidebar-unit-horizontal-padding
top: 1em 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 #save-success, #save-error, #outfit-not-found, #preview-sidebar-donation-request
+sidebar-view-child +sidebar-view-child
@ -507,25 +745,10 @@ body.outfits-edit
+opacity(.5) +opacity(.5)
display: none 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 #new-outfit-name
font: inherit font: inherit
line-height: 1 line-height: 1
#preview-saving-outfit
display: none
padding-bottom: 1em
#pet-type-form, #pet-state-form, #preview-swf, #preview-search-form #pet-type-form, #pet-state-form, #preview-swf, #preview-search-form
position: relative position: relative
@ -541,10 +764,10 @@ body.outfits-edit
display: none display: none
form#save-outfit-form form#save-outfit-form
+outfit +outfit-star-shifted
display: none display: none
margin-right: 0 margin-right: 0
padding: 0 padding: 0
.outfit-star, input, button .outfit-star, input, button
+inline-block +inline-block
@ -572,8 +795,11 @@ body.outfits-edit
display: none display: none
#save-current-outfit, #save-outfit-copy #save-current-outfit, #save-outfit-copy
display: inline-block display: inline-block
#current-outfit-permalink // Phasing out permalink. Shared outfit links have been straight-up
display: inline-block // 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 &.saving-outfit
#save-outfit-form #save-outfit-form
display: block display: block
@ -581,6 +807,10 @@ body.outfits-edit
display: none display: none
.preview-search-form-your-items .preview-search-form-your-items
+inline-block +inline-block
#preview-outfits-not-logged-in
display: none
#preview-outfits-list
display: block
&.user-not-signed-in &.user-not-signed-in
#save-outfit-not-signed-in #save-outfit-not-signed-in

View file

@ -1,27 +1,55 @@
@import "partials/outfit"
@import star @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 body.outfits-index
#outfits #outfits
list-style: none +outfits-list
li > li
+outfit-star height: $outfit-inner-height
clear: left margin: 2px
float: left width: $outfit-inner-width
margin-bottom: .5em
header, footer
h4 padding: $outfit-banner-v-padding $outfit-banner-h-padding
float: left width: $outfit-banner-inner-width
width: 12em
footer
.outfit-edit-link, form display: none
float: left
font-size: 85% .outfit-edit-link
margin-left: 1em float: left
text-decoration: none
.outfit-edit-link
+awesome-button 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 .outfit-delete-button
margin: 0 +reset-awesome-button

View file

@ -15,8 +15,6 @@
background-image: image-url("star.png") background-image: image-url("star.png")
&.loading .outfit-star &.loading .outfit-star
background-image: image-url("loading.gif") background-image: image-url("loading.gif")
&.loading.active .outfit-star
background-image: image-url("loading_current_outfit.gif")
=outfit-star-shifted =outfit-star-shifted
+outfit-star +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 %ul#report-assets
- @swf_assets.each do |swf_asset| - @swf_assets.each do |swf_asset|
%li %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? - unless swf_asset.image_pending_repair?
= form_tag(:action => :create) do = form_tag(:action => :create) do
= hidden_field_tag 'swf_asset_remote_id', swf_asset.remote_id = hidden_field_tag 'swf_asset_remote_id', swf_asset.remote_id

View file

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

View file

@ -1,6 +1,14 @@
= outfit_li_for(outfit) do = outfit_li_for(outfit) do
.outfit-star - if outfit.image?
%h4= link_to outfit.name, outfit = link_to image_tag(outfit.image.small.url), 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}?") %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 #save-outfit-wrapper
%a#current-outfit-permalink{:target => '_blank'} %a#current-outfit-permalink{:target => '_blank'}
= image_tag 'link_go.png', :alt => 'Permalink', :title => 'Permalink to current outfit' = 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 Save outfit
%button#save-outfit-not-signed-in Log in to save %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-current-outfit Save &quot;<span>current outfit</span>&quot;
%button#save-outfit-copy Save a copy
%form#save-outfit-form %form#save-outfit-form
.outfit-star .outfit-star
%input#save-outfit-name{:type => 'text', :placeholder => 'Outfit name'} %input#save-outfit-name{:type => 'text', :placeholder => 'Outfit name'}
@ -62,30 +58,67 @@
%em donate %em donate
at least $5 to help upgrade the server. Thanks! at least $5 to help upgrade the server. Thanks!
#preview-sidebar #preview-sidebar
#outfit-not-found Outfit not found %nav#preview-sidebar-navbar
#save-success Outfit successfully saved #preview-sidebar-navbar-closet Closet
#save-error #preview-sidebar-navbar-sharing Sharing
#preview-closet.sidebar-view #preview-sidebar-navbar-outfits Outfits
%a#preview-sidebar-nav-outfits.preview-sidebar-nav{:href => '#'} Your outfits #preview-sidebar-content
%h2 Closet #outfit-not-found Outfit not found
%ul #save-success Outfit successfully saved
%p#fullscreen-copyright #save-error
Images © 2000-2010 Neopets, Inc. All Rights Reserved. #preview-closet.sidebar-view
Used With Permission %ul
#preview-outfits.sidebar-view %p#fullscreen-copyright
%a#preview-sidebar-nav-closet.preview-sidebar-nav{:href => "#"} &larr; Back to Closet Images © 2000-2010 Neopets, Inc. All Rights Reserved.
%h2 Your outfits Used With Permission
%ul #preview-outfits.sidebar-view
#preview-saving-outfit.sidebar-view %ul#preview-outfits-list
%a#preview-sidebar-nav-cancel-save.preview-sidebar-nav{:href => '#'} &larr; Cancel #preview-outfits-not-logged-in
%h2 Saving new outfit %figure
#new-outfit = image_tag 'outfits_welcome.png'
%form#new-outfit-form %figcaption Ready to become a pro designer?
%header :markdown
.outfit-star We know how hard it can be to keep track of your ideas,
%h4 especially if you end up having a lot of them.
%input#new-outfit-name{:type => 'text', :placeholder => 'Outfit name'} **But Dress to Impress makes it easy.**
%button{:type => 'submit'} Save
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 %form#preview-search-form
%header %header
%h2 Add an item %h2 Add an item
@ -132,18 +165,20 @@
%script#outfit-template{:type => 'text/x-jquery-tmpl'} %script#outfit-template{:type => 'text/x-jquery-tmpl'}
<li class="outfit-${id}{{if starred}} starred{{/if}}"> <li class="outfit-${id}{{if starred}} starred{{/if}}">
%header %header
%button.outfit-delete &times;
.outfit-star .outfit-star
%h4 ${name} %span.outfit-name ${name}
%a.outfit-rename-button{:href => '#'} rename
%form.outfit-rename-form %form.outfit-rename-form
%input.outfit-rename-field{:type => 'text'} %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 .outfit-delete-confirmation
%span Delete forever? %span Delete?
%a.outfit-delete-confirmation-yes{:href => '#'} yes %a.outfit-delete-confirmation-yes{:href => '#'} yes
\/ \/
%a.outfit-delete-confirmation-no{:href => '#'} no, thanks %a.outfit-delete-confirmation-no{:href => '#'} no
</li> </li>
- content_for :javascripts do - content_for :javascripts do
= include_javascript_libraries :jquery, :swfobject, :jquery_tmpl = include_javascript_libraries :jquery, :swfobject, :jquery_tmpl

View file

@ -1,6 +1,11 @@
- title(@outfit.name || "Shared outfit") - title(@outfit.name || "Shared outfit")
- content_for :before_title, campaign_progress - 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 = link_to_edit_outfit(@outfit, :class => 'button', :id => 'outfit-wardrobe-link') do
Edit Edit
- unless user_signed_in? && @outfit.user == current_user - 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. # 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| create_table "auth_servers", :force => true do |t|
t.string "short_name", :limit => 10, :null => false t.string "short_name", :limit => 10, :null => false
@ -116,8 +116,10 @@ ActiveRecord::Schema.define(:version => 20120521164652) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "name" t.string "name"
t.boolean "starred", :default => false, :null => false t.boolean "starred", :default => false, :null => false
t.string "image" t.string "image"
t.string "image_layers_hash"
t.boolean "image_enqueued", :default => false, :null => false
end end
add_index "outfits", ["pet_state_id"], :name => "index_outfits_on_pet_state_id" 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; var item, no_assets, li, no_assets_message;
for(var i = 0, l = specific_items.length; i < l; i++) { for(var i = 0, l = specific_items.length; i < l; i++) {
item = specific_items[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); li = $('li.object-' + item.id).toggleClass('no-assets', no_assets);
(function (li) { (function (li) {
no_assets_message = li.find('span.no-assets-message'); 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); li.append(img).append(controls).append(info_link).append(item.name).appendTo(ul);
} }
setClosetItems(wardrobe.outfit.getClosetItems()); setClosetItems(wardrobe.outfits.getClosetItems());
setOutfitItems(wardrobe.outfit.getWornItems()); setOutfitItems(wardrobe.outfits.getWornItems());
} }
$('span.no-assets-message').live('mouseover', function () { $('span.no-assets-message').live('mouseover', function () {
@ -117,9 +117,9 @@ Partial.ItemSet = function ItemSet(wardrobe, selector) {
no_assets_full_message.removeAttr('style'); no_assets_full_message.removeAttr('style');
}); });
wardrobe.outfit.bind('updateItemAssets', function () { setHasAssets(wardrobe.outfit.getWornItems()) }); wardrobe.outfits.bind('updateItemAssets', function () { setHasAssets(wardrobe.outfits.getWornItems()) });
wardrobe.outfit.bind('updateWornItems', setOutfitItems); wardrobe.outfits.bind('updateWornItems', setOutfitItems);
wardrobe.outfit.bind('updateClosetItems', setClosetItems); wardrobe.outfits.bind('updateClosetItems', setClosetItems);
} }
Partial.ItemSet.CONTROL_SETS = {}; Partial.ItemSet.CONTROL_SETS = {};
@ -151,12 +151,12 @@ Partial.ItemSet.setWardrobe = function (wardrobe) {
} }
toggle_fn.closeted = {}; toggle_fn.closeted = {};
toggle_fn.closeted[true] = $.proxy(wardrobe.outfit, 'closetItem'); toggle_fn.closeted[true] = $.proxy(wardrobe.outfits, 'closetItem');
toggle_fn.closeted[false] = $.proxy(wardrobe.outfit, 'unclosetItem'); toggle_fn.closeted[false] = $.proxy(wardrobe.outfits, 'unclosetItem');
toggle_fn.worn = {}; toggle_fn.worn = {};
toggle_fn.worn[true] = $.proxy(wardrobe.outfit, 'wearItem'); toggle_fn.worn[true] = $.proxy(wardrobe.outfits, 'wearItem');
toggle_fn.worn[false] = $.proxy(wardrobe.outfit, 'unwearItem'); toggle_fn.worn[false] = $.proxy(wardrobe.outfits, 'unwearItem');
Partial.ItemSet.setWardrobe = $.noop; Partial.ItemSet.setWardrobe = $.noop;
} }
@ -164,14 +164,16 @@ Partial.ItemSet.setWardrobe = function (wardrobe) {
View.Closet = function (wardrobe) { View.Closet = function (wardrobe) {
var item_set = new Partial.ItemSet(wardrobe, '#preview-closet ul'); 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) { View.Fullscreen = function (wardrobe) {
var full = $(document.body).hasClass('fullscreen'), win = $(window), var full = $(document.body).hasClass('fullscreen'), win = $(window),
preview_el = $('#preview'), search_el = $('#preview-search-form'), preview_el = $('#preview'), search_el = $('#preview-search-form'),
preview_swf = $('#preview-swf'), sidebar_el = $('#preview-sidebar'), 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() { function fit() {
if(!overrideFull) { if(!overrideFull) {
@ -182,6 +184,7 @@ View.Fullscreen = function (wardrobe) {
if(!full) { if(!full) {
preview_swf.removeAttr('style').css('visibility', 'visible'); preview_swf.removeAttr('style').css('visibility', 'visible');
preview_el.removeAttr('style'); preview_el.removeAttr('style');
sidebar_content_el.removeAttr('style');
} }
} }
} }
@ -213,6 +216,12 @@ View.Fullscreen = function (wardrobe) {
preview_swf.css(size.next); preview_swf.css(size.next);
preview_el.height(available.height); 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); $('#preview').data('fit', fit);
@ -273,32 +282,32 @@ View.Hash = function (wardrobe) {
} }
if(new_data.color !== data.color || new_data.species !== data.species) { 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(new_data.closet) {
if(!arraysMatch(new_data.closet, 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)) { } 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 { } else {
wardrobe.outfit.setClosetItemsByIds([]); wardrobe.outfits.setClosetItemsByIds([]);
} }
if(new_data.objects) { if(new_data.objects) {
if(!arraysMatch(new_data.objects, 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 { } else {
wardrobe.outfit.setWornItemsByIds([]); wardrobe.outfits.setWornItemsByIds([]);
} }
if(new_data.name != data.name && new_data.name) { if(new_data.name != data.name && new_data.name) {
wardrobe.base_pet.setName(new_data.name); wardrobe.base_pet.setName(new_data.name);
} }
if(new_data.state != data.state) { if(new_data.state != data.state) {
wardrobe.outfit.setPetStateById(new_data.state); wardrobe.outfits.setPetStateById(new_data.state);
} }
if(new_data.outfit != data.outfit) { 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) { if(new_data.search != data.search || new_data.search_offset != data.search_offset) {
wardrobe.search.setItemsByQuery(new_data.search, {offset: new_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) { function singleOutfitResponse(event_name, response) {
wardrobe.outfit.bind(event_name, function () { wardrobe.outfits.bind(event_name, function () {
if(!wardrobe.outfit.in_transaction) response.apply(this, arguments); if(!wardrobe.outfits.in_transaction) response.apply(this, arguments);
}); });
} }
singleOutfitResponse('updateClosetItems', function (items) { singleOutfitResponse('updateClosetItems', function (items) {
var item_ids = items.map('id'); var item_ids = items.mapProperty('id');
if(!arraysMatch(item_ids, data.closet)) { if(!arraysMatch(item_ids, data.closet)) {
changeQuery({closet: item_ids}); changeQuery({closet: item_ids});
} }
}); });
singleOutfitResponse('updateWornItems', function (items) { singleOutfitResponse('updateWornItems', function (items) {
var item_ids = items.map('id'), changes = {}; var item_ids = items.mapProperty('id'), changes = {};
if(!arraysMatch(item_ids, data.objects)) { if(!arraysMatch(item_ids, data.objects)) {
changes.objects = item_ids; changes.objects = item_ids;
} }
if(arraysMatch(item_ids, data.closet) || arraysMatch(item_ids, data.objects)) { if(arraysMatch(item_ids, data.closet) || arraysMatch(item_ids, data.objects)) {
changes.closet = undefined; changes.closet = undefined;
} else { } else {
changes.closet = wardrobe.outfit.getClosetItems().map('id'); changes.closet = wardrobe.outfits.getClosetItems().mapProperty('id');
} }
if(changes.objects || changes.closet) changeQuery(changes); if(changes.objects || changes.closet) changeQuery(changes);
}); });
@ -390,7 +399,7 @@ View.Hash = function (wardrobe) {
}); });
singleOutfitResponse('updatePetState', function (pet_state) { 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])) { if(pet_state.id != data.state && pet_type && (data.state || pet_state.id != pet_type.pet_state_ids[0])) {
changeQuery({state: pet_state.id}); 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({ changeQuery({
closet: outfit.getClosetItemIds(), closet: outfit.getClosetItemIds(),
color: outfit.pet_type.color_id, 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; var new_id = outfit ? outfit.id : undefined;
changeQuery({outfit: new_id}); changeQuery({outfit: new_id});
}); });
@ -430,8 +439,6 @@ View.Hash = function (wardrobe) {
View.Outfits = function (wardrobe) { View.Outfits = function (wardrobe) {
var current_outfit_permalink_el = $('#current-outfit-permalink'), 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_form_el = $('#save-outfit-form'),
new_outfit_name_el = $('#save-outfit-name'), new_outfit_name_el = $('#save-outfit-name'),
outfits_el = $('#preview-outfits'), outfits_el = $('#preview-outfits'),
@ -451,13 +458,6 @@ View.Outfits = function (wardrobe) {
return $('li.outfit-' + outfit.id); return $('li.outfit-' + outfit.id);
} }
function navLinkTo(callback) {
return function (e) {
e.preventDefault();
callback();
}
}
function navigateTo(will_be_viewing) { function navigateTo(will_be_viewing) {
var currently_viewing = sidebar_el.attr('class'); var currently_viewing = sidebar_el.attr('class');
if(currently_viewing != will_be_viewing) previously_viewing = currently_viewing; if(currently_viewing != will_be_viewing) previously_viewing = currently_viewing;
@ -476,13 +476,20 @@ View.Outfits = function (wardrobe) {
/* Nav */ /* Nav */
function showCloset() { function showCloset() {
sharing.onHide();
navigateTo(''); navigateTo('');
} }
function showOutfits() { function showOutfits() {
wardrobe.user.loadOutfits(); sharing.onHide();
wardrobe.outfits.loadOutfits();
navigateTo('viewing-outfits'); navigateTo('viewing-outfits');
} }
function showSharing() {
sharing.onShow();
navigateTo('sharing');
}
function showNewOutfitForm() { function showNewOutfitForm() {
new_outfit_name_el.val(''); new_outfit_name_el.val('');
@ -495,9 +502,13 @@ View.Outfits = function (wardrobe) {
save_outfit_wrapper_el.removeClass('saving-outfit'); save_outfit_wrapper_el.removeClass('saving-outfit');
} }
$('#preview-sidebar-nav-outfits').click(navLinkTo(showOutfits)); $('#preview-sidebar-navbar-closet').click(showCloset);
$('#preview-sidebar-navbar-sharing').click(function () {
$('#preview-sidebar-nav-closet').click(navLinkTo(showCloset)); sharing.startLoading();
wardrobe.outfits.share();
showSharing();
});
$('#preview-sidebar-navbar-outfits').click(showOutfits);
$('#save-outfit, #save-outfit-copy').click(showNewOutfitForm); $('#save-outfit, #save-outfit-copy').click(showNewOutfitForm);
@ -508,40 +519,65 @@ View.Outfits = function (wardrobe) {
}); });
/* Outfits list */ /* 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'); $('#outfit-template').template('outfitTemplate');
wardrobe.user.bind('outfitsLoaded', function (outfits) { wardrobe.outfits.bind('outfitsLoaded', function (outfits) {
var outfit_els = $.tmpl('outfitTemplate', outfits); var outfit_els = $.tmpl('outfitTemplate', outfits);
outfits_list_el.html('').append(outfit_els).addClass('loaded'); outfits_list_el.html('').append(outfit_els).addClass('loaded');
updateActiveOutfit(); 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), 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) { if(next_child.length) {
outfit_el.insertBefore(next_child); outfit_el.insertBefore(next_child);
} else { } else {
outfit_el.appendTo(outfits_list_el); outfit_el.appendTo(outfits_list_el);
} }
updateActiveOutfit(); 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); 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 () { $('#preview-outfits li header, #preview-outfits li .outfit-thumbnail-wrapper').live('click', function () {
wardrobe.outfit.load($(this).tmplItem().data.id); wardrobe.outfits.load($(this).tmplItem().data.id);
}); });
$('a.outfit-rename-button').live('click', function (e) { $('a.outfit-rename-button').live('click', function (e) {
e.preventDefault(); e.preventDefault();
var li = $(this).closest('li').addClass('renaming'), 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(); li.find('input.outfit-rename-field').val(name).focus();
}); });
@ -550,7 +586,7 @@ View.Outfits = function (wardrobe) {
li = el.closest('li').removeClass('renaming'); li = el.closest('li').removeClass('renaming');
if(new_name != outfit.name) { if(new_name != outfit.name) {
li.startLoading(); li.startLoading();
wardrobe.user.renameOutfit(outfit, new_name); wardrobe.outfits.renameOutfit(outfit, new_name);
} }
} }
@ -568,7 +604,8 @@ View.Outfits = function (wardrobe) {
this.blur(); this.blur();
}); });
$('button.outfit-delete').live('click', function (e) { $('a.outfit-delete').live('click', function (e) {
e.stopPropagation();
e.preventDefault(); e.preventDefault();
$(this).closest('li').addClass('confirming-deletion'); $(this).closest('li').addClass('confirming-deletion');
}); });
@ -576,9 +613,9 @@ View.Outfits = function (wardrobe) {
$('a.outfit-delete-confirmation-yes').live('click', function (e) { $('a.outfit-delete-confirmation-yes').live('click', function (e) {
var outfit = $(this).tmplItem().data; var outfit = $(this).tmplItem().data;
e.preventDefault(); e.preventDefault();
wardrobe.user.destroyOutfit(outfit); wardrobe.outfits.destroyOutfit(outfit);
if(wardrobe.outfit.getOutfit().id == outfit.id) { if(wardrobe.outfits.getOutfit().id == outfit.id) {
wardrobe.outfit.setId(null); wardrobe.outfits.setId(null);
} }
}); });
@ -587,16 +624,25 @@ View.Outfits = function (wardrobe) {
$(this).closest('li').removeClass('confirming-deletion'); $(this).closest('li').removeClass('confirming-deletion');
}); });
stars.live('click', function () { stars.live('click', function (e) {
e.stopPropagation();
var el = $(this); var el = $(this);
el.closest('li').startLoading(); 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) { function setOutfitPermalink(outfit, outfit_permalink_el, outfit_url_el) {
var url = document.location.protocol + "//" + document.location.host; var url = generateOutfitPermalink(outfit);
if(document.location.port) url += ":" + document.location.port;
url += "/outfits/" + outfit.id;
outfit_permalink_el.attr('href', url); outfit_permalink_el.attr('href', url);
if(outfit_url_el) outfit_url_el.val(url); if(outfit_url_el) outfit_url_el.val(url);
} }
@ -605,10 +651,6 @@ View.Outfits = function (wardrobe) {
setOutfitPermalink(outfit, current_outfit_permalink_el); setOutfitPermalink(outfit, current_outfit_permalink_el);
} }
function setSharedOutfitPermalink(outfit) {
setOutfitPermalink(outfit, shared_outfit_permalink_el, shared_outfit_url_el);
}
function setActiveOutfit(outfit) { function setActiveOutfit(outfit) {
outfits_list_el.find('li.active').removeClass('active'); outfits_list_el.find('li.active').removeClass('active');
if(outfit.id) { if(outfit.id) {
@ -620,33 +662,210 @@ View.Outfits = function (wardrobe) {
} }
function updateActiveOutfit() { function updateActiveOutfit() {
setActiveOutfit(wardrobe.outfit.getOutfit()); setActiveOutfit(wardrobe.outfits.getOutfit());
} }
wardrobe.outfit.bind('setOutfit', setActiveOutfit); wardrobe.outfits.bind('setOutfit', setActiveOutfit);
wardrobe.outfit.bind('outfitNotFound', setActiveOutfit); wardrobe.outfits.bind('outfitNotFound', setActiveOutfit);
wardrobe.user.bind('outfitRenamed', function (outfit) { wardrobe.outfits.bind('outfitRenamed', function (outfit) {
if(outfit.id == wardrobe.outfit.getId()) { if(outfit.id == wardrobe.outfits.getId()) {
save_current_outfit_name_el.text(outfit.name); 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 */ /* Saving */
save_current_outfit_el.click(function () { save_current_outfit_el.click(function () {
wardrobe.outfit.update(); wardrobe.outfits.update();
}); });
new_outfit_form_el.submit(function (e) { new_outfit_form_el.submit(function (e) {
e.preventDefault(); e.preventDefault();
new_outfit_form_el.startLoading(); new_outfit_form_el.startLoading();
wardrobe.outfit.create({starred: new_outfit_form_el.hasClass('starred'), name: new_outfit_name_el.val()}); wardrobe.outfits.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();
}); });
new_outfit_form_el.find('div.outfit-star').click(function () { new_outfit_form_el.find('div.outfit-star').click(function () {
@ -664,32 +883,31 @@ View.Outfits = function (wardrobe) {
save_error_el.text(text).notify(); save_error_el.text(text).notify();
} }
wardrobe.outfit.bind('saveSuccess', function (outfit) { wardrobe.outfits.bind('saveSuccess', function (outfit) {
save_success_el.notify(); save_success_el.notify();
}); });
wardrobe.outfit.bind('createSuccess', function (outfit) { wardrobe.outfits.bind('createSuccess', function (outfit) {
wardrobe.user.addOutfit(outfit);
showOutfits(); showOutfits();
hideNewOutfitForm(); hideNewOutfitForm();
}); });
wardrobe.outfit.bind('updateSuccess', function (outfit) { function shareComplete(outfit) {
wardrobe.user.updateOutfit(outfit);
});
wardrobe.outfit.bind('shareSuccess', function (outfit) {
save_outfit_wrapper_el.stopLoading().addClass('shared-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() { function clearSharedOutfit() {
save_outfit_wrapper_el.removeClass('shared-outfit'); save_outfit_wrapper_el.removeClass('shared-outfit');
} }
wardrobe.outfit.bind('updateClosetItems', clearSharedOutfit); wardrobe.outfits.bind('updateClosetItems', clearSharedOutfit);
wardrobe.outfit.bind('updateWornItems', clearSharedOutfit); wardrobe.outfits.bind('updateWornItems', clearSharedOutfit);
wardrobe.outfit.bind('updatePetState', clearSharedOutfit); wardrobe.outfits.bind('updatePetState', clearSharedOutfit);
function saveFailure(outfit, response) { function saveFailure(outfit, response) {
var errors = response.errors; var errors = response.errors;
@ -712,16 +930,16 @@ View.Outfits = function (wardrobe) {
liForOutfit(outfit).stopLoading(); liForOutfit(outfit).stopLoading();
} }
wardrobe.outfit.bind('saveFailure', saveFailure); wardrobe.outfits.bind('saveFailure', saveFailure);
wardrobe.user.bind('saveFailure', saveFailure) wardrobe.outfits.bind('saveFailure', saveFailure)
wardrobe.outfit.bind('shareFailure', function (outfit, response) { wardrobe.outfits.bind('shareFailure', function (outfit, response) {
save_outfit_wrapper_el.stopLoading(); save_outfit_wrapper_el.stopLoading();
saveFailure(outfit, response); saveFailure(outfit, response);
}); });
/* Error */ /* Error */
wardrobe.outfit.bind('outfitNotFound', function () { wardrobe.outfits.bind('outfitNotFound', function () {
outfit_not_found_el.notify(); outfit_not_found_el.notify();
}); });
} }
@ -733,7 +951,7 @@ View.PetStateForm = function (wardrobe) {
button_query = form_query + ' button'; button_query = form_query + ' button';
$(button_query).live('click', function (e) { $(button_query).live('click', function (e) {
e.preventDefault(); e.preventDefault();
wardrobe.outfit.setPetStateById(+$(this).data('value')); wardrobe.outfits.setPetStateById(+$(this).data('value'));
}); });
function updatePetState(pet_state) { 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; var ids = pet_type.pet_state_ids, i, id, li, button, label;
ul.children().remove(); ul.children().remove();
if(ids.length == 1) { if(ids.length == 1) {
@ -761,18 +979,18 @@ View.PetStateForm = function (wardrobe) {
button.appendTo(li); button.appendTo(li);
li.appendTo(ul); 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) { View.PetTypeForm = function (wardrobe) {
var form = $('#pet-type-form'), dropdowns = {}, loaded = false; var form = $('#pet-type-form'), dropdowns = {}, loaded = false;
form.submit(function (e) { form.submit(function (e) {
e.preventDefault(); e.preventDefault();
wardrobe.outfit.setPetTypeByColorAndSpecies( wardrobe.outfits.setPetTypeByColorAndSpecies(
+dropdowns.color.val(), +dropdowns.species.val() +dropdowns.color.val(), +dropdowns.species.val()
); );
}).children('select').each(function () { }).children('select').each(function () {
@ -803,12 +1021,12 @@ View.PetTypeForm = function (wardrobe) {
}); });
}); });
loaded = true; 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'); $('#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'); var baseURL = link.attr('data-base-url');
function updateLink() { function updateLink() {
var assets = wardrobe.outfit.getVisibleAssets(); var assets = wardrobe.outfits.getVisibleAssets();
var url = baseURL + "?"; var url = baseURL + "?";
for(var i = 0; i < assets.length; i++) { for(var i = 0; i < assets.length; i++) {
@ -876,9 +1094,9 @@ View.ReportBrokenImage = function (wardrobe) {
link.attr('href', url); link.attr('href', url);
} }
wardrobe.outfit.bind('updateWornItems', updateLink); wardrobe.outfits.bind('updateWornItems', updateLink);
wardrobe.outfit.bind('updateItemAssets', updateLink); wardrobe.outfits.bind('updateItemAssets', updateLink);
wardrobe.outfit.bind('updatePetState', updateLink); wardrobe.outfits.bind('updatePetState', updateLink);
} }
View.Search = function (wardrobe) { View.Search = function (wardrobe) {

View file

@ -7,4 +7,4 @@ var main_wardrobe = new Wardrobe(), View = Wardrobe.getStandardView({
}); });
main_wardrobe.registerViews(View); main_wardrobe.registerViews(View);
main_wardrobe.initialize(); 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; return array1 == array2;
} }
temp = []; temp = [];
if ( (!array1[0]) || (!array2[0]) ) {
return false;
}
if (array1.length != array2.length) { if (array1.length != array2.length) {
return false; return false;
} }
@ -28,7 +25,7 @@ function arraysMatch(array1, array2) {
return true; return true;
} }
Array.prototype.map = function (property) { Array.prototype.mapProperty = function (property) {
return $.map(this, function (element) { return $.map(this, function (element) {
return element[property]; return element[property];
}); });
@ -78,9 +75,20 @@ function Wardrobe() {
function Asset(newData) { function Asset(newData) {
var asset = this; 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) { 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) { this.update = function (data) {
@ -267,16 +275,23 @@ function Wardrobe() {
worn_item_ids = new_ids.worn; worn_item_ids = new_ids.worn;
closet_item_ids = new_ids.unworn.concat(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') { if(typeof data != 'undefined') {
this.color_id = data.color_id; loadAttributes(data);
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;
} }
this.closet_items = []; this.closet_items = [];
@ -327,11 +342,11 @@ function Wardrobe() {
new_items = [], new_worn_item_ids = []; new_items = [], new_worn_item_ids = [];
if(added_item) { if(added_item) {
// now that we've loaded, check for conflicts on the 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; item_zones_length = item_zones.length;
for(var i = 0; i < outfit.worn_items.length; i++) { for(var i = 0; i < outfit.worn_items.length; i++) {
existing_item = outfit.worn_items[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; passed = true;
if(existing_item != added_item) { if(existing_item != added_item) {
for(var j = 0; j < item_zones_length; j++) { 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) { this.closetItem = function (item, updateClosetItemsCallback) {
if(!hasItemInCloset(item)) { if(!hasItemInCloset(item)) {
this.closet_items.push(item); this.closet_items.push(item);
@ -415,6 +413,14 @@ function Wardrobe() {
}); });
return visible_assets; 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.rename = function (new_name, success, failure) {
this.updateAttributes({name: new_name}, success, failure); this.updateAttributes({name: new_name}, success, failure);
@ -522,6 +528,9 @@ function Wardrobe() {
new_outfit.id = outfit.id; new_outfit.id = outfit.id;
new_outfit.name = outfit.name; new_outfit.name = outfit.name;
new_outfit.starred = outfit.starred; 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; return new_outfit;
} }
@ -539,6 +548,13 @@ function Wardrobe() {
new_ids.unworn = base_ids.unworn.slice(0); new_ids.unworn = base_ids.unworn.slice(0);
outfit.setWornAndUnwornItemIds(new_ids); 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) { this.destroy = function (success) {
$.ajax({ $.ajax({
@ -554,10 +570,11 @@ function Wardrobe() {
url: '/outfits', url: '/outfits',
type: 'post', type: 'post',
data: {outfit: getAttributes()}, data: {outfit: getAttributes()},
dataType: 'json',
success: function (data) { success: function (data) {
new_record = false; new_record = false;
outfit.id = data; updateFromSaveResponse(data);
Outfit.cache[data] = outfit; Outfit.cache[outfit.id] = outfit;
success(outfit); success(outfit);
}, },
error: function (xhr) { 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) { this.updateAttributes = function (attributes, success, failure) {
var outfit_data = {}; var outfit_data = {};
@ -583,19 +626,23 @@ function Wardrobe() {
if(typeof Outfit.cache[id] !== 'undefined') { if(typeof Outfit.cache[id] !== 'undefined') {
callback(Outfit.cache[id]); callback(Outfit.cache[id]);
} else { } else {
$.ajax({ Outfit.load(id, callback);
url: '/outfits/' + id + '.json',
success: function (data) {
var outfit = new Outfit(data);
Outfit.cache[id] = outfit;
callback(outfit);
},
error: function () {
callback(null);
}
});
} }
} }
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) { Outfit.loadForCurrentUser = function (success) {
var outfits = []; var outfits = [];
@ -788,8 +835,13 @@ function Wardrobe() {
Controller.all = {}; Controller.all = {};
Controller.all.Outfit = function OutfitController() { Controller.all.Outfits = function OutfitsController() {
var controller = this, outfit = new Outfit; // 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; this.in_transaction = false;
@ -862,6 +914,7 @@ function Wardrobe() {
} }
outfit.create( outfit.create(
function (outfit) { function (outfit) {
insertOutfit(outfit);
controller.events.trigger('saveSuccess', outfit); controller.events.trigger('saveSuccess', outfit);
controller.events.trigger('createSuccess', outfit); controller.events.trigger('createSuccess', outfit);
controller.events.trigger('setOutfit', outfit); controller.events.trigger('setOutfit', outfit);
@ -920,12 +973,24 @@ function Wardrobe() {
} }
this.share = function () { this.share = function () {
var sharedOutfit = outfit.clone(); if(outfit.id) {
sharedOutfit.anonymous = true; // If this is a user-saved outfit (user is logged in), no need to
sharedOutfit.create( // re-share it. Skip to using the current outfit.
controller.event('shareSuccess'), controller.events.trigger('shareSkipped', outfit);
controller.event('shareFailure') } 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) { this.unclosetItem = function (item) {
@ -943,6 +1008,7 @@ function Wardrobe() {
this.update = function () { this.update = function () {
outfit.update( outfit.update(
function (outfit) { function (outfit) {
updateUserOutfit(outfit);
controller.events.trigger('saveSuccess', outfit), controller.events.trigger('saveSuccess', outfit),
controller.events.trigger('updateSuccess', outfit) controller.events.trigger('updateSuccess', outfit)
}, },
@ -958,6 +1024,159 @@ function Wardrobe() {
controller.event('updateItemAssets') 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() { 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; var underscored_name;
for(var name in Controller.all) { for(var name in Controller.all) {
@ -1196,13 +1325,13 @@ Wardrobe.getStandardView = function (options) {
var outfit_events = ['updateWornItems', 'updateClosetItems', 'updateItemAssets', 'updatePetType', 'updatePetState']; var outfit_events = ['updateWornItems', 'updateClosetItems', 'updateItemAssets', 'updatePetType', 'updatePetState'];
for(var i = 0; i < outfit_events.length; i++) { for(var i = 0; i < outfit_events.length; i++) {
(function (event) { (function (event) {
wardrobe.outfit.bind(event, function (obj) { wardrobe.outfits.bind(event, function (obj) {
log(event, obj); log(event, obj);
}); });
})(outfit_events[i]); })(outfit_events[i]);
} }
wardrobe.outfit.bind('petTypeNotFound', function (pet_type) { wardrobe.outfits.bind('petTypeNotFound', function (pet_type) {
log(pet_type.toString() + ' not found'); log(pet_type.toString() + ' not found');
}); });
} }
@ -1246,7 +1375,7 @@ Wardrobe.getStandardView = function (options) {
var assets, assets_for_swf; var assets, assets_for_swf;
if(update_pending_flash) return false; if(update_pending_flash) return false;
if(preview_swf && preview_swf.setAssets) { if(preview_swf && preview_swf.setAssets) {
assets = wardrobe.outfit.getVisibleAssets(); assets = wardrobe.outfits.getVisibleAssets();
preview_swf.setAssets(assets); preview_swf.setAssets(assets);
} else { } else {
update_pending_flash = true; 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 // Get a copy of the visible assets, then sort them in ascending zone
// order. // order.
var assets = wardrobe.outfit.getVisibleAssets().slice(0); var assets = wardrobe.outfits.getVisibleAssets().slice(0);
assets.sort(function (a, b) { assets.sort(function (a, b) {
return a.depth - b.depth; return a.depth - b.depth;
}); });
console.log(assets.mapProperty('id'));return;
for(var i = 0; i < assets.length; i++) { for(var i = 0; i < assets.length; i++) {
url += "," + encodeURIComponent(assets[i].imageURL(size)); url += "," + encodeURIComponent(assets[i].imageURL(size));
@ -1316,7 +1446,7 @@ Wardrobe.getStandardView = function (options) {
} }
this.updateAssets = function () { this.updateAssets = function () {
var assets = wardrobe.outfit.getVisibleAssets(), asset, var assets = wardrobe.outfits.getVisibleAssets(), asset,
availableAssets = []; availableAssets = [];
pendingAssets = {}; pendingAssets = {};
pendingAssetsCount = 0; pendingAssetsCount = 0;
@ -1375,10 +1505,9 @@ Wardrobe.getStandardView = function (options) {
for(var i in sizes) { for(var i in sizes) {
if(!sizes.hasOwnProperty(i)) continue; if(!sizes.hasOwnProperty(i)) continue;
size = sizes[i]; size = sizes[i];
size[2] = size[0] * size[1];
inserted = false; inserted = false;
for(var i in SIZES_SMALL_TO_LARGE) { 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); SIZES_SMALL_TO_LARGE.splice(i, 0, size);
inserted = true; inserted = true;
break; break;
@ -1476,9 +1605,9 @@ Wardrobe.getStandardView = function (options) {
preview.adapter.updateAssets(); preview.adapter.updateAssets();
} }
wardrobe.outfit.bind('updateWornItems', updateAssets); wardrobe.outfits.bind('updateWornItems', updateAssets);
wardrobe.outfit.bind('updateItemAssets', updateAssets); wardrobe.outfits.bind('updateItemAssets', updateAssets);
wardrobe.outfit.bind('updatePetState', updateAssets); wardrobe.outfits.bind('updatePetState', updateAssets);
function useAdapter(name) { function useAdapter(name) {
preview.adapter = new Adapter[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.