Compare commits
6 commits
e3d46ea5d0
...
e5bf6d6ba1
Author | SHA1 | Date | |
---|---|---|---|
e5bf6d6ba1 | |||
61a4dcad02 | |||
a8cbce0864 | |||
52e81557c2 | |||
a3dcaa0f0e | |||
ab1fade529 |
10 changed files with 340 additions and 95 deletions
|
@ -30,7 +30,24 @@ class ItemsController < ApplicationController
|
||||||
items: @items.as_json(
|
items: @items.as_json(
|
||||||
methods: [:nc?, :pb?, :owned?, :wanted?],
|
methods: [:nc?, :pb?, :owned?, :wanted?],
|
||||||
),
|
),
|
||||||
appearances: load_appearances,
|
appearances: load_appearances.as_json(
|
||||||
|
include: {
|
||||||
|
swf_assets: {
|
||||||
|
only: [:id, :remote_id, :body_id],
|
||||||
|
include: {
|
||||||
|
zone: {
|
||||||
|
only: [:id, :depth, :label],
|
||||||
|
methods: [:is_commonly_used_by_items],
|
||||||
|
},
|
||||||
|
restricted_zones: {
|
||||||
|
only: [:id, :depth, :label],
|
||||||
|
methods: [:is_commonly_used_by_items],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: [:urls, :known_glitches],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
total_pages: @items.total_pages,
|
total_pages: @items.total_pages,
|
||||||
query: @query.to_s,
|
query: @query.to_s,
|
||||||
}
|
}
|
||||||
|
@ -104,11 +121,12 @@ class ItemsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_appearances
|
def load_appearances
|
||||||
pet_type_name = params[:with_appearances_for]
|
appearance_params = params[:with_appearances_for]
|
||||||
return {} if pet_type_name.blank?
|
return {} if appearance_params.blank?
|
||||||
|
|
||||||
pet_type = Item::Search::Query.load_pet_type_by_name(pet_type_name)
|
pet_type = Item::Search::Query.load_pet_type_by_color_and_species(
|
||||||
pet_type.appearances_for(@items.map(&:id))
|
appearance_params[:color_id], appearance_params[:species_id])
|
||||||
|
pet_type.appearances_for(@items.map(&:id), swf_asset_includes: [:zone])
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_error(e)
|
def search_error(e)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
import React from "react";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { useDebounce } from "../util";
|
import { useDebounce, useLocalStorage } from "../util";
|
||||||
import { emptySearchQuery } from "./SearchToolbar";
|
import { useItemSearch } from "../loaders/items";
|
||||||
|
import { emptySearchQuery, searchQueryIsEmpty } from "./SearchToolbar";
|
||||||
import { itemAppearanceFragment } from "../components/useOutfitAppearance";
|
import { itemAppearanceFragment } from "../components/useOutfitAppearance";
|
||||||
import { SEARCH_PER_PAGE } from "./SearchPanel";
|
import { SEARCH_PER_PAGE } from "./SearchPanel";
|
||||||
|
|
||||||
|
@ -41,11 +43,20 @@ export function useSearchResults(
|
||||||
const currentPageIndex = currentPageNumber - 1;
|
const currentPageIndex = currentPageNumber - 1;
|
||||||
const offset = currentPageIndex * SEARCH_PER_PAGE;
|
const offset = currentPageIndex * SEARCH_PER_PAGE;
|
||||||
|
|
||||||
// Here's the actual GQL query! At the bottom we have more config than usual!
|
// Until the new item search is ready, we can toggle between them! Use
|
||||||
|
// `setItemSearchQueryMode` in the JS console to choose "gql" or "new".
|
||||||
|
const [queryMode, setQueryMode] = useLocalStorage(
|
||||||
|
"DTIItemSearchQueryMode",
|
||||||
|
"gql",
|
||||||
|
);
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.setItemSearchQueryMode = setQueryMode;
|
||||||
|
}, [setQueryMode]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading: loadingGQL,
|
loading: loadingGQL,
|
||||||
error,
|
error: errorGQL,
|
||||||
data,
|
data: dataGQL,
|
||||||
} = useQuery(
|
} = useQuery(
|
||||||
gql`
|
gql`
|
||||||
query SearchPanel(
|
query SearchPanel(
|
||||||
|
@ -122,6 +133,7 @@ export function useSearchResults(
|
||||||
context: { sendAuth: true },
|
context: { sendAuth: true },
|
||||||
skip:
|
skip:
|
||||||
skip ||
|
skip ||
|
||||||
|
queryMode !== "gql" ||
|
||||||
(!debouncedQuery.value &&
|
(!debouncedQuery.value &&
|
||||||
!debouncedQuery.filterToItemKind &&
|
!debouncedQuery.filterToItemKind &&
|
||||||
!debouncedQuery.filterToZoneLabel &&
|
!debouncedQuery.filterToZoneLabel &&
|
||||||
|
@ -134,10 +146,69 @@ export function useSearchResults(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const loading = debouncedQuery !== query || loadingGQL;
|
const {
|
||||||
const items = data?.itemSearch?.items ?? [];
|
isLoading: loadingQuery,
|
||||||
const numTotalItems = data?.itemSearch?.numTotalItems ?? null;
|
error: errorQuery,
|
||||||
const numTotalPages = Math.ceil(numTotalItems / SEARCH_PER_PAGE);
|
data: dataQuery,
|
||||||
|
} = useItemSearch(
|
||||||
|
{
|
||||||
|
filters: buildSearchFilters(debouncedQuery, outfitState),
|
||||||
|
withAppearancesFor: { speciesId, colorId, altStyleId },
|
||||||
|
page: currentPageIndex + 1,
|
||||||
|
perPage: SEARCH_PER_PAGE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled:
|
||||||
|
!skip && queryMode === "new" && !searchQueryIsEmpty(debouncedQuery),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const loading =
|
||||||
|
debouncedQuery !== query ||
|
||||||
|
(queryMode === "gql" ? loadingGQL : loadingQuery);
|
||||||
|
const error = queryMode === "gql" ? errorGQL : errorQuery;
|
||||||
|
const items =
|
||||||
|
(queryMode === "gql" ? dataGQL?.itemSearch?.items : dataQuery?.items) ?? [];
|
||||||
|
const numTotalPages =
|
||||||
|
(queryMode === "gql"
|
||||||
|
? Math.ceil((dataGQL?.itemSearch?.numTotalItems ?? 0) / SEARCH_PER_PAGE)
|
||||||
|
: dataQuery?.numTotalPages) ?? 0;
|
||||||
|
|
||||||
return { loading, error, items, numTotalPages };
|
return { loading, error, items, numTotalPages };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSearchFilters(query, { speciesId, colorId, altStyleId }) {
|
||||||
|
const filters = [];
|
||||||
|
|
||||||
|
if (query.value) {
|
||||||
|
filters.push({ key: "name", value: query.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.filterToItemKind === "NC") {
|
||||||
|
filters.push({ key: "is_nc" });
|
||||||
|
} else if (query.filterToItemKind === "PB") {
|
||||||
|
filters.push({ key: "is_pb" });
|
||||||
|
} else if (query.filterToItemKind === "NP") {
|
||||||
|
filters.push({ key: "is_np" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.filterToZoneLabel != null) {
|
||||||
|
filters.push({
|
||||||
|
key: "occupied_zone_set_name",
|
||||||
|
value: query.filterToZoneLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.filterToCurrentUserOwnsOrWants === "OWNS") {
|
||||||
|
filters.push({ key: "user_closet_hanger_ownership", value: "true" });
|
||||||
|
} else if (query.filterToCurrentUserOwnsOrWants === "WANTS") {
|
||||||
|
filters.push({ key: "user_closet_hanger_ownership", value: "false" });
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
key: "fits",
|
||||||
|
value: { speciesId, colorId, altStyleId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { normalizeSwfAssetToLayer } from "./shared-types";
|
||||||
|
|
||||||
export function useAltStylesForSpecies(speciesId, options = {}) {
|
export function useAltStylesForSpecies(speciesId, options = {}) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
...options,
|
...options,
|
||||||
|
@ -63,21 +65,3 @@ function normalizeAltStyle(altStyleData) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSwfAssetToLayer(swfAssetData) {
|
|
||||||
return {
|
|
||||||
id: String(swfAssetData.id),
|
|
||||||
zone: {
|
|
||||||
id: String(swfAssetData.zone.id),
|
|
||||||
depth: swfAssetData.zone.depth,
|
|
||||||
label: swfAssetData.zone.label,
|
|
||||||
},
|
|
||||||
bodyId: swfAssetData.body_id,
|
|
||||||
knownGlitches: swfAssetData.known_glitches,
|
|
||||||
|
|
||||||
svgUrl: swfAssetData.urls.svg,
|
|
||||||
canvasMovieLibraryUrl: swfAssetData.urls.canvas_library,
|
|
||||||
imageUrl: swfAssetData.urls.png,
|
|
||||||
swfUrl: swfAssetData.urls.swf,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { normalizeSwfAssetToLayer, normalizeZone } from "./shared-types";
|
||||||
|
|
||||||
export function useItemAppearances(id, options = {}) {
|
export function useItemAppearances(id, options = {}) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
...options,
|
...options,
|
||||||
|
@ -9,7 +11,9 @@ export function useItemAppearances(id, options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadItemAppearancesData(id) {
|
async function loadItemAppearancesData(id) {
|
||||||
const res = await fetch(`/items/${encodeURIComponent(id)}/appearances.json`);
|
const res = await fetch(
|
||||||
|
`/items/${encodeURIComponent(id)}/appearances.json`,
|
||||||
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -20,27 +24,121 @@ async function loadItemAppearancesData(id) {
|
||||||
return res.json().then(normalizeItemAppearancesData);
|
return res.json().then(normalizeItemAppearancesData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useItemSearch(searchOptions, queryOptions = {}) {
|
||||||
|
// Item searches are considered fresh for an hour, unless the search
|
||||||
|
// includes user-specific filters, in which case React Query will pretty
|
||||||
|
// aggressively reload it!
|
||||||
|
const includesUserSpecificFilters = searchOptions.filters.some(
|
||||||
|
(f) => f.key === "user_closet_hanger_ownership",
|
||||||
|
);
|
||||||
|
const staleTime = includesUserSpecificFilters ? 0 : 1000 * 60 * 5;
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
...queryOptions,
|
||||||
|
queryKey: ["itemSearch", buildItemSearchParams(searchOptions)],
|
||||||
|
queryFn: () => loadItemSearch(searchOptions),
|
||||||
|
staleTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildItemSearchParams({
|
||||||
|
filters = [],
|
||||||
|
withAppearancesFor = null,
|
||||||
|
page = 1,
|
||||||
|
perPage = 30,
|
||||||
|
}) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const [i, { key, value, isPositive }] of filters.entries()) {
|
||||||
|
params.append(`q[${i}][key]`, key);
|
||||||
|
if (key === "fits") {
|
||||||
|
params.append(`q[${i}][value][species_id]`, value.speciesId);
|
||||||
|
params.append(`q[${i}][value][color_id]`, value.colorId);
|
||||||
|
if (value.altStyleId != null) {
|
||||||
|
params.append(`q[${i}][value][alt_style_id]`, value.altStyleId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params.append(`q[${i}][value]`, value);
|
||||||
|
}
|
||||||
|
if (isPositive == false) {
|
||||||
|
params.append(`q[${i}][is_positive]`, "false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (withAppearancesFor != null) {
|
||||||
|
const { speciesId, colorId, altStyleId } = withAppearancesFor;
|
||||||
|
params.append(`with_appearances_for[species_id]`, speciesId);
|
||||||
|
params.append(`with_appearances_for[color_id]`, colorId);
|
||||||
|
if (altStyleId != null) {
|
||||||
|
params.append(`with_appearances_for[alt_style_id]`, altStyleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params.append("page", page);
|
||||||
|
params.append("per_page", perPage);
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadItemSearch(searchOptions) {
|
||||||
|
const params = buildItemSearchParams(searchOptions);
|
||||||
|
|
||||||
|
const res = await fetch(`/items.json?${params}`);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`loading item search failed: ${res.status} ${res.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
.json()
|
||||||
|
.then((data) => normalizeItemSearchData(data, searchOptions));
|
||||||
|
}
|
||||||
|
window.loadItemSearch = loadItemSearch;
|
||||||
|
|
||||||
function normalizeItemAppearancesData(data) {
|
function normalizeItemAppearancesData(data) {
|
||||||
return {
|
return {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
appearances: data.appearances.map((appearance) => ({
|
appearances: data.appearances.map((appearance) => ({
|
||||||
body: normalizeBody(appearance.body),
|
body: normalizeBody(appearance.body),
|
||||||
swfAssets: appearance.swf_assets.map((asset) => ({
|
swfAssets: appearance.swf_assets.map(normalizeSwfAssetToLayer),
|
||||||
id: String(asset.id),
|
|
||||||
knownGlitches: asset.known_glitches,
|
|
||||||
zone: normalizeZone(asset.zone),
|
|
||||||
restrictedZones: asset.restricted_zones.map((z) => normalizeZone(z)),
|
|
||||||
urls: {
|
|
||||||
swf: asset.urls.swf,
|
|
||||||
png: asset.urls.png,
|
|
||||||
manifest: asset.urls.manifest,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
})),
|
})),
|
||||||
restrictedZones: data.restricted_zones.map((z) => normalizeZone(z)),
|
restrictedZones: data.restricted_zones.map((z) => normalizeZone(z)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeItemSearchData(data, searchOptions) {
|
||||||
|
return {
|
||||||
|
id: buildItemSearchParams(searchOptions),
|
||||||
|
numTotalPages: data.total_pages,
|
||||||
|
items: data.items.map((item) => ({
|
||||||
|
id: String(item.id),
|
||||||
|
name: item.name,
|
||||||
|
thumbnailUrl: item.thumbnail_url,
|
||||||
|
isNc: item["nc?"],
|
||||||
|
isPb: item["pb?"],
|
||||||
|
currentUserOwnsThis: item["owned?"],
|
||||||
|
currentUserWantsThis: item["wanted?"],
|
||||||
|
appearanceOn: normalizeItemSearchAppearance(
|
||||||
|
data.appearances[item.id],
|
||||||
|
item,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeItemSearchAppearance(data, item) {
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `item-${item.id}-body-${data.body.id}`,
|
||||||
|
layers: data.swf_assets.map(normalizeSwfAssetToLayer),
|
||||||
|
restrictedZones: data.swf_assets
|
||||||
|
.map((a) => a.restricted_zones)
|
||||||
|
.flat()
|
||||||
|
.map(normalizeZone),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeBody(body) {
|
function normalizeBody(body) {
|
||||||
if (String(body.id) === "0") {
|
if (String(body.id) === "0") {
|
||||||
return { id: "0" };
|
return { id: "0" };
|
||||||
|
@ -55,7 +153,3 @@ function normalizeBody(body) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeZone(zone) {
|
|
||||||
return { id: String(zone.id), label: zone.label, depth: zone.depth };
|
|
||||||
}
|
|
||||||
|
|
23
app/javascript/wardrobe-2020/loaders/shared-types.js
Normal file
23
app/javascript/wardrobe-2020/loaders/shared-types.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
export function normalizeSwfAssetToLayer(data) {
|
||||||
|
return {
|
||||||
|
id: String(data.id),
|
||||||
|
remoteId: String(data.remote_id),
|
||||||
|
zone: normalizeZone(data.zone),
|
||||||
|
bodyId: data.body_id,
|
||||||
|
knownGlitches: data.known_glitches,
|
||||||
|
|
||||||
|
svgUrl: data.urls.svg,
|
||||||
|
canvasMovieLibraryUrl: data.urls.canvas_library,
|
||||||
|
imageUrl: data.urls.png,
|
||||||
|
swfUrl: data.urls.swf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeZone(data) {
|
||||||
|
return {
|
||||||
|
id: String(data.id),
|
||||||
|
depth: data.depth,
|
||||||
|
label: data.label,
|
||||||
|
isCommonlyUsedByItems: data.is_commonly_used_by_items,
|
||||||
|
};
|
||||||
|
}
|
|
@ -29,20 +29,26 @@ class Item < ApplicationRecord
|
||||||
|
|
||||||
scope :with_closet_hangers, -> { joins(:closet_hangers) }
|
scope :with_closet_hangers, -> { joins(:closet_hangers) }
|
||||||
|
|
||||||
scope :name_includes, ->(value, locale = I18n.locale) {
|
scope :name_includes, ->(value) {
|
||||||
Item.where("name LIKE ?", "%" + sanitize_sql_like(value) + "%")
|
Item.where("name LIKE ?", "%" + sanitize_sql_like(value) + "%")
|
||||||
}
|
}
|
||||||
scope :name_excludes, ->(value, locale = I18n.locale) {
|
scope :name_excludes, ->(value) {
|
||||||
Item.where("name NOT LIKE ?", "%" + sanitize_sql_like(value) + "%")
|
Item.where("name NOT LIKE ?", "%" + sanitize_sql_like(value) + "%")
|
||||||
}
|
}
|
||||||
scope :is_nc, -> {
|
scope :is_nc, -> {
|
||||||
i = Item.arel_table
|
i = Item.arel_table
|
||||||
where(i[:rarity_index].in(Item::NCRarities).or(i[:is_manually_nc].eq(true)))
|
where(i[:rarity_index].in(Item::NCRarities).or(i[:is_manually_nc].eq(true)))
|
||||||
}
|
}
|
||||||
scope :is_np, -> {
|
scope :is_not_nc, -> {
|
||||||
i = Item.arel_table
|
i = Item.arel_table
|
||||||
where(i[:rarity_index].in(Item::NCRarities).or(i[:is_manually_nc].eq(true)).not)
|
where(i[:rarity_index].in(Item::NCRarities).or(i[:is_manually_nc].eq(true)).not)
|
||||||
}
|
}
|
||||||
|
scope :is_np, -> {
|
||||||
|
self.is_not_nc.is_not_pb
|
||||||
|
}
|
||||||
|
scope :is_not_np, -> {
|
||||||
|
self.merge Item.is_nc.or(Item.is_pb)
|
||||||
|
}
|
||||||
scope :is_pb, -> {
|
scope :is_pb, -> {
|
||||||
where('description LIKE ?',
|
where('description LIKE ?',
|
||||||
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
|
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
|
||||||
|
@ -81,8 +87,7 @@ class Item < ApplicationRecord
|
||||||
where("NOT (#{condition})", *zone_ids)
|
where("NOT (#{condition})", *zone_ids)
|
||||||
}
|
}
|
||||||
scope :fits, ->(body_id) {
|
scope :fits, ->(body_id) {
|
||||||
sa = SwfAsset.arel_table
|
joins(:swf_assets).where(swf_assets: {body_id: [body_id, 0]}).distinct
|
||||||
joins(:swf_assets).where(sa[:body_id].eq(body_id)).distinct
|
|
||||||
}
|
}
|
||||||
scope :not_fits, ->(body_id) {
|
scope :not_fits, ->(body_id) {
|
||||||
i = Item.arel_table
|
i = Item.arel_table
|
||||||
|
@ -445,8 +450,23 @@ class Item < ApplicationRecord
|
||||||
@parent_swf_asset_relationships_to_update = rels
|
@parent_swf_asset_relationships_to_update = rels
|
||||||
end
|
end
|
||||||
|
|
||||||
Appearance = Struct.new(:body, :swf_assets)
|
# NOTE: Adding the JSON serializer makes `as_json` treat this like a model
|
||||||
Appearance::Body = Struct.new(:id, :species)
|
# instead of like a hash, so you can target its children with things like
|
||||||
|
# the `include` option. This feels clunky though, I wish I had something a
|
||||||
|
# bit more suited to it!
|
||||||
|
Appearance = Struct.new(:body, :swf_assets) do
|
||||||
|
include ActiveModel::Serializers::JSON
|
||||||
|
def attributes
|
||||||
|
{body: body, swf_assets: swf_assets}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Appearance::Body = Struct.new(:id, :species) do
|
||||||
|
include ActiveModel::Serializers::JSON
|
||||||
|
def attributes
|
||||||
|
{id: id, species: species}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def appearances
|
def appearances
|
||||||
all_swf_assets = swf_assets.to_a
|
all_swf_assets = swf_assets.to_a
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,6 @@ class Item
|
||||||
@text || @filters.map(&:to_s).join(' ')
|
@text || @filters.map(&:to_s).join(' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.locale
|
|
||||||
(I18n.fallbacks[I18n.locale] &
|
|
||||||
I18n.locales_with_neopets_language_code).first
|
|
||||||
end
|
|
||||||
|
|
||||||
TEXT_FILTER_EXPR = /([+-]?)(?:(\p{Word}+):)?(?:"([^"]+)"|(\S+))/
|
TEXT_FILTER_EXPR = /([+-]?)(?:(\p{Word}+):)?(?:"([^"]+)"|(\S+))/
|
||||||
def self.from_text(text, user=nil)
|
def self.from_text(text, user=nil)
|
||||||
filters = []
|
filters = []
|
||||||
|
@ -35,8 +30,8 @@ class Item
|
||||||
case key
|
case key
|
||||||
when 'name'
|
when 'name'
|
||||||
filters << (is_positive ?
|
filters << (is_positive ?
|
||||||
Filter.name_includes(value, locale) :
|
Filter.name_includes(value) :
|
||||||
Filter.name_excludes(value, locale))
|
Filter.name_excludes(value))
|
||||||
when 'occupies'
|
when 'occupies'
|
||||||
filters << (is_positive ?
|
filters << (is_positive ?
|
||||||
Filter.occupies(value) :
|
Filter.occupies(value) :
|
||||||
|
@ -46,10 +41,11 @@ class Item
|
||||||
Filter.restricts(value) :
|
Filter.restricts(value) :
|
||||||
Filter.not_restricts(value))
|
Filter.not_restricts(value))
|
||||||
when 'fits'
|
when 'fits'
|
||||||
pet_type = load_pet_type_by_name(value)
|
color_name, species_name = value.split("-")
|
||||||
|
pet_type = load_pet_type_by_name(color_name, species_name)
|
||||||
filters << (is_positive ?
|
filters << (is_positive ?
|
||||||
Filter.fits(pet_type.body_id, value.downcase) :
|
Filter.fits(pet_type.body_id, color_name, species_name) :
|
||||||
Filter.not_fits(pet_type.body_id, value.downcase))
|
Filter.not_fits(pet_type.body_id, color_name, species_name))
|
||||||
when 'species'
|
when 'species'
|
||||||
begin
|
begin
|
||||||
species = Species.find_by_name!(value)
|
species = Species.find_by_name!(value)
|
||||||
|
@ -116,27 +112,30 @@ class Item
|
||||||
case filter_params[:key]
|
case filter_params[:key]
|
||||||
when 'name'
|
when 'name'
|
||||||
filters << (is_positive ?
|
filters << (is_positive ?
|
||||||
Filter.name_includes(value, locale) :
|
Filter.name_includes(value) :
|
||||||
Filter.name_excludes(value, locale))
|
Filter.name_excludes(value))
|
||||||
when 'is_nc'
|
when 'is_nc'
|
||||||
filters << (is_positive ? Filter.is_nc : Filter.is_not_nc)
|
filters << (is_positive ? Filter.is_nc : Filter.is_not_nc)
|
||||||
|
when 'is_pb'
|
||||||
|
filters << (is_positive ? Filter.is_pb : Filter.is_not_pb)
|
||||||
|
when 'is_np'
|
||||||
|
filters << (is_positive ? Filter.is_np : Filter.is_not_np)
|
||||||
when 'occupied_zone_set_name'
|
when 'occupied_zone_set_name'
|
||||||
filters << (is_positive ?
|
filters << (is_positive ? Filter.occupies(value) :
|
||||||
Filter.occupies(value, locale) :
|
Filter.not_occupies(value))
|
||||||
Filter.not_occupies(value, locale))
|
|
||||||
when 'restricted_zone_set_name'
|
when 'restricted_zone_set_name'
|
||||||
filters << (is_positive ?
|
filters << (is_positive ?
|
||||||
Filter.restricts(value, locale) :
|
Filter.restricts(value) :
|
||||||
Filter.not_restricts(value, locale))
|
Filter.not_restricts(value))
|
||||||
when 'fits_pet_type'
|
when 'fits'
|
||||||
pet_type = PetType.find(value)
|
raise NotImplementedError if value[:alt_style_id].present?
|
||||||
color_name = pet_type.color.name
|
pet_type = load_pet_type_by_color_and_species(
|
||||||
species_name = pet_type.species.name
|
value[:color_id], value[:species_id])
|
||||||
# NOTE: Some color syntaxes are weird, like `fits:"polka dot-aisha"`!
|
color = Color.find value[:color_id]
|
||||||
value = "#{color_name}-#{species_name}"
|
species = Species.find value[:species_id]
|
||||||
filters << (is_positive ?
|
filters << (is_positive ?
|
||||||
Filter.fits(pet_type.body_id, value) :
|
Filter.fits(pet_type.body_id, color.name, species.name) :
|
||||||
Filter.not_fits(pet_type.body_id, value))
|
Filter.not_fits(pet_type.body_id, color.name, species.name))
|
||||||
when 'user_closet_hanger_ownership'
|
when 'user_closet_hanger_ownership'
|
||||||
case value
|
case value
|
||||||
when 'true'
|
when 'true'
|
||||||
|
@ -156,9 +155,7 @@ class Item
|
||||||
self.new(filters, user)
|
self.new(filters, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.load_pet_type_by_name(pet_type_string)
|
def self.load_pet_type_by_name(color_name, species_name)
|
||||||
color_name, species_name = pet_type_string.split("-")
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
PetType.matching_name(color_name, species_name).first!
|
PetType.matching_name(color_name, species_name).first!
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
@ -167,6 +164,18 @@ class Item
|
||||||
raise Item::Search::Error, message
|
raise Item::Search::Error, message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.load_pet_type_by_color_and_species(color_id, species_id)
|
||||||
|
begin
|
||||||
|
PetType.where(color_id: color_id, species_id: species_id).first!
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
color_name = Color.find(color_id).name rescue "Color #{color_id}"
|
||||||
|
species_name = Species.find(species_id).name rescue "Species #{species_id}"
|
||||||
|
message = I18n.translate('items.search.errors.not_found.pet_type',
|
||||||
|
name1: color_name.capitalize, name2: species_name.capitalize)
|
||||||
|
raise Item::Search::Error, message
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Error < Exception
|
class Error < Exception
|
||||||
|
@ -194,12 +203,12 @@ class Item
|
||||||
"#<#{self.class.name} #{@text.inspect}>"
|
"#<#{self.class.name} #{@text.inspect}>"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.name_includes(value, locale)
|
def self.name_includes(value)
|
||||||
self.new Item.name_includes(value, locale), "#{q value}"
|
self.new Item.name_includes(value), "#{q value}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.name_excludes(value, locale)
|
def self.name_excludes(value)
|
||||||
self.new Item.name_excludes(value, locale), "-#{q value}"
|
self.new Item.name_excludes(value), "-#{q value}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.occupies(value)
|
def self.occupies(value)
|
||||||
|
@ -218,11 +227,15 @@ class Item
|
||||||
self.new Item.not_restricts(value), "-restricts:#{q value}"
|
self.new Item.not_restricts(value), "-restricts:#{q value}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.fits(body_id, value)
|
def self.fits(body_id, color_name, species_name)
|
||||||
|
# NOTE: Some color syntaxes are weird, like `fits:"polka dot-aisha"`!
|
||||||
|
value = "#{color_name}-#{species_name}".downcase
|
||||||
self.new Item.fits(body_id), "fits:#{q value}"
|
self.new Item.fits(body_id), "fits:#{q value}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.not_fits(body_id, value)
|
def self.not_fits(body_id, color_name, species_name)
|
||||||
|
# NOTE: Some color syntaxes are weird, like `fits:"polka dot-aisha"`!
|
||||||
|
value = "#{color_name}-#{species_name}".downcase
|
||||||
self.new Item.not_fits(body_id), "-fits:#{q value}"
|
self.new Item.not_fits(body_id), "-fits:#{q value}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -255,7 +268,7 @@ class Item
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.is_not_nc
|
def self.is_not_nc
|
||||||
self.new Item.is_np, '-is:nc'
|
self.new Item.is_not_nc, '-is:nc'
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.is_np
|
def self.is_np
|
||||||
|
@ -263,7 +276,7 @@ class Item
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.is_not_np
|
def self.is_not_np
|
||||||
self.new Item.is_nc, '-is:np'
|
self.new Item.is_not_np, '-is:np'
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.is_pb
|
def self.is_pb
|
||||||
|
|
|
@ -169,16 +169,30 @@ class PetType < ApplicationRecord
|
||||||
}.first
|
}.first
|
||||||
end
|
end
|
||||||
|
|
||||||
def appearances_for(item_ids)
|
def appearances_for(item_ids, swf_asset_includes: [])
|
||||||
# First, load all the relationships for these items that also fit this
|
# First, load all the relationships for these items that also fit this
|
||||||
# body.
|
# body.
|
||||||
relationships = ParentSwfAssetRelationship.includes(:swf_asset).
|
relationships = ParentSwfAssetRelationship.
|
||||||
|
includes(swf_asset: swf_asset_includes).
|
||||||
where(parent_type: "Item", parent_id: item_ids).
|
where(parent_type: "Item", parent_id: item_ids).
|
||||||
where(swf_asset: {body_id: [body_id, 0]})
|
where(swf_asset: {body_id: [body_id, 0]})
|
||||||
|
|
||||||
|
pet_type_body = Item::Appearance::Body.new(body_id, species)
|
||||||
|
all_pets_body = Item::Appearance::Body.new(0, nil)
|
||||||
|
|
||||||
# Then, convert this into a hash from item ID to SWF assets.
|
# Then, convert this into a hash from item ID to SWF assets.
|
||||||
assets_by_item_id = relationships.group_by(&:parent_id).
|
assets_by_item_id = relationships.group_by(&:parent_id).
|
||||||
transform_values { |rels| rels.map(&:swf_asset) }
|
transform_values { |rels| rels.map(&:swf_asset) }
|
||||||
|
|
||||||
|
# Finally, for each item, return an appearance—even if it's empty!
|
||||||
|
item_ids.to_h do |item_id|
|
||||||
|
assets = assets_by_item_id.fetch(item_id, [])
|
||||||
|
|
||||||
|
fits_all_pets = assets.present? && assets.all? { |a| a.body_id == 0 }
|
||||||
|
body = fits_all_pets ? all_pets_body : pet_type_body
|
||||||
|
|
||||||
|
[item_id, Item::Appearance.new(body, assets)]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.all_by_ids_or_children(ids, pet_states)
|
def self.all_by_ids_or_children(ids, pet_states)
|
||||||
|
|
|
@ -17,6 +17,14 @@ class Zone < ActiveRecord::Base
|
||||||
@sometimes ? "#{label} sometimes" : label
|
@sometimes ? "#{label} sometimes" : label
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def is_commonly_used_by_items
|
||||||
|
# Zone metadata marks item zones with types 2, 3, and 4. But also, in
|
||||||
|
# practice, the Biology Effects zone (type 1, ID 4) has been used for a few
|
||||||
|
# items too. So, that's what we return true for!
|
||||||
|
# TODO: It'd probably be better to make this a database field?
|
||||||
|
[2, 3, 4].include?(type_id) || id == 4
|
||||||
|
end
|
||||||
|
|
||||||
def self.plainify_label(label)
|
def self.plainify_label(label)
|
||||||
label.delete('\- /').parameterize
|
label.delete('\- /').parameterize
|
||||||
end
|
end
|
||||||
|
|
|
@ -110,4 +110,4 @@
|
||||||
|
|
||||||
- content_for :javascripts do
|
- content_for :javascripts do
|
||||||
= include_javascript_libraries :jquery20, :jquery_tmpl
|
= include_javascript_libraries :jquery20, :jquery_tmpl
|
||||||
= javascript_include_tag 'ajax_auth', 'lib/react', 'lib/jquery.timeago', 'outfits/new'
|
= javascript_include_tag 'ajax_auth', 'lib/jquery.timeago', 'outfits/new'
|
Loading…
Reference in a new issue