Skip to content

Commit

Permalink
- Improve error handling for API calls by making everything call AniT…
Browse files Browse the repository at this point in the history
…ools.ts' fetch function

- Handle request abortions caused by user interations
- BetterList: send DataTables requests via POST instead of GET
- Mapper: Users can now get a list of past votes and are able to revoke them if they want to
- Mapper: Button for manually loading MU entries should be disabled while loading suggestions
  • Loading branch information
Koopzington committed Jun 6, 2024
1 parent 1228e96 commit ee0ea77
Show file tree
Hide file tree
Showing 7 changed files with 987 additions and 651 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 2024-06-06
- Improved handling of errors and abortion of running requests caused by user interactions
- Mapper: Users can now get a list of past votes and are able to revoke them if they want to
- Mapper: Load button for manual loading of MU entries is now disabled while loading suggestions

## 2024-06-04
- Updated dependencies
- vite from 5.2.11 to 5.2.12
Expand Down
20 changes: 18 additions & 2 deletions assets/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,15 @@ img[src="null"] {
font-size: 1.5rem;
}

.mapper-content img {
.mapper-content img,
#my-votes img {
min-width: 150px;
max-height: 233px;
}

@media (width <= 768px) {
.mapper-content img {
.mapper-content img,
#my-votes img {
min-width: 125px;
max-height: 166px;
}
Expand All @@ -344,6 +346,20 @@ img[src="null"] {
padding: 0.5rem;
}

#my-votes.dt-table td {
white-space: wrap;
}

#my-votes.dt-table td.media-col {
text-align: left;
}

#my-votes.dt-table td.media-col a {
display: flex;
align-items: center;
gap: 1rem;
}

.loader {
margin: 0 auto;
margin-top: 30px;
Expand Down
37 changes: 29 additions & 8 deletions src/AniTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ class AniTools {

// Basic stuff that needs to run on page load
const settings = new Settings()
const filters = new Filters(settings)
const filters = new Filters(this, settings)
const columns = new Columns()
this.Tools.BetterList = new BetterList(settings, filters, columns, this.AniList)
this.Tools.BetterList = new BetterList(this, settings, filters, columns, this.AniList)
this.Tools.Mapper = new Mapper(this, filters)

settings.initSettings()
Expand Down Expand Up @@ -171,7 +171,7 @@ class AniTools {
return localStorage.getItem('al-access-token') !== null
}

public readonly fetch = async (url: string, init: RequestInit | undefined = undefined) => {
public readonly fetch = async (url: string, init: RequestInit | undefined = undefined): Promise<Response> => {
const accessToken = localStorage.getItem('al-access-token');
if (accessToken !== null) {
if (init === undefined) {
Expand All @@ -192,20 +192,41 @@ class AniTools {
}
}

return await fetch(import.meta.env.VITE_API_URL + url, init)
try {
return await fetch(import.meta.env.VITE_API_URL + url, init)
} catch (error) {
this.errorHandler(error)
throw error
}
}

public readonly alert = (msg: string, type: string = '') => {
private readonly errorHandler = (error): void => {
// Ignore AbortErrors, they're caused by the user when clicking things too fast
if (error.name === 'AbortError') {
return
}

console.error(error)
this.alert(
'There seems to be a problem with the AniTools Backend. Please contact the dev.',
'danger',
false
)
}

public readonly alert = (msg: string, type: string = '', autoremove: boolean = true) => {
let alert = document.createElement('div')
alert.innerHTML = this.alertTemplate
if (type.length > 0) {
alert.querySelector('.alert')!.classList.add('alert-' + type)
}
alert.querySelector('.message')!.innerHTML = msg
document.querySelector('#alert-container')?.insertAdjacentElement('beforeend', alert)
window.setTimeout(() => {
alert.remove()
}, 10000)
if (autoremove) {
window.setTimeout(() => {
alert.remove()
}, 10000)
}
}
}

Expand Down
184 changes: 107 additions & 77 deletions src/Filters.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AniTools from "./AniTools"
import Tagify from '@yaireo/tagify'
import noUiSlider from 'nouislider'
import wNumb from 'wnumb'
Expand All @@ -9,6 +10,7 @@ class Filters extends EventTarget {
private readonly filterContainer: HTMLDivElement = document!.querySelector('#filters')!
private readonly mediaTypeSelect: HTMLSelectElement = document!.querySelector('.media-type')!
private readonly userNameField: HTMLInputElement = document!.querySelector('#al-user')!
private readonly AniTools: AniTools
private readonly ATSettings: Settings
private curFilterValues: any
private readonly andOrSwitch: HTMLButtonElement = document.createElement('button')
Expand Down Expand Up @@ -370,12 +372,14 @@ class Filters extends EventTarget {
deathdayUntil: HTMLInputElement | undefined,
} = {}

private abortController: AbortController
// We cache tags on initialization so the user can switch between grouped and non-grouped mode on the filter
private tagCache: Object

constructor (settings: Settings) {
constructor (anitools: AniTools, settings: Settings) {
super()
settings.addEventListener('tag-grouping-updated', this.updateTagFilter)
this.AniTools = anitools
this.ATSettings = settings
this.andOrSwitch.classList.add('btn', 'btn-primary', 'and-or-switch')
this.andOrSwitch.innerText = 'AND'
Expand Down Expand Up @@ -446,23 +450,32 @@ class Filters extends EventTarget {

let lists: TagifyValue[] = [];
if (this.userNameField.value.length > 0 && this.filterMap[filterSet].includes('userList')) {
const response = await fetch(import.meta.env.VITE_API_URL + '/userLists?user_name=' + this.userNameField.value + '&media_type=' + this.mediaTypeSelect.value)
const data: UserList[] = await handleResponse(response);

data.forEach(function (list) {
lists.push({
label: list.name,
value: list.id,
customProperties: {
completion: Object.hasOwn(list, 'amount_completed')
? ' (' + list.amount_completed.toString() + '/' + list.amount_total.toString() + ') ' + Math.floor(list.amount_completed / list.amount_total * 100).toString() + '%'
: ' (' + list.amount_total.toString() + ')'
}
})
}, this)
this.abortController && this.abortController.abort()
this.abortController = new AbortController()

try {
const response = await this.AniTools.fetch(
'/userLists?user_name=' + this.userNameField.value + '&media_type=' + this.mediaTypeSelect.value,
{ signal: this.abortController.signal }
)

const data: UserList[] = await handleResponse(response);

data.forEach(function (list) {
lists.push({
label: list.name,
value: list.id,
customProperties: {
completion: Object.hasOwn(list, 'amount_completed')
? ' (' + list.amount_completed.toString() + '/' + list.amount_total.toString() + ') ' + Math.floor(list.amount_completed / list.amount_total * 100).toString() + '%'
: ' (' + list.amount_total.toString() + ')'
}
})
}, this)

// Reindex array and filter out empty lists
lists = [...lists].sort(undefined).filter(a => a)
// Reindex array and filter out empty lists
lists = [...lists].sort(undefined).filter(a => a)
} catch (error) {}
}

this.filterMap[filterSet].forEach((filterName: string) => {
Expand All @@ -489,7 +502,7 @@ class Filters extends EventTarget {
this.addRange(filterName, filterDef.label, filterDef.experimental ?? false)
break;
case 'userList':
if (this.userNameField.value.length === 0) {
if (this.userNameField.value.length === 0 || lists.length === 0) {
return
}

Expand Down Expand Up @@ -568,66 +581,83 @@ class Filters extends EventTarget {
filter.noUiSlider.updateOptions(options, false)
}

// Function for Tools to call upon unloading to stop any running requests
public readonly abort = (): void => {
this.abortController && this.abortController.abort()
}

// Function that updates the available options in the filters using the data the API returned
public readonly updateFilters = async (filterSet: string): Promise<void> => {
await this.insertFilters(filterSet)
await this.insertFilters(filterSet)

this.abortController && this.abortController.abort()
this.abortController = new AbortController()

let response: Response
try {
response = await this.AniTools.fetch(
'/filterValues?media_type=' + this.mediaTypeSelect.value,
{ signal: this.abortController.signal }
)
} catch (error) {
return
}

const response = await fetch(import.meta.env.VITE_API_URL + '/filterValues?media_type=' + this.mediaTypeSelect.value)
const filterValues = await response.json()
this.tagCache = filterValues.tags
if (this.filters.format !== undefined) {
this.filters.format.whitelist = filterValues.format.map((v) => { return {value: v, text: v}})
}
if (this.filters.genre !== undefined) {
this.filters.genre.whitelist = filterValues.genres.map((v) => { return {value: v, text: v}})
}
if (this.filters.country !== undefined) {
this.filters.country.whitelist = filterValues.country_of_origin.map((v) => { return {value: v, text: v}})
}
if (this.filters.externalLink !== undefined) {
this.filters.externalLink.whitelist = filterValues.external_links.map((v) => { return {value: v, text: v}})
}
if (this.filters.season !== undefined) {
this.filters.season.whitelist = filterValues.season.map((v) => { return {value: v, text: v}})
}
if (this.filters.year !== undefined) {
this.filters.year.whitelist = filterValues.season_year.map((v) => { return {value: v, text: v}})
}
if (this.filters.source !== undefined) {
this.filters.source.whitelist = filterValues.source.map((v) => { return {value: v, text: v}})
}
if (this.filters.airStatus !== undefined) {
this.filters.airStatus.whitelist = filterValues.status.map((v) => { return {value: v, text: v}})
}
if (this.filters.awcCommunityList !== undefined) {
this.filters.awcCommunityList.whitelist = filterValues.awc_community_lists.map((v) => { return {value: v, text: v}})
}
if (this.filters.tag !== undefined) {
this.updateTagFilter()
}
if (this.filters.tagPercentage !== undefined) {
this.updateRangeFilter(this.filters.tagPercentage, [0, 100])
}
if (this.filters.totalRuntime !== undefined) {
this.updateRangeFilter(this.filters.totalRuntime, filterValues.total_runtime)
}
if (this.filters.episodes !== undefined) {
this.updateRangeFilter(this.filters.episodes, filterValues.episodes)
}
if (this.filters.volumes !== undefined) {
this.updateRangeFilter(this.filters.volumes, filterValues.volumes)
}
if (this.filters.mcCount !== undefined) {
this.updateRangeFilter(this.filters.mcCount, filterValues.mcCount)
}
if (this.filters.bloodType !== undefined) {
this.filters.bloodType.whitelist = filterValues.blood_type.map((v) => { return {value: v, text: v}})
}
if (this.filters.gender !== undefined) {
this.filters.gender.whitelist = filterValues.gender.map((v) => { return {value: v, text: v}})
}

this.curFilterValues = this.getFilterParams()
const filterValues = await handleResponse(response)
this.tagCache = filterValues.tags
if (this.filters.format !== undefined) {
this.filters.format.whitelist = filterValues.format.map((v) => { return {value: v, text: v}})
}
if (this.filters.genre !== undefined) {
this.filters.genre.whitelist = filterValues.genres.map((v) => { return {value: v, text: v}})
}
if (this.filters.country !== undefined) {
this.filters.country.whitelist = filterValues.country_of_origin.map((v) => { return {value: v, text: v}})
}
if (this.filters.externalLink !== undefined) {
this.filters.externalLink.whitelist = filterValues.external_links.map((v) => { return {value: v, text: v}})
}
if (this.filters.season !== undefined) {
this.filters.season.whitelist = filterValues.season.map((v) => { return {value: v, text: v}})
}
if (this.filters.year !== undefined) {
this.filters.year.whitelist = filterValues.season_year.map((v) => { return {value: v, text: v}})
}
if (this.filters.source !== undefined) {
this.filters.source.whitelist = filterValues.source.map((v) => { return {value: v, text: v}})
}
if (this.filters.airStatus !== undefined) {
this.filters.airStatus.whitelist = filterValues.status.map((v) => { return {value: v, text: v}})
}
if (this.filters.awcCommunityList !== undefined) {
this.filters.awcCommunityList.whitelist = filterValues.awc_community_lists.map((v) => { return {value: v, text: v}})
}
if (this.filters.tag !== undefined) {
this.updateTagFilter()
}
if (this.filters.tagPercentage !== undefined) {
this.updateRangeFilter(this.filters.tagPercentage, [0, 100])
}
if (this.filters.totalRuntime !== undefined) {
this.updateRangeFilter(this.filters.totalRuntime, filterValues.total_runtime)
}
if (this.filters.episodes !== undefined) {
this.updateRangeFilter(this.filters.episodes, filterValues.episodes)
}
if (this.filters.volumes !== undefined) {
this.updateRangeFilter(this.filters.volumes, filterValues.volumes)
}
if (this.filters.mcCount !== undefined) {
this.updateRangeFilter(this.filters.mcCount, filterValues.mcCount)
}
if (this.filters.bloodType !== undefined) {
this.filters.bloodType.whitelist = filterValues.blood_type.map((v) => { return {value: v, text: v}})
}
if (this.filters.gender !== undefined) {
this.filters.gender.whitelist = filterValues.gender.map((v) => { return {value: v, text: v}})
}

this.curFilterValues = this.getFilterParams()
}

private readonly filterChangeCallback = () => {
Expand Down Expand Up @@ -731,13 +761,13 @@ class Filters extends EventTarget {
tagify.loading(false)
return
}
fetch(import.meta.env.VITE_API_URL + urlOrData + '?q=' + value, { signal: controller.signal })
this.AniTools.fetch(urlOrData + '?q=' + value, { signal: controller.signal })
.then(async response => await response.json())
.then((newWhitelist) => {
tagify.whitelist = newWhitelist // update whitelist Array in-place
tagify.loading(false).dropdown.show(value) // render the suggestions dropdown
})
.catch(handleError)
.catch(() => null)
}

tagify.on('input', (e) => {
Expand Down
Loading

0 comments on commit ee0ea77

Please sign in to comment.