Compare commits

...

9 commits

Author SHA1 Message Date
d9bf4f745b Skip glitched appearances in bulk-labeling mode
It's not as important to label glitched states, and sometimes the glitch
prevents it from being visually identifiable. Don't sweat 'em!
2024-12-01 10:39:19 -08:00
407c4b38a5 Add link to reference pet type when labeling pet appearances
Sometimes I forget like, what the masc/fem variants of a given pet
actually look like? Some are super obvious about things like eyelashes,
and others use more subtle eye differences.

This is a cheap lil hack to make it easier to open a reference! Ideally
I think it would be neat to like, when you hover over an option, have
it show you the reference variant of that pose? But this is good enough
I think!
2024-12-01 10:28:58 -08:00
6dc5aa28a4 When labeling pet appearances on mobile, give pose options equal height
If the screen is narrow, many of the bubbles will wrap their text onto
two lines, but "Unconverted" won't. Give it equal height to the rest
anyway, for visual consistency!
2024-12-01 10:27:38 -08:00
b656ccd982 Add bulk labeling mode for pet appearances
We copy the same feature from alt styles, now that the UI is shared via
support form helpers! Easy peasy!

This adds a "Then: Go to unlabeled appearance" checkbox next to the
submit button on the pet appearance edit form. If checked, it takes you
to the first unlabeled appearance in the database, and keeps the box
checked for next time. Slam through 'em!
2024-12-01 10:10:33 -08:00
02836494ae Add debug gem in development
This helped me debug a thing in the upcoming change! It lets you drop a
`debugger` line into the app, then run `rdbg --attach` in another
terminal to get into a debug session. Neat!
2024-12-01 10:05:54 -08:00
b6e6f27fdf Minor refactor to support_form_with implementation
Realizing that, with the keyword argument spread syntax, I don't need
to do merging, I can just. spread at the right place!

