diff --git a/locale/en_US.ts b/locale/en_US.ts index 410d0ce1..59742f51 100644 --- a/locale/en_US.ts +++ b/locale/en_US.ts @@ -199,7 +199,7 @@ export default { }, list: { searching: "Searching", - emote_count: "{0} emotes", + emote_count: "{0} emote | {0} emotes", no_emotes_listed: "No emotes found", fetching_slowly: "Sorry, it seems this is taking a while", category: { @@ -211,9 +211,20 @@ export default { new: "New", }, filters: { + hint: "Search Filters", + zero_width: "Overlaying / Zero-Width", + animated: "Animated", case_sensitive: "Case Sensitive", exact_match: "Exact Match", ignore_tags: "Ignore Tags", + sorting: "Sorting", + sorting_ascending: "Ascending", + sorting_descending: "Descending", + aspect_ratio: "Aspect Ratio", + aspect_ratio_format: "Width to Height", + aspect_ratio_width: "Ratio Width", + aspect_ratio_height: "Ratio Height", + aspect_ratio_tolerance: "Ratio Tolerance %", }, }, context: { diff --git a/src/assets/gql/emotes/search.ts b/src/assets/gql/emotes/search.ts index 929c4e6b..50910f6e 100644 --- a/src/assets/gql/emotes/search.ts +++ b/src/assets/gql/emotes/search.ts @@ -2,8 +2,8 @@ import { Emote } from "@/structures/Emote"; import gql from "graphql-tag"; export const SearchEmotes = gql` - query SearchEmotes($query: String!, $page: Int, $limit: Int, $filter: EmoteSearchFilter) { - emotes(query: $query, page: $page, limit: $limit, filter: $filter) { + query SearchEmotes($query: String!, $page: Int, $sort: Sort, $limit: Int, $filter: EmoteSearchFilter) { + emotes(query: $query, page: $page, sort: $sort, limit: $limit, filter: $filter) { count items { id diff --git a/src/assets/scss/emotes.scss b/src/assets/scss/emotes.scss index 50186c23..570e5907 100644 --- a/src/assets/scss/emotes.scss +++ b/src/assets/scss/emotes.scss @@ -40,22 +40,59 @@ $headingDistance: 8.5em; align-items: center; > .search-filters-button { + cursor: pointer; position: relative; right: 2em; - cursor: pointer; + + filter: drop-shadow(0 0 0.5em currentColor); + &:hover { + filter: drop-shadow(0 0 0.35em currentColor) brightness(1.5); + } } > .search-filters { position: absolute; + display: grid; + justify-content: start; top: 3em; - width: 9em; + width: 12em; border-radius: 0.25em; + border: 0.01em solid currentColor; > div { display: grid; - row-gap: 1em; + row-gap: 1.25em; padding: 0.5em; font-size: 0.85rem; } + + .sort { + display: grid; + row-gap: 0.5em; + border-top: 0.01em solid currentColor; + padding-top: 0.33em; + + > p:nth-child(1) { + font-weight: bold; + } + } + + .search-ratio { + border-top: 0.01em solid currentColor; + padding-top: 0.33em; + + > p:nth-child(1) { + font-weight: bold; + } + > p:nth-child(2) { + font-size: 0.65rem; + margin-bottom: 1em; + } + + > .ratio-inputs { + display: grid; + row-gap: 1em; + } + } } } } diff --git a/src/components/form/Dropdown.vue b/src/components/form/Dropdown.vue index fe1b82c4..63e1db5d 100644 --- a/src/components/form/Dropdown.vue +++ b/src/components/form/Dropdown.vue @@ -101,7 +101,6 @@ interface OptionData { @import "@scss/themes.scss"; .dropdown { width: v-bind(width); - z-index: 50; @include themify() { > div.dropdown-selected { @@ -118,18 +117,19 @@ interface OptionData { } > div.options { - background-color: lighten(themed("backgroundColor"), 4); + background-color: darken(themed("backgroundColor"), 4); > option:hover { - background-color: darken(themed("backgroundColor"), 4); + background-color: darken(themed("backgroundColor"), 6); } > option.selected { - background-color: darken(themed("backgroundColor"), 4); + background-color: darken(themed("backgroundColor"), 8); } } } > div.dropdown-selected { + z-index: 50; cursor: pointer; padding: 0.5em; border-radius: 0.25em; diff --git a/src/views/EmoteList/EmoteList.vue b/src/views/EmoteList/EmoteList.vue index 73c3419e..6ce11145 100644 --- a/src/views/EmoteList/EmoteList.vue +++ b/src/views/EmoteList/EmoteList.vue @@ -23,11 +23,27 @@ -
- +
+
+ + + + +
+

{{ t("emote.list.filters.sorting") }}

+ + + + +
+ +
+

{{ t("emote.list.filters.aspect_ratio") }}

+

{{ t("emote.list.filters.aspect_ratio_format") }}

+
+ + + +
+
@@ -63,7 +121,7 @@
- {{ t("emote.list.emote_count", [itemCount]) }} + {{ t("emote.list.emote_count", [itemCount], { plural: itemCount }) }}
@@ -124,6 +182,7 @@ import Icon from "@/components/utility/Icon.vue"; import Checkbox from "@/components/form/Checkbox.vue"; import EmoteCardList from "@/components/utility/EmoteCardList.vue"; import { useSizedRows } from "@/composable/calculate-sized-rows"; +import Dropdown from "@/components/form/Dropdown.vue"; const { t } = useI18n(); @@ -144,15 +203,73 @@ const { update: updateSizing } = useSizedRows([128, 160]); const getSizedRows = (): number => (emotelist.value ? updateSizing(emotelist.value).sum : 0); const category = ref((route.query.category as string)?.toLowerCase() ?? "TOP"); + +// Aspect Ratio Search +const aspectRatio = reactive({ + used: false, + width: 1, + height: 1, + tolerance: 10, +}); +if (route.query.aspect_ratio) { + const [width, height, tolerance] = (route.query.aspect_ratio as string).split(":"); + aspectRatio.width = Number(width); + aspectRatio.height = Number(height); + aspectRatio.tolerance = Number(tolerance); +} + +const computedRatio = computed(() => + aspectRatio.used ? `${aspectRatio.width}:${aspectRatio.height}:${aspectRatio.tolerance}` : "", +); + +watch(aspectRatio, (v) => { + if (v.width === 1 && v.height === 0 && v.tolerance === 0) { + aspectRatio.used = false; + return; + } + + v.used = true; + + queryVariables.filter.aspect_ratio = computedRatio.value; +}); + +// Sort +const sort = reactive({ + used: false, + value: "popularity", + order: "DESCENDING", +}); +if (route.query.sort) { + const [name, order] = (route.query.sort as string).split(":"); + + sort.value = name; + sort.order = order?.toUpperCase(); +} + +const computedSort = computed(() => (sort.used ? `${sort.value}:${sort.order.toLowerCase()}` : "")); + +watch(sort, (v) => { + v.used = v.value === "popularity" && v.order === "DESCENDING"; + + queryVariables.sort = { value: v.value, order: v.order }; +}); + const queryVariables = reactive({ query: initQuery, limit: Math.max(1, getSizedRows()), page: initPage, + sort: { + value: sort.value, + order: sort.order, + }, filter: { category: category.value.toUpperCase(), exact_match: initFilter.includes("exact_match"), case_sensitive: initFilter.includes("case_sensitive"), ignore_tags: initFilter.includes("ignore_tags"), + zero_width: initFilter.includes("zero_width"), + animated: initFilter.includes("animated"), + aspect_ratio: computedRatio.value, }, }); @@ -319,14 +436,20 @@ watch(queryVariables, (_, old) => { } const filter = Object.keys(queryVariables.filter) - .filter((k) => k !== "category" && queryVariables.filter[k as keyof typeof queryVariables.filter]) + .filter( + (k) => + !["category", "aspect_ratio"].includes(k) && + queryVariables.filter[k as keyof typeof queryVariables.filter], + ) .join(","); router[act]({ query: { page: queryVariables.page, query: queryVariables.query || undefined, + sort: computedSort.value || undefined, category: category.value !== "TOP" ? category.value.toLowerCase() : undefined, + aspect_ratio: computedRatio.value || undefined, filter: filter.length > 0 ? filter : undefined, }, });