2019-11-18 02:04:13 +00:00
|
|
|
import Diff from "scripts/Diff"
|
|
|
|
import delay from "scripts/Utils/delay"
|
|
|
|
import requestIdleCallback from "scripts/Utils/requestIdleCallback"
|
2018-04-02 05:34:16 +00:00
|
|
|
import AnimeNotifier from "../AnimeNotifier"
|
2017-10-17 09:27:15 +00:00
|
|
|
|
2018-03-17 19:41:18 +00:00
|
|
|
// Search page reference
|
2019-11-17 09:25:14 +00:00
|
|
|
let emptySearchHTML = ""
|
|
|
|
let searchPage: HTMLElement
|
|
|
|
let searchPageTitle: HTMLElement
|
|
|
|
const correctResponseRendered = {
|
2018-03-17 19:41:18 +00:00
|
|
|
"anime": false,
|
|
|
|
"character": false,
|
2018-12-06 04:27:01 +00:00
|
|
|
"posts": false,
|
|
|
|
"threads": false,
|
2018-03-17 19:41:18 +00:00
|
|
|
"soundtrack": false,
|
2018-03-17 21:09:17 +00:00
|
|
|
"user": false,
|
2018-11-12 07:52:07 +00:00
|
|
|
"amv": false,
|
2018-03-17 21:09:17 +00:00
|
|
|
"company": false
|
2018-03-17 19:41:18 +00:00
|
|
|
}
|
|
|
|
|
2019-04-22 06:59:08 +00:00
|
|
|
// Search types
|
2019-11-17 09:25:14 +00:00
|
|
|
const searchTypes = Object.keys(correctResponseRendered)
|
2019-04-22 06:59:08 +00:00
|
|
|
|
2018-03-17 21:20:26 +00:00
|
|
|
// Save old term to compare
|
2019-11-17 09:25:14 +00:00
|
|
|
let oldTerm = ""
|
2018-03-17 21:20:26 +00:00
|
|
|
|
2018-03-17 19:41:18 +00:00
|
|
|
// Containers for all the search results
|
2019-11-17 09:25:14 +00:00
|
|
|
const results = new Map<string, HTMLElement>()
|
2018-03-17 19:41:18 +00:00
|
|
|
|
2018-03-29 10:46:40 +00:00
|
|
|
// Delay before a request is sent
|
2018-04-02 14:07:52 +00:00
|
|
|
const searchDelay = 140
|
2018-03-29 10:46:40 +00:00
|
|
|
|
2018-03-29 08:49:53 +00:00
|
|
|
// Fetch options
|
|
|
|
const fetchOptions: RequestInit = {
|
|
|
|
credentials: "same-origin"
|
|
|
|
}
|
|
|
|
|
2018-04-20 18:55:40 +00:00
|
|
|
// Speech recognition
|
|
|
|
let recognition: SpeechRecognition
|
|
|
|
|
2017-10-17 09:27:15 +00:00
|
|
|
// Search
|
2018-04-20 18:16:59 +00:00
|
|
|
export async function search(arn: AnimeNotifier, search: HTMLInputElement, evt?: KeyboardEvent) {
|
|
|
|
if(evt && (evt.ctrlKey || evt.altKey)) {
|
2017-10-17 09:27:15 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-03-22 00:32:04 +00:00
|
|
|
// Determine if we're already seeing the search page
|
2019-11-17 09:25:14 +00:00
|
|
|
const searchPageActivated = (searchPage === arn.app.content.children[0])
|
2018-03-22 00:32:04 +00:00
|
|
|
|
2018-03-17 21:20:26 +00:00
|
|
|
// Check if the search term really changed
|
2019-11-17 09:25:14 +00:00
|
|
|
const term = search.value.trim()
|
2018-03-17 21:20:26 +00:00
|
|
|
|
2018-03-22 00:32:04 +00:00
|
|
|
if(term === oldTerm && searchPageActivated) {
|
2018-03-17 21:20:26 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
oldTerm = term
|
|
|
|
|
2018-03-17 19:41:18 +00:00
|
|
|
// Reset
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const key of searchTypes) {
|
2019-04-22 06:59:08 +00:00
|
|
|
correctResponseRendered[key] = false
|
|
|
|
}
|
2018-03-17 19:41:18 +00:00
|
|
|
|
|
|
|
// Set browser URL
|
2019-11-17 09:25:14 +00:00
|
|
|
const url = "/search/" + term
|
2018-03-17 23:35:38 +00:00
|
|
|
document.title = "Search: " + term
|
2018-03-17 19:41:18 +00:00
|
|
|
arn.app.currentPath = url
|
|
|
|
|
|
|
|
// Unmount mountables to improve visual responsiveness on key press
|
|
|
|
arn.unmountMountables()
|
|
|
|
|
|
|
|
// Show loading spinner
|
|
|
|
arn.loading(true)
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Fetch empty search frame if needed
|
|
|
|
if(emptySearchHTML === "") {
|
2019-11-17 09:25:14 +00:00
|
|
|
const response = await fetch("/_/empty-search")
|
2018-03-17 19:41:18 +00:00
|
|
|
emptySearchHTML = await response.text()
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!searchPageActivated) {
|
|
|
|
if(!searchPage) {
|
|
|
|
searchPage = document.createElement("div")
|
|
|
|
searchPage.innerHTML = emptySearchHTML
|
|
|
|
}
|
|
|
|
|
|
|
|
arn.app.content.innerHTML = ""
|
|
|
|
arn.app.content.appendChild(searchPage)
|
2018-03-29 08:49:53 +00:00
|
|
|
|
|
|
|
history.pushState(url, document.title, url)
|
|
|
|
} else {
|
|
|
|
history.replaceState(url, document.title, url)
|
2018-03-17 19:41:18 +00:00
|
|
|
|
2018-04-02 14:07:52 +00:00
|
|
|
// Delay
|
|
|
|
await delay(searchDelay)
|
|
|
|
}
|
2018-03-29 11:43:17 +00:00
|
|
|
|
|
|
|
if(term !== search.value.trim()) {
|
|
|
|
arn.mountMountables()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-04-22 06:59:08 +00:00
|
|
|
if(!results["anime"]) {
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const key of searchTypes) {
|
2019-04-22 06:59:08 +00:00
|
|
|
results[key] = document.getElementById(`${key}-search-results`)
|
|
|
|
}
|
|
|
|
|
2018-03-17 23:35:38 +00:00
|
|
|
searchPageTitle = document.getElementsByTagName("h1")[0]
|
2018-03-17 19:41:18 +00:00
|
|
|
}
|
|
|
|
|
2018-06-28 06:30:24 +00:00
|
|
|
searchPageTitle.textContent = document.title
|
2018-03-17 23:35:38 +00:00
|
|
|
|
2018-03-17 19:41:18 +00:00
|
|
|
if(!term || term.length < 1) {
|
2019-11-18 02:04:13 +00:00
|
|
|
await Diff.innerHTML(searchPage, emptySearchHTML)
|
2018-03-17 19:41:18 +00:00
|
|
|
arn.app.emit("DOMContentLoaded")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-04-22 06:59:08 +00:00
|
|
|
// Start searching anime
|
2018-03-17 19:53:18 +00:00
|
|
|
fetch("/_/anime-search/" + term, fetchOptions)
|
2019-04-22 06:59:08 +00:00
|
|
|
.then(showResponseInElement(arn, url, "anime", results["anime"]))
|
2019-08-31 07:52:42 +00:00
|
|
|
.catch(err => arn.statusMessage.showError(err))
|
2018-03-17 19:41:18 +00:00
|
|
|
|
2018-04-02 05:34:16 +00:00
|
|
|
requestIdleCallback(() => {
|
2018-07-07 10:29:13 +00:00
|
|
|
// Check that the term hasn't changed in the meantime
|
|
|
|
if(term !== search.value.trim()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-04-22 06:59:08 +00:00
|
|
|
// Search the other types (everything except anime)
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const key of searchTypes) {
|
2019-04-22 06:59:08 +00:00
|
|
|
if(key === "anime") {
|
|
|
|
continue
|
|
|
|
}
|
2018-12-06 04:27:01 +00:00
|
|
|
|
2019-04-22 06:59:08 +00:00
|
|
|
fetch(`/_/${key}-search/` + term, fetchOptions)
|
|
|
|
.then(showResponseInElement(arn, url, key, results[key]))
|
2019-08-31 07:52:42 +00:00
|
|
|
.catch(err => arn.statusMessage.showError(err))
|
2019-04-22 06:59:08 +00:00
|
|
|
}
|
2018-03-29 10:46:40 +00:00
|
|
|
})
|
2018-03-17 19:41:18 +00:00
|
|
|
} catch(err) {
|
|
|
|
console.error(err)
|
|
|
|
} finally {
|
|
|
|
arn.loading(false)
|
2017-10-17 09:27:15 +00:00
|
|
|
}
|
2018-03-17 19:41:18 +00:00
|
|
|
}
|
2017-10-17 09:27:15 +00:00
|
|
|
|
2018-03-17 19:41:18 +00:00
|
|
|
function showResponseInElement(arn: AnimeNotifier, url: string, typeName: string, element: HTMLElement) {
|
2018-03-29 10:46:40 +00:00
|
|
|
return async (response: Response) => {
|
2019-04-26 07:57:22 +00:00
|
|
|
if(!response.ok) {
|
|
|
|
throw response.statusText
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const html = await response.text()
|
2018-03-17 19:41:18 +00:00
|
|
|
|
2018-12-06 04:27:01 +00:00
|
|
|
if(html.includes("no-search-results")) {
|
2019-04-22 06:59:08 +00:00
|
|
|
Diff.mutations.queue(() => (element.parentElement as HTMLElement).classList.add("search-section-disabled"))
|
2018-12-06 04:27:01 +00:00
|
|
|
} else {
|
2019-04-22 06:59:08 +00:00
|
|
|
Diff.mutations.queue(() => (element.parentElement as HTMLElement).classList.remove("search-section-disabled"))
|
2018-12-06 04:27:01 +00:00
|
|
|
}
|
|
|
|
|
2018-03-17 19:41:18 +00:00
|
|
|
if(arn.app.currentPath !== url) {
|
|
|
|
// Return if this result would overwrite the already arrived correct result
|
|
|
|
if(correctResponseRendered[typeName]) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
correctResponseRendered[typeName] = true
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
await Diff.innerHTML(element, html)
|
2018-11-05 11:57:37 +00:00
|
|
|
arn.onNewContent(element)
|
2018-03-17 19:41:18 +00:00
|
|
|
}
|
2017-10-19 19:43:42 +00:00
|
|
|
}
|
2018-04-16 22:02:24 +00:00
|
|
|
|
2018-04-20 18:16:59 +00:00
|
|
|
export function searchBySpeech(arn: AnimeNotifier, element: HTMLElement) {
|
2018-04-20 18:55:40 +00:00
|
|
|
if(recognition) {
|
|
|
|
recognition.stop()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const searchInput = document.getElementById("search") as HTMLInputElement
|
|
|
|
const oldPlaceholder = searchInput.placeholder
|
2018-04-20 18:24:42 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const SpeechRecognition: any = window["SpeechRecognition"] || window["webkitSpeechRecognition"]
|
2018-04-20 18:55:40 +00:00
|
|
|
recognition = new SpeechRecognition()
|
2018-04-20 18:16:59 +00:00
|
|
|
recognition.continuous = false
|
|
|
|
recognition.interimResults = false
|
2018-07-07 10:42:38 +00:00
|
|
|
recognition.lang = navigator.language
|
2018-04-20 18:16:59 +00:00
|
|
|
|
|
|
|
recognition.onresult = evt => {
|
|
|
|
if(evt.results.length > 0) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const result = evt.results.item(0).item(0)
|
|
|
|
const term = result.transcript
|
2018-04-20 18:16:59 +00:00
|
|
|
|
|
|
|
if(term !== "") {
|
|
|
|
searchInput.value = term
|
|
|
|
arn.sideBar.hide()
|
|
|
|
search(arn, searchInput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
recognition.stop()
|
|
|
|
}
|
|
|
|
|
2019-04-22 09:06:50 +00:00
|
|
|
recognition.onerror = _ => {
|
2018-04-20 18:16:59 +00:00
|
|
|
recognition.stop()
|
|
|
|
}
|
|
|
|
|
2019-04-22 09:06:50 +00:00
|
|
|
recognition.onend = _ => {
|
2018-04-20 18:16:59 +00:00
|
|
|
searchInput.placeholder = oldPlaceholder
|
|
|
|
element.classList.remove("speech-listening")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Focus search field
|
|
|
|
searchInput.placeholder = "Listening..."
|
|
|
|
searchInput.value = ""
|
|
|
|
searchInput.focus()
|
|
|
|
searchInput.select()
|
|
|
|
|
|
|
|
// Highlight microphone icon
|
|
|
|
element.classList.add("speech-listening")
|
|
|
|
|
|
|
|
// Start voice recognition
|
|
|
|
recognition.start()
|
|
|
|
}
|