My rationale for the ordering here is: if the caller theoretically tried
to override the builder (even though I don't see why), I think we would
want to respect that. Whereas the `class` argument should be overridden
because we're safely *merging* our `.support-form` class into it.
2024-12-01 09:45:26 -08:00
aeb00f73cf Extract alt style's "go to next" field into a support form helper
I want to reuse this for unlabeled pet styles is why! (That's been the
immediate motivation for this refactor, but also I do just like that
it'll make support forms easier to build.)
2024-12-01 09:42:19 -08:00
06a301e6d7 Add actions helper to support form builder 2024-12-01 09:30:17 -08:00
1119bbb292 Extract the more complex support form helpers into templates
I think helpers are fine for the simpler ones that are basically *just*
wrapper tags, but once it starts getting into `concat`, I think that's
too unfamiliar of a syntax for developers; let's bail into our usual
templating system!

I'm not sure about putting them in `application/support_form` like this.
That's cute for one-offs like `application/hanger_spinner`, because
`render partial: "hanger_spinner"` assumes the `application` view folder
by default, but that doesn't work once it's nested: it looks for a
`views/support_form` folder.

I think maybe it could soon be time to bail from the strict "view
folders belong to controllers" thing, similar to how we did for
`SupportFormHelper`, and add a `components` folder or similar? Idk, not
sure yet!
2024-12-01 09:26:40 -08:00
15 changed files with 124 additions and 48 deletions

View file

@ -66,7 +66,10 @@ gem "async-http", "~> 0.75.0", require: false
gem "thread-local", "~> 1.1", require: false gem "thread-local", "~> 1.1", require: false
# For debugging. # For debugging.
gem 'web-console', '~> 4.2', group: :development group :development do
gem 'debug', '~> 1.9.2'
gem 'web-console', '~> 4.2'
end
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '~> 1.16', require: false gem 'bootsnap', '~> 1.16', require: false

View file

@ -134,6 +134,9 @@ GEM
crass (1.0.6) crass (1.0.6)
csv (3.3.0) csv (3.3.0)
date (3.3.4) date (3.3.4)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
devise (4.9.4) devise (4.9.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
@ -521,6 +524,7 @@ DEPENDENCIES
async (~> 2.17) async (~> 2.17)
async-http (~> 0.75.0) async-http (~> 0.75.0)
bootsnap (~> 1.16) bootsnap (~> 1.16)
debug (~> 1.9.2)
devise (~> 4.9, >= 4.9.2) devise (~> 4.9, >= 4.9.2)
devise-encryptable (~> 0.2.0) devise-encryptable (~> 0.2.0)
dotenv-rails (~> 2.8, >= 2.8.1) dotenv-rails (~> 2.8, >= 2.8.1)

View file

@ -44,6 +44,10 @@
grid-template-columns: repeat(var(--num-columns, 1), 1fr) grid-template-columns: repeat(var(--num-columns, 1), 1fr)
gap: .25em gap: .25em
li
display: flex
align-items: stretch // Give the bubbles equal heights!
label label
display: flex display: flex
align-items: center align-items: center
@ -51,6 +55,7 @@
padding: .5em 1em padding: .5em 1em
border: 1px solid $soft-border-color border: 1px solid $soft-border-color
border-radius: 1em border-radius: 1em
flex: 1 1 auto
input input
margin: 0 margin: 0
@ -89,7 +94,7 @@
align-items: center align-items: center
gap: 1em gap: 1em
label .go-to-next
display: flex display: flex
align-items: center align-items: center
gap: .25em gap: .25em

View file

@ -3,3 +3,13 @@ outfit-viewer
.fields li[data-type=radio-grid] .fields li[data-type=radio-grid]
--num-columns: 3 --num-columns: 3
.reference-link
display: flex
align-items: center
gap: .5em
padding-inline: .5em
img
height: 2em
width: auto

View file

@ -8,7 +8,7 @@ class PetStatesController < ApplicationController
def update def update
if @pet_state.update(pet_state_params) if @pet_state.update(pet_state_params)
flash[:notice] = "Pet appearance \##{@pet_state.id} successfully saved!" flash[:notice] = "Pet appearance \##{@pet_state.id} successfully saved!"
redirect_to @pet_type redirect_to destination_after_save
else else
render action: :edit, status: :bad_request render action: :edit, status: :bad_request
end end
@ -19,9 +19,35 @@ class PetStatesController < ApplicationController
def find_pet_state def find_pet_state
@pet_type = PetType.find_by_param!(params[:pet_type_name]) @pet_type = PetType.find_by_param!(params[:pet_type_name])
@pet_state = @pet_type.pet_states.find(params[:id]) @pet_state = @pet_type.pet_states.find(params[:id])
@reference_pet_type = @pet_type.reference
end end
def pet_state_params def pet_state_params
params.require(:pet_state).permit(:pose, :glitched) params.require(:pet_state).permit(:pose, :glitched)
end end
def destination_after_save
if params[:next] == "unlabeled-appearance"
next_unlabeled_appearance_path
else
@pet_type
end
end
def next_unlabeled_appearance_path
# Rather than just getting the newest unlabeled pet state, prioritize the
# newest *pet type*. This better matches the user's perception of what the
# newest state is, because the Rainbow Pool UI is grouped by pet type!
unlabeled_appearance = PetState.needs_labeling.newest_pet_type.newest.first
if unlabeled_appearance
edit_pet_type_pet_state_path(
unlabeled_appearance.pet_type,
unlabeled_appearance,
next: "unlabeled-appearance"
)
else
@pet_type
end
end
end end

View file

@ -1,44 +1,29 @@
module SupportFormHelper module SupportFormHelper
class SupportFormBuilder < ActionView::Helpers::FormBuilder class SupportFormBuilder < ActionView::Helpers::FormBuilder
attr_reader :template attr_reader :template
delegate :concat, :content_tag, :image_tag, to: :template, private: true delegate :capture, :check_box_tag, :content_tag, :params, :render,
to: :template, private: true
def errors def errors
return nil if object.errors.empty? render partial: "application/support_form/errors", locals: {form: self}
error_list = content_tag(:ul) do
object.errors.each do |error|
concat content_tag(:li, error.full_message)
end
end
content_tag(:p) do
concat "Could not save:"
concat error_list
end
end end
def fields(&block) def fields(&block)
content_tag(:ul, class: "fields", &block) content_tag(:ul, class: "fields", &block)
end end
def field(**kwargs, &block) def field(**options, &block)
content_tag(:li, **kwargs, &block) content_tag(:li, **options, &block)
end end
def radio_fieldset(legend, **kwargs, &block) def radio_fieldset(legend, **options, &block)
kwargs.reverse_merge!("data-type": "radio") render partial: "application/support_form/radio_fieldset",
field(**kwargs) do locals: {form: self, legend:, options:, content: capture(&block)}
content_tag(:fieldset) do
concat content_tag(:legend, legend)
concat content_tag(:ul, &block)
end
end
end end
def radio_field(**kwargs, &block) def radio_field(**options, &block)
content_tag(:li) do content_tag(:li) do
content_tag(:label, **kwargs, &block) content_tag(:label, **options, &block)
end end
end end
@ -47,19 +32,29 @@ module SupportFormHelper
end end
def thumbnail_input(method) def thumbnail_input(method)
url = object.send(method) render partial: "application/support_form/thumbnail_input",
content_tag(:div, class: "thumbnail-input") do locals: {form: self, method:}
concat image_tag(url, alt: "Thumbnail") if url.present?
concat url_field(method)
end end
def actions(&block)
content_tag(:section, class: "actions", &block)
end
def go_to_next_field(**options, &block)
content_tag(:label, class: "go-to-next", **options, &block)
end
def go_to_next_check_box(value)
check_box_tag "next", value, checked: params[:next] == value
end end
end end
def support_form_with(**kwargs, &block) def support_form_with(**options, &block)
kwargs.merge!( form_with(
builder: SupportFormBuilder, builder: SupportFormBuilder,
class: ["support-form", kwargs[:class]], **options,
class: ["support-form", options[:class]],
&block
) )
form_with(**kwargs, &block)
end end
end end

View file

@ -17,6 +17,11 @@ class PetState < ApplicationRecord
alias_method :swf_asset_ids_from_association, :swf_asset_ids alias_method :swf_asset_ids_from_association, :swf_asset_ids
scope :newest, -> { order(created_at: :desc) }
scope :newest_pet_type, -> { joins(:pet_type).merge(PetType.newest) }
scope :unlabeled, -> { with_pose("UNKNOWN") }
scope :needs_labeling, -> { unlabeled.where(glitched: false) }
# A simple ordering that tries to bring reliable pet states to the front. # A simple ordering that tries to bring reliable pet states to the front.
scope :emotion_order, -> { scope :emotion_order, -> {
order(Arel.sql( order(Arel.sql(

View file

@ -15,6 +15,7 @@ class PetType < ApplicationRecord
species = Species.find_by_name!(species_name) species = Species.find_by_name!(species_name)
where(color_id: color.id, species_id: species.id) where(color_id: color.id, species_id: species.id)
} }
scope :newest, -> { order(created_at: :desc) }
scope :preferring_species, ->(species_id) { scope :preferring_species, ->(species_id) {
joins(:species).order([Arel.sql("species_id = ? DESC"), species_id]) joins(:species).order([Arel.sql("species_id = ? DESC"), species_id])
} }
@ -136,6 +137,10 @@ class PetType < ApplicationRecord
pet_states.count { |ps| ps.pose == "UNKNOWN" } pet_states.count { |ps| ps.pose == "UNKNOWN" }
end end
def reference
PetType.where(species_id: species).basic.merge(Color.alphabetical).first
end
def self.find_by_param!(param) def self.find_by_param!(param)
raise ActiveRecord::RecordNotFound unless param.include?("-") raise ActiveRecord::RecordNotFound unless param.include?("-")
color_param, _, species_param = param.rpartition("-") color_param, _, species_param = param.rpartition("-")

View file

@ -26,11 +26,10 @@
= f.label :thumbnail_url, "Thumbnail" = f.label :thumbnail_url, "Thumbnail"
= f.thumbnail_input :thumbnail_url = f.thumbnail_input :thumbnail_url
.actions = f.actions do
= f.submit "Save changes" = f.submit "Save changes"
%label{title: "If checked, takes you to the next unlabeled pet style, if any. Useful for labeling in bulk!"} = f.go_to_next_field title: "If checked, takes you to the next unlabeled pet style, if any. Useful for labeling in bulk!" do
= check_box_tag "next", "unlabeled-style", = f.go_to_next_check_box "unlabeled-style"
checked: params[:next] == "unlabeled-style"
Then: Go to unlabeled style Then: Go to unlabeled style
- content_for :stylesheets do - content_for :stylesheets do

View file

@ -0,0 +1,7 @@
- if form.object.errors.any?
%section.errors
Could not save:
%ul
- form.object.errors.each do |error|
%li= error.full_message

View file

@ -0,0 +1,4 @@
= form.field("data-type": "radio", **options) do
%fieldset
%legend= legend
%ul= content

View file

@ -0,0 +1,5 @@
- url = form.object.send(method)
.thumbnail-input
- if url.present?
= image_tag url, alt: "Thumbnail"
= form.url_field method

View file

@ -51,7 +51,7 @@
= f.radio_button :explicitly_body_specific, true = f.radio_button :explicitly_body_specific, true
Body-specific: Fits all species differently Body-specific: Fits all species differently
.actions = f.actions do
= f.submit "Save changes" = f.submit "Save changes"
- content_for :stylesheets do - content_for :stylesheets do

View file

@ -26,14 +26,22 @@
= f.radio_field do = f.radio_field do
= f.radio_button :pose, pose = f.radio_button :pose, pose
= pose_name(pose) = pose_name(pose)
- if @reference_pet_type
= link_to @reference_pet_type, target: "_blank", class: "reference-link" do
= pet_type_image @reference_pet_type, :happy, :face
%span Reference: #{@reference_pet_type.human_name}
= external_link_icon
= f.field do = f.field do
= f.label :glitched, "Gliched?" = f.label :glitched, "Gliched?"
= f.select :glitched, [["✅ Not marked as Glitched", false], = f.select :glitched, [["✅ Not marked as Glitched", false],
["👾 Yes, it's bad news bonko'd", true]] ["👾 Yes, it's bad news bonko'd", true]]
.actions = f.actions do
= f.submit "Save changes" = f.submit "Save changes"
= f.go_to_next_field title: "If checked, takes you to the first unlabeled appearance in the database, if any. Useful for labeling in bulk!" do
= f.go_to_next_check_box "unlabeled-appearance"
Then: Go to unlabeled appearance
- content_for :stylesheets do - content_for :stylesheets do
= stylesheet_link_tag "application/breadcrumbs" = stylesheet_link_tag "application/breadcrumbs"

BIN
vendor/cache/debug-1.9.2.gem vendored Normal file

Binary file not shown.