mirror of
https://github.com/NoCrypt/migu.git
synced 2026-03-17 00:06:21 +00:00
feat: tags and multiple genres
This commit is contained in:
parent
f22b497e03
commit
13fa10f475
3 changed files with 209 additions and 45 deletions
|
|
@ -1,5 +1,7 @@
|
|||
<script context='module'>
|
||||
const badgeKeys = ['search', 'genre', 'season', 'year', 'format', 'status', 'sort']
|
||||
const badgeKeys = ['title', 'search', 'genre', 'tag', 'season', 'year', 'format', 'status', 'sort']
|
||||
const badgeDisplayNames = { title: 'List:', search: 'Title:', genre: 'Genre:', tag: 'Tag:', season: 'Season:', year: 'Year:', format: 'Format:', status: 'Status:', sort: 'Sort:'}
|
||||
const sortOptions = { START_DATE_DESC: 'Release Date', SCORE_DESC: 'Score', POPULARITY_DESC: 'Popularity', TRENDING_DESC: 'Trending', UPDATED_TIME_DESC: 'Last Updated', STARTED_ON_DESC: 'Started On', FINISHED_ON_DESC: 'Finished On', PROGRESS_DESC: 'Your Progress', USER_SCORE_DESC: 'Your Score' }
|
||||
|
||||
export function searchCleanup (search) {
|
||||
return Object.fromEntries(Object.entries(search).map((entry) => {
|
||||
|
|
@ -16,39 +18,144 @@
|
|||
import { toast } from 'svelte-sonner'
|
||||
|
||||
export let search
|
||||
let searchTextInput
|
||||
let searchTextInput = {
|
||||
title: null,
|
||||
genre: null,
|
||||
tag: null
|
||||
}
|
||||
let form
|
||||
|
||||
$: sanitisedSearch = Object.values(searchCleanup(search))
|
||||
const tagList = [
|
||||
'Boys\' Love',
|
||||
'Demons',
|
||||
'Yuri',
|
||||
'Food',
|
||||
'Isekai',
|
||||
'Iyashikei',
|
||||
'Josei',
|
||||
'Magic',
|
||||
'Martial Arts',
|
||||
'Military',
|
||||
'Parody',
|
||||
'Female Harem',
|
||||
'Male Harem',
|
||||
'Mixed Gender Harem',
|
||||
'Parody',
|
||||
'School',
|
||||
'Seinen',
|
||||
'Shoujo',
|
||||
'Shounen',
|
||||
'Space',
|
||||
'Super Power',
|
||||
'Vampire',
|
||||
'Kids'
|
||||
]
|
||||
|
||||
function searchClear () {
|
||||
const genreList = [
|
||||
'Action',
|
||||
'Adventure',
|
||||
'Comedy',
|
||||
'Drama',
|
||||
'Ecchi',
|
||||
'Fantasy',
|
||||
'Horror',
|
||||
'Mahou Shoujo',
|
||||
'Mecha',
|
||||
'Music',
|
||||
'Mystery',
|
||||
'Psychological',
|
||||
'Romance',
|
||||
'Sci-Fi',
|
||||
'Slice of Life',
|
||||
'Sports',
|
||||
'Supernatural',
|
||||
'Thriller'
|
||||
]
|
||||
|
||||
$: sanitisedSearch = Object.entries(searchCleanup(search)).flatMap(
|
||||
([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => ({ key, value: item }))
|
||||
} else {
|
||||
return [{ key, value }]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function searchClear() {
|
||||
search = {
|
||||
title: '',
|
||||
search: '',
|
||||
genre: '',
|
||||
tag: '',
|
||||
season: '',
|
||||
year: null,
|
||||
format: '',
|
||||
status: '',
|
||||
sort: ''
|
||||
}
|
||||
searchTextInput.focus()
|
||||
searchTextInput.title.focus()
|
||||
form.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
$page = 'search'
|
||||
}
|
||||
|
||||
function handleFile ({ target }) {
|
||||
function getSortDisplayName(value) {
|
||||
return sortOptions[value] || value
|
||||
}
|
||||
|
||||
function getBadgeDisplayName(key) {
|
||||
return badgeDisplayNames[key] || ''
|
||||
}
|
||||
|
||||
function removeBadge(badge) {
|
||||
if (badge.key === 'title') {
|
||||
delete search.load
|
||||
delete search.disableHide
|
||||
delete search.userList
|
||||
delete search.continueWatching
|
||||
delete search.completedList
|
||||
search.sort = ''
|
||||
} else if (badge.key === 'genre' || badge.key === 'tag') {
|
||||
delete search.title
|
||||
}
|
||||
if (Array.isArray(search[badge.key])) {
|
||||
search[badge.key] = search[badge.key].filter(
|
||||
(item) => item !== badge.value
|
||||
)
|
||||
if (search[badge.key].length === 0) {
|
||||
search[badge.key] = ''
|
||||
}
|
||||
} else {
|
||||
search[badge.key] = ''
|
||||
}
|
||||
form.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
|
||||
function filterList(event, type) {
|
||||
const list = type === 'tag' ? tagList : genreList
|
||||
const searchKey = type === 'tag' ? 'tag' : 'genre'
|
||||
const selectedValue = event.target.value
|
||||
if (list.includes(selectedValue) && (!search[searchKey] || !search[searchKey].includes(selectedValue))) {
|
||||
search[searchKey] = search[searchKey] ? [...search[searchKey], selectedValue] : [selectedValue]
|
||||
searchTextInput[searchKey] = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleFile({ target }) {
|
||||
const { files } = target
|
||||
if (files?.[0]) {
|
||||
toast.promise(traceAnime(files[0]), {
|
||||
description: 'You can also paste an URL to an image.',
|
||||
loading: 'Looking up anime for image...',
|
||||
success: 'Found anime for image!',
|
||||
error: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.'
|
||||
error:
|
||||
'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.'
|
||||
})
|
||||
target.value = null
|
||||
}
|
||||
}
|
||||
function changeCardMode (type) {
|
||||
|
||||
function changeCardMode(type) {
|
||||
$settings.cards = type
|
||||
form.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
|
|
@ -66,44 +173,61 @@
|
|||
<span class='input-group-text d-flex material-symbols-outlined bg-dark-light pr-0 font-size-18'>search</span>
|
||||
</div>
|
||||
<input
|
||||
bind:this={searchTextInput}
|
||||
bind:this={searchTextInput.title}
|
||||
type='search'
|
||||
class='form-control bg-dark-light border-left-0 text-capitalize'
|
||||
autocomplete='off'
|
||||
bind:value={search.search}
|
||||
data-option='search'
|
||||
disabled={search.disableSearch}
|
||||
placeholder='Any' />
|
||||
placeholder='Any'/>
|
||||
</div>
|
||||
</div>
|
||||
<div class='col-lg col-4 p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
<div class='material-symbols-outlined mr-10 font-size-30'>theater_comedy</div>
|
||||
Genre
|
||||
Genres
|
||||
</div>
|
||||
<div class='input-group'>
|
||||
<select class='form-control bg-dark-light' required bind:value={search.genre} disabled={search.disableSearch}>
|
||||
<option value selected disabled hidden>Any</option>
|
||||
<option value='Action'>Action</option>
|
||||
<option value='Adventure'>Adventure</option>
|
||||
<option value='Comedy'>Comedy</option>
|
||||
<option value='Drama'>Drama</option>
|
||||
<option value='Ecchi'>Ecchi</option>
|
||||
<option value='Fantasy'>Fantasy</option>
|
||||
<option value='Horror'>Horror</option>
|
||||
<option value='Mahou Shoujo'>Mahou Shoujo</option>
|
||||
<option value='Mecha'>Mecha</option>
|
||||
<option value='Music'>Music</option>
|
||||
<option value='Mystery'>Mystery</option>
|
||||
<option value='Psychological'>Psychological</option>
|
||||
<option value='Romance'>Romance</option>
|
||||
<option value='Sci-Fi'>Sci-Fi</option>
|
||||
<option value='Slice of Life'>Slice of Life</option>
|
||||
<option value='Sports'>Sports</option>
|
||||
<option value='Supernatural'>Supernatural</option>
|
||||
<option value='Thriller'>Thriller</option>
|
||||
</select>
|
||||
<input
|
||||
type='search'
|
||||
class='form-control bg-dark-light border-left-0 text-capitalize'
|
||||
autocomplete='off'
|
||||
bind:value={searchTextInput.genre}
|
||||
on:change={(event) => filterList(event, 'genre')}
|
||||
data-option='search'
|
||||
disabled={search.disableSearch}
|
||||
placeholder='Any'
|
||||
list='search-genre'/>
|
||||
</div>
|
||||
<datalist id='search-genre'>
|
||||
{#each genreList as genre}
|
||||
<option>{genre}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class='col-lg col-4 p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
<div class='material-symbols-outlined mr-10 font-size-30'>tag</div>
|
||||
Tags
|
||||
</div>
|
||||
<div class='input-group'>
|
||||
<input
|
||||
type='search'
|
||||
class='form-control bg-dark-light border-left-0 text-capitalize'
|
||||
autocomplete='off'
|
||||
bind:value={searchTextInput.tag}
|
||||
on:change={(event) => filterList(event, 'tag')}
|
||||
data-option='search'
|
||||
disabled={search.disableSearch}
|
||||
placeholder='Any'
|
||||
list='search-tag'/>
|
||||
</div>
|
||||
<datalist id='search-tag'>
|
||||
{#each tagList as tag}
|
||||
<option>{tag}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class='col-lg col-4 p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
|
|
@ -151,9 +275,9 @@
|
|||
<div class='input-group'>
|
||||
<select class='form-control bg-dark-light' required bind:value={search.status} disabled={search.disableSearch}>
|
||||
<option value selected disabled hidden>Any</option>
|
||||
<option value='RELEASING'>Airing</option>
|
||||
<option value='RELEASING'>Releasing</option>
|
||||
<option value='FINISHED'>Finished</option>
|
||||
<option value='NOT_YET_RELEASED'>Not Yet Aired</option>
|
||||
<option value='NOT_YET_RELEASED'>Not Yet Released</option>
|
||||
<option value='CANCELLED'>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -165,12 +289,21 @@
|
|||
</div>
|
||||
<div class='input-group'>
|
||||
<select class='form-control bg-dark-light' required bind:value={search.sort} disabled={search.disableSearch}>
|
||||
<option value selected disabled hidden>Name</option>
|
||||
<option value selected disabled hidden>Any</option>
|
||||
<option value='START_DATE_DESC'>Release Date</option>
|
||||
<option value='SCORE_DESC'>Score</option>
|
||||
<option value='POPULARITY_DESC'>Popularity</option>
|
||||
<option value='TRENDING_DESC'>Trending</option>
|
||||
<option value='UPDATED_TIME_DESC' disabled hidden>Updated Date</option>
|
||||
{#if search.userList}
|
||||
<option value='UPDATED_TIME_DESC'>Last Updated</option>
|
||||
<option value='STARTED_ON_DESC'>Started On</option>
|
||||
{#if search.completedList}
|
||||
<option value='FINISHED_ON_DESC'>Finished On</option>
|
||||
<option value='USER_SCORE_DESC'>Your Score</option>
|
||||
{:else}
|
||||
<option value='PROGRESS_DESC'>Your Progress</option>
|
||||
{/if}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -186,17 +319,28 @@
|
|||
</div>
|
||||
<div class='col-auto p-10 d-flex'>
|
||||
<div class='align-self-end'>
|
||||
<button class='btn btn-square bg-dark-light material-symbols-outlined font-size-18 px-5 align-self-end border-0' type='button' use:click={searchClear} class:text-primary={!!sanitisedSearch?.length || search.disableSearch || search.clearNext}>
|
||||
delete
|
||||
<button class='btn btn-square bg-dark-light material-symbols-outlined font-size-18 px-5 align-self-end border-0' type='button' use:click={searchClear} disabled={sanitisedSearch.length <= 0} class:text-danger={!!sanitisedSearch?.length || search.disableSearch || search.clearNext}>
|
||||
{!!sanitisedSearch?.length || search.disableSearch || search.clearNext ? 'filter_alt_off' : 'filter_alt'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='w-full px-10 pt-10 h-50 d-flex flex-colum align-items-center'>
|
||||
{#if sanitisedSearch?.length}
|
||||
<span class='material-symbols-outlined font-size-24 mr-20 filled text-dark-light'>sell</span>
|
||||
{#each sanitisedSearch as badge}
|
||||
<span class='badge bg-light border-0 py-5 px-10 text-capitalize mr-20 text-white text-nowrap'>{('' + badge).replace(/_/g, ' ').toLowerCase()}</span>
|
||||
<span
|
||||
class='material-symbols-outlined font-size-24 mr-20 filled text-dark-light'
|
||||
>sell</span>
|
||||
{#each badgeKeys as key}
|
||||
{#each sanitisedSearch as badge}
|
||||
{#if badge.key === key}
|
||||
{#if (search.userList || badge.key !== 'title') }
|
||||
<span class='badge bg-light border-0 py-5 px-10 text-capitalize mr-20 text-white text-nowrap'>
|
||||
{badge.key === 'sort' ? 'Sort: ' : getBadgeDisplayName(badge.key)} {badge.key === 'sort' ? getSortDisplayName(badge.value) : ('' + badge.value).replace(/_/g, ' ').toLowerCase()}
|
||||
<button on:click={() => removeBadge(badge)} class='badge-remove-btn'>x</button>
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
<span class='material-symbols-outlined font-size-24 mr-10 filled ml-auto text-dark-light pointer' class:text-muted={$settings.cards === 'small'} use:click={() => changeCardMode('small')}>grid_on</span>
|
||||
|
|
@ -229,4 +373,16 @@
|
|||
.font-size-30 {
|
||||
font-size: 3rem !important;
|
||||
}
|
||||
|
||||
.badge-remove-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.badge-remove-btn:hover {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
4
common/modules/al.d.ts
vendored
4
common/modules/al.d.ts
vendored
|
|
@ -15,6 +15,10 @@ export type Media = {
|
|||
duration?: number
|
||||
averageScore?: number
|
||||
genres?: string[]
|
||||
tags?: {
|
||||
name: string
|
||||
rank: integer
|
||||
}[]
|
||||
isFavourite: boolean
|
||||
coverImage?: {
|
||||
extraLarge: string
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ episodes,
|
|||
duration,
|
||||
averageScore,
|
||||
genres,
|
||||
tags {
|
||||
name,
|
||||
rank
|
||||
},
|
||||
isFavourite,
|
||||
coverImage{
|
||||
extraLarge,
|
||||
|
|
@ -417,12 +421,12 @@ class AnilistClient {
|
|||
|
||||
async searchIDS (variables) {
|
||||
const query = /* js */`
|
||||
query($id: [Int], $page: Int, $perPage: Int, $status: [MediaStatus], $onList: Boolean, $sort: [MediaSort], $search: String, $season: MediaSeason, $year: Int, $genre: String, $format: MediaFormat){
|
||||
query($id: [Int], $page: Int, $perPage: Int, $status: [MediaStatus], $onList: Boolean, $sort: [MediaSort], $search: String, $season: MediaSeason, $year: Int, $genre: [String], $tag: [String], $format: MediaFormat){
|
||||
Page(page: $page, perPage: $perPage){
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
},
|
||||
media(id_in: $id, type: ANIME, status_in: $status, onList: $onList, search: $search, sort: $sort, season: $season, seasonYear: $year, genre: $genre, format: $format){
|
||||
media(id_in: $id, type: ANIME, status_in: $status, onList: $onList, search: $search, sort: $sort, season: $season, seasonYear: $year, genre_in: $genre, tag_in: $tag, format: $format){
|
||||
${queryObjects}
|
||||
}
|
||||
}
|
||||
|
|
@ -544,12 +548,12 @@ class AnilistClient {
|
|||
async search (variables = {}) {
|
||||
variables.sort ||= 'SEARCH_MATCH'
|
||||
const query = /* js */`
|
||||
query($page: Int, $perPage: Int, $sort: [MediaSort], $search: String, $onList: Boolean, $status: MediaStatus, $season: MediaSeason, $year: Int, $genre: String, $format: MediaFormat){
|
||||
query($page: Int, $perPage: Int, $sort: [MediaSort], $search: String, $onList: Boolean, $status: MediaStatus, $season: MediaSeason, $year: Int, $genre: [String], $tag: [String], $format: MediaFormat, $id_not: [Int]){
|
||||
Page(page: $page, perPage: $perPage){
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
},
|
||||
media(type: ANIME, search: $search, sort: $sort, onList: $onList, status: $status, season: $season, seasonYear: $year, genre: $genre, format: $format, format_not: MUSIC){
|
||||
media(id_not_in: $id_not, type: ANIME, search: $search, sort: $sort, onList: $onList, status: $status, season: $season, seasonYear: $year, genre_in: $genre, tag_in: $tag, format: $format, format_not: MUSIC){
|
||||
${queryObjects}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue