2019-11-17 09:09:39 +00:00
|
|
|
import * as actions from "./Actions"
|
2019-11-18 02:04:13 +00:00
|
|
|
import { uploadAnalytics } from "./Analytics"
|
2018-04-02 05:34:16 +00:00
|
|
|
import Application from "./Application"
|
2019-11-17 09:09:39 +00:00
|
|
|
import AudioPlayer from "./AudioPlayer"
|
|
|
|
import { displayAiringDate, displayDate, displayTime } from "./DateView"
|
2018-04-02 05:34:16 +00:00
|
|
|
import Diff from "./Diff"
|
2019-11-17 09:09:39 +00:00
|
|
|
import ToolTip from "./Elements/tool-tip/tool-tip"
|
2019-11-18 02:04:13 +00:00
|
|
|
import infiniteScroll from "./infiniteScroll"
|
2019-11-17 09:09:39 +00:00
|
|
|
import NotificationManager from "./NotificationManager"
|
2018-04-02 05:34:16 +00:00
|
|
|
import PushManager from "./PushManager"
|
2019-11-18 02:04:13 +00:00
|
|
|
import receiveServerEvents from "./ServerEvent/receiveServerEvents"
|
2019-11-17 09:09:39 +00:00
|
|
|
import ServiceWorkerManager from "./ServiceWorkerManager"
|
|
|
|
import SideBar from "./SideBar"
|
|
|
|
import StatusMessage from "./StatusMessage"
|
2019-11-18 02:04:13 +00:00
|
|
|
import User from "./User"
|
|
|
|
import delay from "./Utils/delay"
|
|
|
|
import emptyPixel from "./Utils/emptyPixel"
|
|
|
|
import findAll from "./Utils/findAll"
|
|
|
|
import findAllInside from "./Utils/findAllInside"
|
|
|
|
import requestIdleCallback from "./Utils/requestIdleCallback"
|
|
|
|
import supportsWebP from "./Utils/supportsWebP"
|
|
|
|
import swapElements from "./Utils/swapElements"
|
2018-12-09 03:37:54 +00:00
|
|
|
import VideoPlayer from "./VideoPlayer"
|
2019-08-30 07:04:28 +00:00
|
|
|
import * as WebComponents from "./WebComponents"
|
2017-06-29 06:32:46 +00:00
|
|
|
|
2018-04-02 05:34:16 +00:00
|
|
|
export default class AnimeNotifier {
|
2019-11-18 02:04:13 +00:00
|
|
|
public isLoading: boolean
|
|
|
|
public app: Application
|
|
|
|
public statusMessage: StatusMessage
|
|
|
|
public notificationManager: NotificationManager | undefined
|
|
|
|
public currentMediaId: string
|
|
|
|
public audioPlayer: AudioPlayer
|
|
|
|
public videoPlayer: VideoPlayer
|
|
|
|
public user: User | null
|
|
|
|
public sideBar: SideBar
|
|
|
|
public pushManager: PushManager
|
|
|
|
|
|
|
|
private title: string
|
|
|
|
private webpCheck: Promise<boolean>
|
|
|
|
private webpEnabled: boolean
|
|
|
|
private visibilityObserver: IntersectionObserver
|
|
|
|
private serviceWorkerManager: ServiceWorkerManager
|
|
|
|
private diffCompletedForCurrentPath: boolean
|
|
|
|
private tip: ToolTip
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.app = new Application()
|
2017-07-06 20:08:49 +00:00
|
|
|
this.title = "Anime Notifier"
|
2017-10-14 10:45:22 +00:00
|
|
|
this.isLoading = true
|
2017-06-24 14:28:16 +00:00
|
|
|
|
2017-07-19 02:18:56 +00:00
|
|
|
// These classes will never be removed on DOM diffs
|
|
|
|
Diff.persistentClasses.add("mounted")
|
2017-10-12 15:52:46 +00:00
|
|
|
Diff.persistentClasses.add("element-found")
|
2018-03-24 16:14:26 +00:00
|
|
|
Diff.persistentClasses.add("active")
|
2017-07-19 02:18:56 +00:00
|
|
|
|
2017-07-19 07:09:55 +00:00
|
|
|
// Never remove src property on diffs
|
|
|
|
Diff.persistentAttributes.add("src")
|
2017-06-19 14:49:24 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
public init() {
|
2017-07-21 10:55:36 +00:00
|
|
|
// App init
|
|
|
|
this.app.init()
|
|
|
|
|
|
|
|
// Event listeners
|
2019-11-18 02:04:13 +00:00
|
|
|
document.addEventListener("readystatechange", () => this.onReadyStateChange())
|
|
|
|
document.addEventListener("DOMContentLoaded", () => this.onContentLoaded())
|
2018-04-24 11:29:46 +00:00
|
|
|
|
|
|
|
// If we finished loading the DOM (either "interactive" or "complete" state),
|
|
|
|
// immediately trigger the event listener functions.
|
|
|
|
if(document.readyState !== "loading") {
|
|
|
|
this.app.emit("DOMContentLoaded")
|
|
|
|
this.run()
|
|
|
|
}
|
2017-06-30 15:51:17 +00:00
|
|
|
|
2017-07-14 02:58:21 +00:00
|
|
|
// Idle
|
2019-11-18 02:04:13 +00:00
|
|
|
requestIdleCallback(() => this.onIdle())
|
2017-06-30 15:51:17 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
public reloadContent(cached?: boolean) {
|
|
|
|
const headers = new Headers()
|
|
|
|
|
|
|
|
if(cached) {
|
|
|
|
headers.set("X-Force-Cache", "true")
|
|
|
|
} else {
|
|
|
|
headers.set("X-No-Cache", "true")
|
|
|
|
}
|
|
|
|
|
|
|
|
const path = this.app.currentPath
|
|
|
|
|
|
|
|
return fetch("/_" + path, {
|
|
|
|
credentials: "same-origin",
|
|
|
|
headers
|
|
|
|
})
|
|
|
|
.then(response => {
|
|
|
|
if(this.app.currentPath !== path) {
|
|
|
|
return Promise.reject("old request")
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve(response)
|
|
|
|
})
|
|
|
|
.then(response => response.text())
|
|
|
|
.then(html => Diff.innerHTML(this.app.content, html))
|
|
|
|
.then(() => this.app.emit("DOMContentLoaded"))
|
|
|
|
}
|
|
|
|
|
|
|
|
public reloadPage() {
|
|
|
|
console.log("reload page", this.app.currentPath)
|
|
|
|
|
|
|
|
const path = this.app.currentPath
|
|
|
|
|
|
|
|
return fetch(path, {
|
|
|
|
credentials: "same-origin"
|
|
|
|
})
|
|
|
|
.then(response => {
|
|
|
|
if(this.app.currentPath !== path) {
|
|
|
|
return Promise.reject("old request")
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve(response)
|
|
|
|
})
|
|
|
|
.then(response => response.text())
|
|
|
|
.then(html => Diff.root(document.documentElement, html))
|
|
|
|
.then(() => this.app.emit("DOMContentLoaded"))
|
|
|
|
.then(() => this.loading(false)) // Because our loading element gets reset due to full page diff
|
|
|
|
}
|
|
|
|
|
|
|
|
public async diff(url: string) {
|
|
|
|
if(url === this.app.currentPath) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const path = "/_" + url
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Start the request
|
|
|
|
const request = fetch(path, {
|
|
|
|
credentials: "same-origin"
|
|
|
|
})
|
|
|
|
.then(response => response.text())
|
|
|
|
|
|
|
|
history.pushState(url, "", url)
|
|
|
|
this.app.currentPath = url
|
|
|
|
this.diffCompletedForCurrentPath = false
|
|
|
|
this.app.markActiveLinks()
|
|
|
|
this.unmountMountables()
|
|
|
|
this.loading(true)
|
|
|
|
|
|
|
|
// Delay by mountable-transition-speed
|
|
|
|
await delay(150)
|
|
|
|
|
|
|
|
const html = await request
|
|
|
|
|
|
|
|
// If the response for the correct path has not arrived yet, show this response
|
|
|
|
if(!this.diffCompletedForCurrentPath) {
|
|
|
|
// If this response was the most recently requested one, mark the requests as completed
|
|
|
|
if(this.app.currentPath === url) {
|
|
|
|
this.diffCompletedForCurrentPath = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update contents
|
|
|
|
await Diff.innerHTML(this.app.content, html)
|
|
|
|
this.app.emit("DOMContentLoaded")
|
|
|
|
}
|
|
|
|
} catch(err) {
|
|
|
|
console.error(err)
|
|
|
|
} finally {
|
|
|
|
this.loading(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public post(url: string, body?: any) {
|
|
|
|
if(this.isLoading) {
|
|
|
|
return Promise.resolve(null)
|
|
|
|
}
|
|
|
|
|
|
|
|
if(body !== undefined && typeof body !== "string") {
|
|
|
|
body = JSON.stringify(body)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.loading(true)
|
|
|
|
|
|
|
|
return fetch(url, {
|
|
|
|
method: "POST",
|
|
|
|
body,
|
|
|
|
credentials: "same-origin"
|
|
|
|
})
|
|
|
|
.then(response => {
|
|
|
|
this.loading(false)
|
|
|
|
|
|
|
|
if(response.status === 200) {
|
|
|
|
return Promise.resolve(response)
|
|
|
|
}
|
|
|
|
|
|
|
|
return response.text().then(err => {
|
|
|
|
throw err
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
this.loading(false)
|
|
|
|
throw err
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
public loading(newState: boolean) {
|
|
|
|
this.isLoading = newState
|
|
|
|
|
|
|
|
if(this.isLoading) {
|
|
|
|
document.documentElement.style.cursor = "progress"
|
|
|
|
this.app.loading.classList.remove(this.app.fadeOutClass)
|
|
|
|
} else {
|
|
|
|
document.documentElement.style.cursor = "auto"
|
|
|
|
this.app.loading.classList.add(this.app.fadeOutClass)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public onNewContent(element: HTMLElement) {
|
|
|
|
// Do the same as for the content loaded event,
|
|
|
|
// except here we are limiting it to the element.
|
|
|
|
this.app.ajaxify(element.getElementsByTagName("a"))
|
|
|
|
this.lazyLoad(findAllInside("lazy", element))
|
|
|
|
this.mountMountables(findAllInside("mountable", element))
|
|
|
|
this.prepareTooltips(findAllInside("tip", element))
|
|
|
|
this.textAreaFocus()
|
|
|
|
}
|
|
|
|
|
|
|
|
public scrollTo(target: HTMLElement) {
|
|
|
|
const duration = 250.0
|
|
|
|
const fullSin = Math.PI / 2
|
|
|
|
const contentPadding = 23
|
|
|
|
|
|
|
|
let newScroll = 0
|
|
|
|
const finalScroll = Math.max(target.getBoundingClientRect().top - contentPadding, 0)
|
|
|
|
|
|
|
|
// Calculating scrollTop will force a layout - careful!
|
|
|
|
const contentContainer = this.app.content.parentElement as HTMLElement
|
|
|
|
const oldScroll = contentContainer.scrollTop
|
|
|
|
const scrollDistance = finalScroll - oldScroll
|
|
|
|
|
|
|
|
if(scrollDistance > 0 && scrollDistance < 1) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const timeStart = Date.now()
|
|
|
|
const timeEnd = timeStart + duration
|
|
|
|
|
|
|
|
const scroll = () => {
|
|
|
|
const time = Date.now()
|
|
|
|
let progress = (time - timeStart) / duration
|
|
|
|
|
|
|
|
if(progress > 1.0) {
|
|
|
|
progress = 1.0
|
|
|
|
}
|
|
|
|
|
|
|
|
newScroll = oldScroll + scrollDistance * Math.sin(progress * fullSin)
|
|
|
|
contentContainer.scrollTop = newScroll
|
|
|
|
|
|
|
|
if(time < timeEnd && newScroll !== finalScroll) {
|
|
|
|
window.requestAnimationFrame(scroll)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
window.requestAnimationFrame(scroll)
|
|
|
|
}
|
|
|
|
|
|
|
|
public findAPIEndpoint(element: HTMLElement | null): string {
|
|
|
|
while(element) {
|
|
|
|
if(element.dataset.api !== undefined) {
|
|
|
|
return element.dataset.api
|
|
|
|
}
|
|
|
|
|
|
|
|
element = element.parentElement
|
|
|
|
}
|
|
|
|
|
|
|
|
this.statusMessage.showError("API object not found")
|
|
|
|
throw "API object not found"
|
|
|
|
}
|
|
|
|
|
|
|
|
public markPlayingMedia() {
|
|
|
|
for(const element of findAll("media-play-area")) {
|
|
|
|
if(element.dataset.mediaId === this.currentMediaId) {
|
|
|
|
element.classList.add("playing")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public mountMountables(elements?: IterableIterator<HTMLElement>) {
|
|
|
|
if(!elements) {
|
|
|
|
elements = findAll("mountable")
|
|
|
|
}
|
|
|
|
|
|
|
|
this.modifyDelayed(elements, element => element.classList.add("mounted"))
|
|
|
|
}
|
|
|
|
|
|
|
|
public unmountMountables() {
|
|
|
|
for(const element of findAll("mountable")) {
|
|
|
|
if(element.classList.contains("never-unmount")) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
Diff.mutations.queue(() => element.classList.remove("mounted"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async updatePushUI() {
|
|
|
|
if(!this.app.currentPath.includes("/settings/notifications")) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-20 07:47:00 +00:00
|
|
|
const enableButton = document.getElementById("notifications-enable") as HTMLButtonElement
|
|
|
|
const disableButton = document.getElementById("notifications-disable") as HTMLButtonElement
|
|
|
|
const testButton = document.getElementById("notifications-test") as HTMLButtonElement
|
|
|
|
const footer = document.getElementById("notifications-footer") as HTMLElement
|
2019-11-18 02:04:13 +00:00
|
|
|
|
|
|
|
if(!this.pushManager.pushSupported) {
|
|
|
|
enableButton.classList.add("hidden")
|
|
|
|
disableButton.classList.add("hidden")
|
2019-11-20 07:47:00 +00:00
|
|
|
testButton.classList.add("hidden")
|
|
|
|
footer.innerHTML = "Your browser doesn't support push notifications!"
|
2019-11-18 02:04:13 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const subscription = await this.pushManager.subscription()
|
|
|
|
|
|
|
|
if(subscription) {
|
|
|
|
enableButton.classList.add("hidden")
|
|
|
|
disableButton.classList.remove("hidden")
|
|
|
|
} else {
|
|
|
|
enableButton.classList.remove("hidden")
|
|
|
|
disableButton.classList.add("hidden")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public assignActions() {
|
|
|
|
for(const element of findAll("action")) {
|
|
|
|
const actionTrigger = element.dataset.trigger
|
|
|
|
const actionName = element.dataset.action
|
|
|
|
|
|
|
|
// Filter out invalid definitions
|
|
|
|
if(!actionTrigger || !actionName) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const oldAction = element["action assigned"]
|
|
|
|
|
|
|
|
if(oldAction) {
|
|
|
|
if(oldAction.trigger === actionTrigger && oldAction.action === actionName) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
element.removeEventListener(oldAction.trigger, oldAction.handler)
|
|
|
|
}
|
|
|
|
|
|
|
|
// This prevents default actions on links
|
|
|
|
if(actionTrigger === "click" && element.tagName === "A") {
|
|
|
|
element.onclick = null
|
|
|
|
}
|
|
|
|
|
|
|
|
// Warn us about undefined actions
|
|
|
|
if(!(actionName in actions)) {
|
|
|
|
this.statusMessage.showError(`Action '${actionName}' has not been defined`)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register the actual action handler
|
|
|
|
const actionHandler = e => {
|
|
|
|
if(!actionName) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
actions[actionName](this, element, e)
|
|
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
e.preventDefault()
|
|
|
|
}
|
|
|
|
|
|
|
|
element.addEventListener(actionTrigger, actionHandler)
|
|
|
|
|
|
|
|
// Use "action assigned" flag instead of removing the class.
|
|
|
|
// This will make sure that DOM diffs which restore the class name
|
|
|
|
// will not assign the action multiple times to the same element.
|
|
|
|
element["action assigned"] = {
|
|
|
|
trigger: actionTrigger,
|
|
|
|
action: actionName,
|
|
|
|
handler: actionHandler
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private onReadyStateChange() {
|
2017-06-19 15:45:27 +00:00
|
|
|
if(document.readyState !== "interactive") {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.run()
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private run() {
|
2017-07-03 23:20:27 +00:00
|
|
|
// Initiate the elements we need
|
2019-04-19 13:12:33 +00:00
|
|
|
this.app.content = document.getElementById("content") as HTMLElement
|
|
|
|
this.app.loading = document.getElementById("loading") as HTMLElement
|
2017-07-03 23:20:27 +00:00
|
|
|
|
2019-07-06 04:01:26 +00:00
|
|
|
// Web components
|
2019-08-30 07:04:28 +00:00
|
|
|
WebComponents.register()
|
2019-07-06 04:01:26 +00:00
|
|
|
|
|
|
|
// Tooltip
|
|
|
|
this.tip = new ToolTip()
|
|
|
|
document.body.appendChild(this.tip)
|
|
|
|
document.addEventListener("linkclicked", () => this.tip.classList.add("fade-out"))
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
// Enable lazy load
|
|
|
|
this.visibilityObserver = new IntersectionObserver(
|
|
|
|
entries => {
|
|
|
|
for(const entry of entries) {
|
|
|
|
if(entry.isIntersecting) {
|
|
|
|
entry.target["became visible"]()
|
|
|
|
this.visibilityObserver.unobserve(entry.target)
|
2019-07-06 04:01:26 +00:00
|
|
|
}
|
2019-11-18 02:04:13 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
)
|
2019-07-06 04:01:26 +00:00
|
|
|
|
2017-07-12 18:37:34 +00:00
|
|
|
// Status message
|
|
|
|
this.statusMessage = new StatusMessage(
|
2019-04-19 13:50:52 +00:00
|
|
|
document.getElementById("status-message") as HTMLElement,
|
|
|
|
document.getElementById("status-message-text") as HTMLElement
|
2017-07-12 18:37:34 +00:00
|
|
|
)
|
|
|
|
|
2018-04-02 15:20:25 +00:00
|
|
|
this.app.onError = (error: Error) => {
|
|
|
|
this.statusMessage.showError(error, 3000)
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
// User
|
|
|
|
const userElement = document.getElementById("user")
|
|
|
|
|
|
|
|
if(userElement && userElement.dataset.id) {
|
|
|
|
this.user = new User(userElement.dataset.id)
|
|
|
|
|
|
|
|
if(userElement.dataset.pro === "true") {
|
|
|
|
const theme = userElement.dataset.theme
|
|
|
|
|
|
|
|
// Don't apply light theme on load because
|
|
|
|
// it's already the standard theme.
|
|
|
|
if(theme && theme !== "light") {
|
|
|
|
actions.applyTheme(theme)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-14 21:50:34 +00:00
|
|
|
// Push manager
|
|
|
|
this.pushManager = new PushManager()
|
2017-07-19 14:56:02 +00:00
|
|
|
|
2018-02-28 15:26:49 +00:00
|
|
|
// Notification manager
|
2019-04-19 13:50:52 +00:00
|
|
|
if(this.user) {
|
|
|
|
this.notificationManager = new NotificationManager(
|
|
|
|
document.getElementById("notification-icon") as HTMLElement,
|
|
|
|
document.getElementById("notification-count") as HTMLElement
|
|
|
|
)
|
|
|
|
}
|
2018-02-28 15:26:49 +00:00
|
|
|
|
2018-03-24 22:04:31 +00:00
|
|
|
// Audio player
|
|
|
|
this.audioPlayer = new AudioPlayer(this)
|
|
|
|
|
2018-12-09 03:39:25 +00:00
|
|
|
// Video player
|
|
|
|
this.videoPlayer = new VideoPlayer(this)
|
|
|
|
|
2017-07-19 14:56:02 +00:00
|
|
|
// Sidebar control
|
2018-04-02 05:44:11 +00:00
|
|
|
this.sideBar = new SideBar(document.getElementById("sidebar"))
|
2017-10-16 10:56:46 +00:00
|
|
|
|
|
|
|
// Infinite scrolling
|
2019-11-18 02:04:13 +00:00
|
|
|
if(this.app.content.parentElement) {
|
|
|
|
infiniteScroll(this.app.content.parentElement, 150)
|
|
|
|
}
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2019-03-26 06:06:15 +00:00
|
|
|
// WebP
|
2019-03-26 16:07:02 +00:00
|
|
|
this.webpCheck = supportsWebP().then(val => this.webpEnabled = val)
|
2019-03-26 06:06:15 +00:00
|
|
|
|
2017-10-14 10:45:22 +00:00
|
|
|
// Loading
|
|
|
|
this.loading(false)
|
2017-06-19 14:49:24 +00:00
|
|
|
}
|
2017-06-19 15:45:27 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private onContentLoaded() {
|
2017-06-30 15:51:17 +00:00
|
|
|
// Stop watching all the objects from the previous page.
|
2017-06-24 14:17:38 +00:00
|
|
|
this.visibilityObserver.disconnect()
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
Promise.all([
|
2017-09-22 04:19:32 +00:00
|
|
|
Promise.resolve().then(() => this.mountMountables()),
|
2017-10-12 15:52:46 +00:00
|
|
|
Promise.resolve().then(() => this.lazyLoad()),
|
2017-07-14 02:58:21 +00:00
|
|
|
Promise.resolve().then(() => this.displayLocalDates()),
|
|
|
|
Promise.resolve().then(() => this.setSelectBoxValue()),
|
2018-11-15 11:19:40 +00:00
|
|
|
Promise.resolve().then(() => this.textAreaFocus()),
|
2018-12-07 00:54:17 +00:00
|
|
|
Promise.resolve().then(() => this.markPlayingMedia()),
|
2017-07-14 23:32:06 +00:00
|
|
|
Promise.resolve().then(() => this.assignActions()),
|
2017-07-17 01:14:05 +00:00
|
|
|
Promise.resolve().then(() => this.updatePushUI()),
|
2017-10-04 11:39:59 +00:00
|
|
|
Promise.resolve().then(() => this.dragAndDrop()),
|
2018-11-23 10:11:46 +00:00
|
|
|
Promise.resolve().then(() => this.colorBoxes()),
|
2018-10-27 02:01:21 +00:00
|
|
|
Promise.resolve().then(() => this.loadCharacterRanking()),
|
2019-07-06 04:01:26 +00:00
|
|
|
Promise.resolve().then(() => this.prepareTooltips()),
|
2018-07-05 17:37:22 +00:00
|
|
|
Promise.resolve().then(() => this.countUp())
|
2017-07-14 02:58:21 +00:00
|
|
|
])
|
2017-07-06 20:08:49 +00:00
|
|
|
|
2017-07-14 23:32:06 +00:00
|
|
|
// Apply page title
|
2018-06-28 06:30:24 +00:00
|
|
|
this.applyPageTitle()
|
2018-11-15 03:42:10 +00:00
|
|
|
|
|
|
|
// Auto-focus first input element on welcome page.
|
|
|
|
if(location.pathname === "/welcome") {
|
2019-11-17 09:25:14 +00:00
|
|
|
const firstInput = this.app.content.getElementsByTagName("input")[0] as HTMLInputElement
|
2018-11-15 04:04:14 +00:00
|
|
|
|
|
|
|
if(firstInput) {
|
|
|
|
firstInput.focus()
|
|
|
|
}
|
2018-11-15 03:42:10 +00:00
|
|
|
}
|
2018-06-28 06:30:24 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private applyPageTitle() {
|
2019-11-17 09:25:14 +00:00
|
|
|
const headers = document.getElementsByTagName("h1")
|
2017-07-06 20:08:49 +00:00
|
|
|
|
2018-06-28 06:30:24 +00:00
|
|
|
if(this.app.currentPath === "/" || headers.length === 0 || headers[0].textContent === "NOTIFY.MOE") {
|
2017-07-06 20:08:49 +00:00
|
|
|
if(document.title !== this.title) {
|
|
|
|
document.title = this.title
|
|
|
|
}
|
2019-04-19 13:12:33 +00:00
|
|
|
} else if(headers[0].textContent) {
|
2018-06-28 06:30:24 +00:00
|
|
|
document.title = headers[0].textContent
|
2017-07-06 20:08:49 +00:00
|
|
|
}
|
2017-06-29 13:55:04 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private textAreaFocus() {
|
2018-11-15 11:19:40 +00:00
|
|
|
const newPostText = document.getElementById("new-post-text") as HTMLTextAreaElement
|
|
|
|
|
|
|
|
if(!newPostText || newPostText["has-input-listener"]) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
newPostText.addEventListener("input", () => {
|
|
|
|
if(newPostText.value.length > 0) {
|
|
|
|
const newPostActions = document.getElementsByClassName("new-post-actions")[0]
|
|
|
|
newPostActions.classList.add("new-post-actions-enabled")
|
|
|
|
} else {
|
|
|
|
const newPostActions = document.getElementsByClassName("new-post-actions")[0]
|
|
|
|
newPostActions.classList.remove("new-post-actions-enabled")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
newPostText["has-input-listener"] = true
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private async onIdle() {
|
2018-04-24 11:29:46 +00:00
|
|
|
// Register event listeners
|
|
|
|
document.addEventListener("keydown", this.onKeyDown.bind(this), false)
|
|
|
|
window.addEventListener("popstate", this.onPopState.bind(this))
|
|
|
|
window.addEventListener("error", this.onError.bind(this))
|
|
|
|
|
2017-07-19 03:23:06 +00:00
|
|
|
// Service worker
|
2017-11-07 08:06:04 +00:00
|
|
|
this.serviceWorkerManager = new ServiceWorkerManager(this, "/service-worker")
|
2017-10-20 00:43:02 +00:00
|
|
|
this.serviceWorkerManager.register()
|
2017-07-19 03:23:06 +00:00
|
|
|
|
|
|
|
// Analytics
|
2017-10-01 05:50:53 +00:00
|
|
|
if(this.user) {
|
2019-11-18 02:04:13 +00:00
|
|
|
uploadAnalytics()
|
2017-10-01 05:50:53 +00:00
|
|
|
}
|
2017-07-15 00:01:14 +00:00
|
|
|
|
2017-07-19 03:23:06 +00:00
|
|
|
// Offline message
|
2017-07-15 00:01:14 +00:00
|
|
|
if(navigator.onLine === false) {
|
2018-04-17 23:08:04 +00:00
|
|
|
this.statusMessage.showInfo("You are viewing an offline version of the site now.")
|
2017-07-15 00:01:14 +00:00
|
|
|
}
|
2017-11-16 15:56:34 +00:00
|
|
|
|
2018-02-28 15:26:49 +00:00
|
|
|
// Notification manager
|
2019-04-19 13:50:52 +00:00
|
|
|
if(this.notificationManager) {
|
2018-02-28 15:26:49 +00:00
|
|
|
this.notificationManager.update()
|
|
|
|
}
|
|
|
|
|
2018-03-19 22:49:58 +00:00
|
|
|
// Bind unload event
|
|
|
|
window.addEventListener("beforeunload", this.onBeforeUnload.bind(this))
|
|
|
|
|
2018-04-20 18:24:42 +00:00
|
|
|
// Show microphone icon if speech input is available
|
|
|
|
if(window["SpeechRecognition"] || window["webkitSpeechRecognition"]) {
|
|
|
|
document.getElementsByClassName("speech-input")[0].classList.add("speech-input-available")
|
|
|
|
}
|
|
|
|
|
2018-10-28 02:31:43 +00:00
|
|
|
// Ensure a minimum size for the desktop app
|
|
|
|
const minWidth = 1420
|
|
|
|
const minHeight = 800
|
|
|
|
|
|
|
|
if(window.outerWidth <= minWidth || window.outerHeight <= minHeight) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const finalWidth = window.outerWidth < minWidth ? minWidth : window.outerWidth
|
|
|
|
const finalHeight = window.outerHeight < minHeight ? minHeight : window.outerHeight
|
2018-10-28 02:31:43 +00:00
|
|
|
|
|
|
|
window.resizeTo(finalWidth, finalHeight)
|
|
|
|
}
|
|
|
|
|
2018-11-06 20:40:03 +00:00
|
|
|
// Server sent events
|
|
|
|
if(this.user && EventSource) {
|
2019-11-18 02:04:13 +00:00
|
|
|
receiveServerEvents(this)
|
2018-11-06 20:40:03 +00:00
|
|
|
}
|
|
|
|
|
2018-04-08 10:59:36 +00:00
|
|
|
// // Download popular anime titles for the search
|
2017-11-16 15:56:34 +00:00
|
|
|
// let response = await fetch("/api/popular/anime/titles/500")
|
|
|
|
// let titles = await response.json()
|
|
|
|
// let titleList = document.createElement("datalist")
|
|
|
|
// titleList.id = "popular-anime-titles-list"
|
|
|
|
|
|
|
|
// for(let title of titles) {
|
|
|
|
// let option = document.createElement("option")
|
|
|
|
// option.value = title
|
|
|
|
// titleList.appendChild(option)
|
|
|
|
// }
|
|
|
|
|
|
|
|
// document.body.appendChild(titleList)
|
|
|
|
|
2018-04-02 05:44:11 +00:00
|
|
|
// let search = document.getElementById("search") as HTMLInputElement
|
2017-11-16 15:56:34 +00:00
|
|
|
// search.setAttribute("list", titleList.id)
|
2017-07-13 15:56:14 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private onBeforeUnload(e: BeforeUnloadEvent) {
|
2019-11-17 09:44:30 +00:00
|
|
|
if(this.app.currentPath !== "/new/thread") {
|
|
|
|
return
|
|
|
|
}
|
2018-03-19 22:49:58 +00:00
|
|
|
|
2019-11-17 09:44:30 +00:00
|
|
|
if(!document.activeElement) {
|
|
|
|
return
|
2018-03-19 22:49:58 +00:00
|
|
|
}
|
|
|
|
|
2019-11-17 09:44:30 +00:00
|
|
|
if(document.activeElement.tagName !== "TEXTAREA") {
|
|
|
|
return
|
2018-03-19 22:49:58 +00:00
|
|
|
}
|
2019-11-17 09:44:30 +00:00
|
|
|
|
|
|
|
if((document.activeElement as HTMLTextAreaElement).value.length < 20) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prevent closing tab on new thread page
|
|
|
|
e.returnValue = "You have unsaved changes on the current page. Are you sure you want to leave?"
|
2018-03-19 22:49:58 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private prepareTooltips(elements?: IterableIterator<HTMLElement>) {
|
2019-07-06 04:01:26 +00:00
|
|
|
if(!elements) {
|
|
|
|
elements = findAll("tip")
|
|
|
|
}
|
2018-04-20 11:34:12 +00:00
|
|
|
|
2019-07-06 04:01:26 +00:00
|
|
|
this.tip.setAttribute("active", "false")
|
2018-04-20 11:34:12 +00:00
|
|
|
|
2019-07-06 04:01:26 +00:00
|
|
|
// Assign mouse enter event handler
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const element of elements) {
|
2019-07-06 04:01:26 +00:00
|
|
|
element.onmouseenter = () => {
|
|
|
|
this.tip.classList.remove("fade-out")
|
|
|
|
this.tip.show(element)
|
|
|
|
}
|
2018-04-22 12:45:06 +00:00
|
|
|
|
2019-07-06 04:01:26 +00:00
|
|
|
element.onmouseleave = () => {
|
|
|
|
this.tip.hide()
|
2018-04-19 09:31:01 +00:00
|
|
|
}
|
2019-07-06 04:01:26 +00:00
|
|
|
}
|
2018-04-18 18:11:20 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private dragAndDrop() {
|
2018-11-12 10:48:54 +00:00
|
|
|
if(location.pathname.includes("/animelist/")) {
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const listItem of findAll("anime-list-item")) {
|
2018-11-12 10:48:54 +00:00
|
|
|
// Skip elements that have their event listeners attached already
|
2019-08-31 02:11:11 +00:00
|
|
|
if(listItem["drag-listeners-attached"]) {
|
2018-11-12 10:48:54 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const name = listItem.getElementsByClassName("anime-list-item-name")[0]
|
|
|
|
const imageContainer = listItem.getElementsByClassName("anime-list-item-image-container")[0]
|
2019-08-31 02:11:11 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const onDrag = evt => {
|
2019-08-31 02:11:11 +00:00
|
|
|
if(!evt.dataTransfer) {
|
2018-11-12 10:48:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const image = imageContainer.getElementsByClassName("anime-list-item-image")[0]
|
2019-03-04 02:28:51 +00:00
|
|
|
|
|
|
|
if(image) {
|
2019-04-19 13:12:33 +00:00
|
|
|
evt.dataTransfer.setDragImage(image, 0, 0)
|
2019-03-04 02:28:51 +00:00
|
|
|
}
|
2018-11-12 10:48:54 +00:00
|
|
|
|
2019-04-19 13:12:33 +00:00
|
|
|
evt.dataTransfer.setData("text/plain", JSON.stringify({
|
2019-08-31 02:11:11 +00:00
|
|
|
api: listItem.dataset.api,
|
2018-11-12 10:48:54 +00:00
|
|
|
animeTitle: name.textContent
|
|
|
|
}))
|
2019-04-19 13:12:33 +00:00
|
|
|
|
|
|
|
evt.dataTransfer.effectAllowed = "move"
|
2019-08-31 02:11:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
name.addEventListener("dragstart", onDrag, false)
|
|
|
|
imageContainer.addEventListener("dragstart", onDrag, false)
|
2018-11-12 10:48:54 +00:00
|
|
|
|
|
|
|
// Prevent re-attaching the same listeners
|
2019-08-31 02:11:11 +00:00
|
|
|
listItem["drag-listeners-attached"] = true
|
2017-10-05 07:39:37 +00:00
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const element of findAll("tab")) {
|
2018-11-12 10:48:54 +00:00
|
|
|
// Skip elements that have their event listeners attached already
|
|
|
|
if(element["drop-listeners-attached"]) {
|
|
|
|
continue
|
2017-10-05 07:54:11 +00:00
|
|
|
}
|
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
element.addEventListener("drop", async e => {
|
2019-09-06 05:57:43 +00:00
|
|
|
let toElement: HTMLElement | null = e.target as HTMLElement
|
2017-10-05 07:54:11 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
// Find tab element
|
|
|
|
while(toElement && !toElement.classList.contains("tab")) {
|
|
|
|
toElement = toElement.parentElement
|
|
|
|
}
|
2017-10-05 07:54:11 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
// Ignore a drop on the current status tab
|
2019-04-19 13:12:33 +00:00
|
|
|
if(!toElement || toElement.classList.contains("active") || !e.dataTransfer) {
|
2018-11-12 10:48:54 +00:00
|
|
|
return
|
|
|
|
}
|
2017-10-05 07:54:11 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const data = e.dataTransfer.getData("text/plain")
|
2019-04-19 13:12:33 +00:00
|
|
|
let json: any
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
try {
|
|
|
|
json = JSON.parse(data)
|
2018-11-16 12:45:13 +00:00
|
|
|
} catch(err) {
|
2018-11-12 10:48:54 +00:00
|
|
|
return
|
|
|
|
}
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
if(!json || !json.api) {
|
|
|
|
return
|
|
|
|
}
|
2017-10-04 11:39:59 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
e.stopPropagation()
|
|
|
|
e.preventDefault()
|
2017-10-04 11:39:59 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const tabText = toElement.textContent
|
2019-04-19 13:12:33 +00:00
|
|
|
|
|
|
|
if(!tabText) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
let newStatus = tabText.toLowerCase()
|
2017-10-04 11:39:59 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
if(newStatus === "on hold") {
|
|
|
|
newStatus = "hold"
|
|
|
|
}
|
2017-10-04 11:39:59 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
try {
|
|
|
|
await this.post(json.api, {
|
|
|
|
Status: newStatus
|
|
|
|
})
|
|
|
|
await this.reloadContent()
|
2017-10-04 11:39:59 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
this.statusMessage.showInfo(`Moved "${json.animeTitle}" to "${tabText}".`)
|
|
|
|
} catch(err) {
|
|
|
|
this.statusMessage.showError(err)
|
|
|
|
}
|
2017-10-04 11:39:59 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
}, false)
|
2017-10-05 07:54:11 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
element.addEventListener("dragenter", e => {
|
|
|
|
e.preventDefault()
|
|
|
|
}, false)
|
2017-10-05 07:54:11 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
element.addEventListener("dragleave", e => {
|
|
|
|
e.preventDefault()
|
|
|
|
}, false)
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
element.addEventListener("dragover", e => {
|
|
|
|
e.preventDefault()
|
|
|
|
}, false)
|
2017-10-05 07:54:11 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
// Prevent re-attaching the same listeners
|
|
|
|
element["drop-listeners-attached"] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(location.pathname.startsWith("/inventory")) {
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const element of findAll("inventory-slot")) {
|
2018-11-12 10:48:54 +00:00
|
|
|
// Skip elements that have their event listeners attached already
|
|
|
|
if(element["drag-listeners-attached"]) {
|
|
|
|
continue
|
2017-10-04 11:39:59 +00:00
|
|
|
}
|
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
element.addEventListener("dragstart", e => {
|
2019-04-21 09:16:19 +00:00
|
|
|
if(!element.draggable || !element.dataset.index || !e.dataTransfer) {
|
2018-11-12 10:48:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
e.dataTransfer.setData("text", element.dataset.index)
|
|
|
|
}, false)
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2019-04-22 09:06:50 +00:00
|
|
|
element.addEventListener("dblclick", async _ => {
|
2019-04-21 09:16:19 +00:00
|
|
|
if(!element.draggable || !element.dataset.index) {
|
2018-11-12 10:48:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const itemName = element.getAttribute("aria-label")
|
2018-11-12 10:48:54 +00:00
|
|
|
|
|
|
|
if(element.dataset.consumable !== "true") {
|
|
|
|
return this.statusMessage.showError(itemName + " is not a consumable item.")
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const apiEndpoint = this.findAPIEndpoint(element)
|
2018-11-12 10:48:54 +00:00
|
|
|
|
2019-04-22 06:02:51 +00:00
|
|
|
try {
|
|
|
|
await this.post(apiEndpoint + "/use/" + element.dataset.index)
|
|
|
|
await this.reloadContent()
|
|
|
|
this.statusMessage.showInfo(`You used ${itemName}.`)
|
|
|
|
} catch(err) {
|
|
|
|
this.statusMessage.showError(err)
|
|
|
|
}
|
2018-11-12 10:48:54 +00:00
|
|
|
}, false)
|
2017-10-05 07:39:37 +00:00
|
|
|
|
2019-04-22 09:06:50 +00:00
|
|
|
element.addEventListener("dragenter", _ => {
|
2018-11-12 10:48:54 +00:00
|
|
|
element.classList.add("drag-enter")
|
|
|
|
}, false)
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2019-04-22 09:06:50 +00:00
|
|
|
element.addEventListener("dragleave", _ => {
|
2018-11-12 10:48:54 +00:00
|
|
|
element.classList.remove("drag-enter")
|
|
|
|
}, false)
|
2017-10-05 07:39:37 +00:00
|
|
|
|
2018-11-12 10:48:54 +00:00
|
|
|
element.addEventListener("dragover", e => {
|
|
|
|
e.preventDefault()
|
|
|
|
}, false)
|
|
|
|
|
2019-09-06 05:57:43 +00:00
|
|
|
element.addEventListener("drop", async e => {
|
2019-04-21 08:39:24 +00:00
|
|
|
element.classList.remove("drag-enter")
|
2018-11-12 10:48:54 +00:00
|
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
e.preventDefault()
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const inventory = element.parentElement
|
2019-04-21 08:39:24 +00:00
|
|
|
|
|
|
|
if(!inventory || !e.dataTransfer) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const fromIndex = e.dataTransfer.getData("text")
|
2018-11-12 10:48:54 +00:00
|
|
|
|
|
|
|
if(!fromIndex) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const fromElement = inventory.childNodes[fromIndex] as HTMLElement
|
|
|
|
const toIndex = element.dataset.index
|
2018-11-12 10:48:54 +00:00
|
|
|
|
2019-04-21 08:39:24 +00:00
|
|
|
if(!toIndex || fromElement === element || fromIndex === toIndex) {
|
|
|
|
console.error("Invalid drag & drop from", fromIndex, "to", toIndex)
|
2018-11-12 10:48:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Swap in database
|
2019-11-17 09:25:14 +00:00
|
|
|
const apiEndpoint = this.findAPIEndpoint(inventory)
|
2018-11-12 10:48:54 +00:00
|
|
|
|
2019-09-06 05:57:43 +00:00
|
|
|
try {
|
|
|
|
await this.post(apiEndpoint + "/swap/" + fromIndex + "/" + toIndex)
|
|
|
|
} catch(err) {
|
|
|
|
this.statusMessage.showError(err)
|
|
|
|
}
|
2018-11-12 10:48:54 +00:00
|
|
|
|
|
|
|
// Swap in UI
|
2019-04-21 08:39:24 +00:00
|
|
|
swapElements(fromElement, element)
|
2018-11-12 10:48:54 +00:00
|
|
|
|
|
|
|
fromElement.dataset.index = toIndex
|
2019-04-21 08:39:24 +00:00
|
|
|
element.dataset.index = fromIndex
|
2018-11-12 10:48:54 +00:00
|
|
|
}, false)
|
|
|
|
|
|
|
|
// Prevent re-attaching the same listeners
|
|
|
|
element["drag-listeners-attached"] = true
|
|
|
|
}
|
2017-10-04 11:39:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private loadCharacterRanking() {
|
2018-10-27 02:01:21 +00:00
|
|
|
if(!this.app.currentPath.includes("/character/")) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const element of findAll("character-ranking")) {
|
2018-10-27 02:01:21 +00:00
|
|
|
fetch(`/api/character/${element.dataset.characterId}/ranking`).then(async response => {
|
2019-11-17 09:25:14 +00:00
|
|
|
const ranking = await response.json()
|
2018-10-27 02:01:21 +00:00
|
|
|
|
2018-10-29 10:52:25 +00:00
|
|
|
if(!ranking.rank) {
|
2018-10-27 02:01:21 +00:00
|
|
|
return
|
|
|
|
}
|
2019-11-18 02:04:13 +00:00
|
|
|
|
|
|
|
Diff.mutations.queue(() => {
|
|
|
|
const percentile = Math.ceil(ranking.percentile * 100)
|
|
|
|
|
|
|
|
element.textContent = "#" + ranking.rank.toString()
|
|
|
|
element.title = "Top " + percentile + "%"
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2017-07-13 22:11:25 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private colorBoxes() {
|
|
|
|
if(!this.app.currentPath.includes("/explore/color/") && !this.app.currentPath.includes("/settings")) {
|
|
|
|
return
|
|
|
|
}
|
2017-10-14 10:45:22 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
for(const element of findAll("color-box")) {
|
|
|
|
Diff.mutations.queue(() => {
|
|
|
|
if(!element.dataset.color) {
|
|
|
|
console.error("color-box missing data-color attribute:", element)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
element.style.backgroundColor = element.dataset.color
|
|
|
|
})
|
2017-06-20 10:41:26 +00:00
|
|
|
}
|
|
|
|
}
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private countUp() {
|
|
|
|
if(!this.app.currentPath.includes("/paypal/success")) {
|
|
|
|
return
|
|
|
|
}
|
2017-06-20 10:41:26 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
for(const element of findAll("count-up")) {
|
|
|
|
if(!element.textContent) {
|
|
|
|
console.error("count-up missing text content:", element)
|
2017-10-05 19:27:49 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
const final = parseInt(element.textContent, 10)
|
|
|
|
const duration = 2000.0
|
|
|
|
const start = Date.now()
|
2017-07-08 21:27:24 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
element.textContent = "0"
|
|
|
|
|
|
|
|
const callback = () => {
|
|
|
|
let progress = (Date.now() - start) / duration
|
|
|
|
|
|
|
|
if(progress > 1) {
|
|
|
|
progress = 1
|
2017-07-08 21:27:24 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
element.textContent = String(Math.round(progress * final))
|
2017-07-08 21:27:24 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
if(progress < 1) {
|
|
|
|
window.requestAnimationFrame(callback)
|
|
|
|
}
|
2017-12-04 11:21:41 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
window.requestAnimationFrame(callback)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private setSelectBoxValue() {
|
|
|
|
for(const element of document.getElementsByTagName("select")) {
|
|
|
|
const attributeValue = element.getAttribute("value")
|
|
|
|
|
|
|
|
if(!attributeValue) {
|
|
|
|
console.error("Select box without a value:", element)
|
2017-10-05 19:27:49 +00:00
|
|
|
continue
|
2017-10-05 11:48:16 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
element.value = attributeValue
|
|
|
|
}
|
|
|
|
}
|
2019-04-21 09:16:19 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private displayLocalDates() {
|
|
|
|
const now = new Date()
|
2017-06-26 01:57:29 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
for(const element of findAll("utc-airing-date")) {
|
|
|
|
displayAiringDate(element, now)
|
|
|
|
}
|
2017-07-08 21:27:24 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
for(const element of findAll("utc-date")) {
|
|
|
|
displayDate(element, now)
|
|
|
|
}
|
2017-06-20 20:54:45 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
for(const element of findAll("utc-date-absolute")) {
|
|
|
|
displayTime(element)
|
2017-06-20 10:41:26 +00:00
|
|
|
}
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private async lazyLoad(elements?: IterableIterator<Element>) {
|
2018-04-02 14:07:52 +00:00
|
|
|
if(!elements) {
|
|
|
|
elements = findAll("lazy")
|
|
|
|
}
|
|
|
|
|
2019-03-26 16:07:02 +00:00
|
|
|
await this.webpCheck
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const element of elements) {
|
2017-10-12 15:52:46 +00:00
|
|
|
switch(element.tagName) {
|
|
|
|
case "IMG":
|
|
|
|
this.lazyLoadImage(element as HTMLImageElement)
|
|
|
|
break
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2018-04-15 09:14:11 +00:00
|
|
|
case "VIDEO":
|
|
|
|
this.lazyLoadVideo(element as HTMLVideoElement)
|
|
|
|
break
|
|
|
|
|
2017-10-12 15:52:46 +00:00
|
|
|
case "IFRAME":
|
|
|
|
this.lazyLoadIFrame(element as HTMLIFrameElement)
|
|
|
|
break
|
|
|
|
}
|
2017-06-24 14:17:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private lazyLoadImage(element: HTMLImageElement) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const pixelRatio = window.devicePixelRatio
|
2018-04-11 13:06:08 +00:00
|
|
|
|
2017-06-24 14:17:38 +00:00
|
|
|
// Once the image becomes visible, load it
|
2017-10-12 15:52:46 +00:00
|
|
|
element["became visible"] = () => {
|
2019-11-17 09:25:14 +00:00
|
|
|
const dataSrc = element.dataset.src
|
2019-04-21 09:16:19 +00:00
|
|
|
|
|
|
|
if(!dataSrc) {
|
|
|
|
console.error("Image missing data-src attribute:", element)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const dotPos = dataSrc.lastIndexOf(".")
|
2018-03-03 15:44:21 +00:00
|
|
|
let base = dataSrc.substring(0, dotPos)
|
2017-11-09 17:10:10 +00:00
|
|
|
let extension = ""
|
|
|
|
|
2017-07-08 13:40:13 +00:00
|
|
|
// Replace URL with WebP if supported
|
2018-11-21 11:35:25 +00:00
|
|
|
if(this.webpEnabled && element.dataset.webp === "true" && !dataSrc.endsWith(".svg")) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const queryPos = dataSrc.lastIndexOf("?")
|
2018-03-03 15:44:21 +00:00
|
|
|
|
|
|
|
if(queryPos !== -1) {
|
|
|
|
extension = ".webp" + dataSrc.substring(queryPos)
|
|
|
|
} else {
|
|
|
|
extension = ".webp"
|
|
|
|
}
|
2017-07-08 13:40:13 +00:00
|
|
|
} else {
|
2018-03-03 15:44:21 +00:00
|
|
|
extension = dataSrc.substring(dotPos)
|
2017-11-09 17:10:10 +00:00
|
|
|
}
|
|
|
|
|
2018-04-11 13:06:08 +00:00
|
|
|
// Anime and character images on Retina displays
|
|
|
|
if(pixelRatio > 1) {
|
2018-11-22 10:30:34 +00:00
|
|
|
if(base.includes("/anime/") || base.includes("/groups/") || (base.includes("/characters/") && !base.includes("/large/"))) {
|
2018-04-11 13:06:08 +00:00
|
|
|
base += "@2"
|
|
|
|
}
|
2017-07-08 13:40:13 +00:00
|
|
|
}
|
2017-06-19 15:45:27 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const finalSrc = base + extension
|
2018-03-20 22:58:37 +00:00
|
|
|
|
2018-03-20 23:03:58 +00:00
|
|
|
if(element.src !== finalSrc && element.src !== "https:" + finalSrc && element.src !== "https://notify.moe" + finalSrc) {
|
2018-03-20 23:31:55 +00:00
|
|
|
// Show average color
|
|
|
|
if(element.dataset.color) {
|
2019-11-18 02:04:13 +00:00
|
|
|
element.src = emptyPixel
|
2018-03-20 23:31:55 +00:00
|
|
|
element.style.backgroundColor = element.dataset.color
|
2018-03-20 23:43:56 +00:00
|
|
|
Diff.mutations.queue(() => element.classList.add("element-color-preview"))
|
2018-03-20 23:31:55 +00:00
|
|
|
}
|
|
|
|
|
2018-03-20 23:43:56 +00:00
|
|
|
Diff.mutations.queue(() => element.classList.remove("element-found"))
|
2018-03-20 22:58:37 +00:00
|
|
|
element.src = finalSrc
|
|
|
|
}
|
2017-11-09 17:10:10 +00:00
|
|
|
|
2017-10-12 15:52:46 +00:00
|
|
|
if(element.naturalWidth === 0) {
|
|
|
|
element.onload = () => {
|
2018-03-20 22:58:37 +00:00
|
|
|
if(element.src.startsWith("data:")) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-03-20 23:43:56 +00:00
|
|
|
Diff.mutations.queue(() => element.classList.add("element-found"))
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
|
2017-10-12 15:52:46 +00:00
|
|
|
element.onerror = () => {
|
2019-08-30 01:10:31 +00:00
|
|
|
// Try loading from the origin server if our CDN failed
|
|
|
|
if(element.src.includes("media.notify.moe/")) {
|
|
|
|
console.warn(`CDN failed loading ${element.src}`)
|
|
|
|
element.src = element.src.replace("media.notify.moe/", "notify.moe/")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-03-20 20:22:16 +00:00
|
|
|
if(element.classList.contains("element-found")) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-03-20 23:43:56 +00:00
|
|
|
Diff.mutations.queue(() => element.classList.add("element-not-found"))
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
|
|
|
} else {
|
2018-03-20 23:43:56 +00:00
|
|
|
Diff.mutations.queue(() => element.classList.add("element-found"))
|
2017-10-12 15:52:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.visibilityObserver.observe(element)
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private lazyLoadIFrame(element: HTMLIFrameElement) {
|
2017-10-12 15:52:46 +00:00
|
|
|
// Once the iframe becomes visible, load it
|
|
|
|
element["became visible"] = () => {
|
2019-04-21 09:16:19 +00:00
|
|
|
if(!element.dataset.src) {
|
|
|
|
console.error("IFrame missing data-src attribute:", element)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-12 15:52:46 +00:00
|
|
|
// If the source is already set correctly, don't set it again to avoid iframe flickering.
|
2017-10-16 09:53:47 +00:00
|
|
|
if(element.src !== element.dataset.src && element.src !== (window.location.protocol + element.dataset.src)) {
|
2017-10-12 15:52:46 +00:00
|
|
|
element.src = element.dataset.src
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
2017-10-12 15:52:46 +00:00
|
|
|
|
2018-03-20 23:43:56 +00:00
|
|
|
Diff.mutations.queue(() => element.classList.add("element-found"))
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
2017-06-24 14:17:38 +00:00
|
|
|
|
2017-10-12 15:52:46 +00:00
|
|
|
this.visibilityObserver.observe(element)
|
2017-06-19 15:45:27 +00:00
|
|
|
}
|
2017-06-20 12:16:23 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private lazyLoadVideo(video: HTMLVideoElement) {
|
2018-12-07 19:40:27 +00:00
|
|
|
const hideControlsDelay = 1500
|
|
|
|
|
2018-04-15 09:14:11 +00:00
|
|
|
// Once the video becomes visible, load it
|
|
|
|
video["became visible"] = () => {
|
2018-12-07 22:02:22 +00:00
|
|
|
if(!video["listeners attached"]) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const videoParent = video.parentElement
|
2019-04-21 09:16:19 +00:00
|
|
|
|
|
|
|
if(!videoParent) {
|
|
|
|
console.error("video has no parent element")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-12-07 22:02:22 +00:00
|
|
|
// Prevent context menu
|
|
|
|
video.addEventListener("contextmenu", e => e.preventDefault())
|
2018-04-15 09:26:05 +00:00
|
|
|
|
2018-12-07 22:02:22 +00:00
|
|
|
// Show and hide controls on mouse movement
|
2019-11-17 09:25:14 +00:00
|
|
|
const controls = videoParent.getElementsByClassName("video-controls")[0]
|
|
|
|
const playButton = videoParent.getElementsByClassName("video-control-play")[0] as HTMLElement
|
|
|
|
const pauseButton = videoParent.getElementsByClassName("video-control-pause")[0] as HTMLElement
|
2018-12-07 19:40:27 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const hideControls = () => {
|
2018-12-07 22:02:22 +00:00
|
|
|
controls.classList.add("fade-out")
|
2018-12-08 06:01:07 +00:00
|
|
|
video.style.cursor = "none"
|
2018-12-07 22:02:22 +00:00
|
|
|
}
|
2018-12-07 19:40:27 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const showControls = () => {
|
2018-12-07 22:02:22 +00:00
|
|
|
controls.classList.remove("fade-out")
|
2018-12-08 06:01:07 +00:00
|
|
|
video.style.cursor = "default"
|
2018-12-07 22:02:22 +00:00
|
|
|
}
|
2018-12-07 19:40:27 +00:00
|
|
|
|
2018-12-07 22:02:22 +00:00
|
|
|
video.addEventListener("mousemove", () => {
|
|
|
|
showControls()
|
|
|
|
clearTimeout(video["hideControlsTimeout"])
|
|
|
|
video["hideControlsTimeout"] = setTimeout(hideControls, hideControlsDelay)
|
|
|
|
})
|
2018-12-07 19:40:27 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const progressElement = videoParent.getElementsByClassName("video-progress")[0] as HTMLElement
|
|
|
|
const progressClickable = videoParent.getElementsByClassName("video-progress-clickable")[0]
|
|
|
|
const timeElement = videoParent.getElementsByClassName("video-time")[0]
|
2018-12-07 21:01:06 +00:00
|
|
|
|
2018-12-07 22:02:22 +00:00
|
|
|
video.addEventListener("canplay", () => {
|
|
|
|
video["playable"] = true
|
|
|
|
})
|
2018-12-07 21:01:06 +00:00
|
|
|
|
2018-12-07 22:02:22 +00:00
|
|
|
video.addEventListener("timeupdate", () => {
|
|
|
|
if(!video["playable"]) {
|
|
|
|
return
|
|
|
|
}
|
2018-12-07 21:01:06 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const time = video.currentTime
|
|
|
|
const minutes = Math.trunc(time / 60)
|
|
|
|
const seconds = Math.trunc(time) % 60
|
|
|
|
const paddedSeconds = ("00" + seconds).slice(-2)
|
2018-12-07 22:12:41 +00:00
|
|
|
|
|
|
|
Diff.mutations.queue(() => {
|
|
|
|
timeElement.textContent = `${minutes}:${paddedSeconds}`
|
|
|
|
progressElement.style.transform = `scaleX(${time / video.duration})`
|
|
|
|
})
|
2018-12-07 22:02:22 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
video.addEventListener("waiting", () => {
|
|
|
|
this.loading(true)
|
|
|
|
})
|
|
|
|
|
|
|
|
video.addEventListener("playing", () => {
|
|
|
|
this.loading(false)
|
|
|
|
})
|
|
|
|
|
2018-12-09 02:57:37 +00:00
|
|
|
video.addEventListener("play", () => {
|
|
|
|
playButton.style.display = "none"
|
|
|
|
pauseButton.style.display = "block"
|
|
|
|
})
|
|
|
|
|
|
|
|
video.addEventListener("pause", () => {
|
|
|
|
playButton.style.display = "block"
|
|
|
|
pauseButton.style.display = "none"
|
|
|
|
})
|
|
|
|
|
2018-12-08 00:12:50 +00:00
|
|
|
progressClickable.addEventListener("click", (e: MouseEvent) => {
|
2019-11-17 09:25:14 +00:00
|
|
|
const rect = progressClickable.getBoundingClientRect()
|
|
|
|
const x = e.clientX
|
|
|
|
const progress = (x - rect.left) / rect.width
|
2018-12-07 22:02:22 +00:00
|
|
|
video.currentTime = progress * video.duration
|
|
|
|
video.dispatchEvent(new Event("timeupdate"))
|
|
|
|
e.stopPropagation()
|
|
|
|
})
|
|
|
|
|
|
|
|
video["listeners attached"] = true
|
|
|
|
}
|
2018-12-07 21:01:06 +00:00
|
|
|
|
2018-04-16 14:49:20 +00:00
|
|
|
let modified = false
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const child of video.children) {
|
2018-12-09 04:36:01 +00:00
|
|
|
if(child.tagName !== "SOURCE") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const element = child as HTMLSourceElement
|
2018-04-15 09:14:11 +00:00
|
|
|
|
2019-04-21 09:16:19 +00:00
|
|
|
if(!element.dataset.src || !element.dataset.type) {
|
|
|
|
console.error("Source element missing data-src or data-type attribute:", element)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-04-16 14:49:20 +00:00
|
|
|
if(element.src !== element.dataset.src) {
|
|
|
|
element.src = element.dataset.src
|
|
|
|
modified = true
|
|
|
|
}
|
2018-12-07 00:54:17 +00:00
|
|
|
|
|
|
|
if(element.type !== element.dataset.type) {
|
|
|
|
element.type = element.dataset.type
|
|
|
|
modified = true
|
|
|
|
}
|
2018-04-15 09:14:11 +00:00
|
|
|
}
|
|
|
|
|
2018-04-16 14:49:20 +00:00
|
|
|
if(modified) {
|
2018-12-07 22:02:22 +00:00
|
|
|
video["playable"] = false
|
|
|
|
|
2018-04-16 14:49:20 +00:00
|
|
|
Diff.mutations.queue(() => {
|
2018-12-07 21:01:06 +00:00
|
|
|
video.load()
|
2018-04-16 14:49:20 +00:00
|
|
|
video.classList.add("element-found")
|
|
|
|
})
|
|
|
|
}
|
2018-04-15 09:14:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.visibilityObserver.observe(video)
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private modifyDelayed(elements: IterableIterator<HTMLElement>, func: (element: HTMLElement) => void) {
|
2018-04-22 16:14:44 +00:00
|
|
|
const maxDelay = 2500
|
2019-11-18 02:04:13 +00:00
|
|
|
const delayTime = 20
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2017-09-22 04:19:32 +00:00
|
|
|
let time = 0
|
2019-11-17 09:25:14 +00:00
|
|
|
const start = Date.now()
|
|
|
|
const maxTime = start + maxDelay
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const mountableTypes = new Map<string, number>()
|
|
|
|
const mountableTypeMutations = new Map<string, any[]>()
|
2017-06-20 18:13:04 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
for(const element of elements) {
|
2018-03-14 19:35:00 +00:00
|
|
|
// Skip already mounted elements.
|
|
|
|
// This helps a lot when dealing with infinite scrolling
|
|
|
|
// where the first elements are already mounted.
|
|
|
|
if(element.classList.contains("mounted")) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const type = element.dataset.mountableType || "general"
|
|
|
|
const typeTime = mountableTypes.get(type)
|
2017-07-02 15:51:17 +00:00
|
|
|
|
2019-04-21 09:16:19 +00:00
|
|
|
if(typeTime !== undefined) {
|
2019-11-18 02:04:13 +00:00
|
|
|
time = typeTime + delayTime
|
2017-09-22 04:19:32 +00:00
|
|
|
mountableTypes.set(type, time)
|
|
|
|
} else {
|
|
|
|
time = start
|
|
|
|
mountableTypes.set(type, time)
|
|
|
|
mountableTypeMutations.set(type, [])
|
|
|
|
}
|
2017-06-20 18:13:04 +00:00
|
|
|
|
2017-09-22 04:19:32 +00:00
|
|
|
if(time > maxTime) {
|
|
|
|
time = maxTime
|
|
|
|
}
|
2017-07-06 01:24:56 +00:00
|
|
|
|
2019-04-21 09:16:19 +00:00
|
|
|
const mutations = mountableTypeMutations.get(type) as any[]
|
|
|
|
|
|
|
|
mutations.push({
|
2017-09-22 04:19:32 +00:00
|
|
|
element,
|
|
|
|
time
|
|
|
|
})
|
|
|
|
}
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2019-04-21 09:16:19 +00:00
|
|
|
for(const mutations of mountableTypeMutations.values()) {
|
2017-09-22 04:19:32 +00:00
|
|
|
let mutationIndex = 0
|
2017-07-20 12:26:43 +00:00
|
|
|
|
2019-11-17 09:25:14 +00:00
|
|
|
const updateBatch = () => {
|
|
|
|
const now = Date.now()
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2017-09-22 04:19:32 +00:00
|
|
|
for(; mutationIndex < mutations.length; mutationIndex++) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const mutation = mutations[mutationIndex]
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2017-09-22 04:19:32 +00:00
|
|
|
if(mutation.time > now) {
|
|
|
|
break
|
|
|
|
}
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2017-09-22 04:19:32 +00:00
|
|
|
func(mutation.element)
|
|
|
|
}
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2017-09-22 04:19:32 +00:00
|
|
|
if(mutationIndex < mutations.length) {
|
|
|
|
window.requestAnimationFrame(updateBatch)
|
|
|
|
}
|
|
|
|
}
|
2017-07-05 13:29:18 +00:00
|
|
|
|
2017-09-22 04:19:32 +00:00
|
|
|
window.requestAnimationFrame(updateBatch)
|
|
|
|
}
|
|
|
|
}
|
2017-06-20 18:13:04 +00:00
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private onPopState(e: PopStateEvent) {
|
2017-06-21 12:00:52 +00:00
|
|
|
if(e.state) {
|
|
|
|
this.app.load(e.state, {
|
|
|
|
addToHistory: false
|
|
|
|
})
|
|
|
|
} else if(this.app.currentPath !== this.app.originalPath) {
|
|
|
|
this.app.load(this.app.originalPath, {
|
|
|
|
addToHistory: false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-18 02:04:13 +00:00
|
|
|
private onKeyDown(e: KeyboardEvent) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const activeElement = document.activeElement
|
2017-06-30 21:52:42 +00:00
|
|
|
|
2019-04-19 13:12:33 +00:00
|
|
|
if(!activeElement) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-06-24 14:41:22 +00:00
|
|
|
// Ignore hotkeys on input elements
|
2017-06-30 21:52:42 +00:00
|
|
|
switch(activeElement.tagName) {
|
2017-06-24 14:41:22 +00:00
|
|
|
case "INPUT":
|
2018-04-20 12:33:11 +00:00
|
|
|
// If the active element is the search and we press Enter, re-activate search.
|
2018-03-22 00:32:04 +00:00
|
|
|
if(activeElement.id === "search" && e.keyCode === 13) {
|
2018-03-24 22:04:31 +00:00
|
|
|
actions.search(this, activeElement as HTMLInputElement, e)
|
2018-03-22 00:32:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
|
2017-06-24 14:41:22 +00:00
|
|
|
case "TEXTAREA":
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-04-20 12:33:11 +00:00
|
|
|
// When called, this will prevent the default action for that key.
|
2019-11-17 09:25:14 +00:00
|
|
|
const preventDefault = () => {
|
2018-04-20 12:33:11 +00:00
|
|
|
e.preventDefault()
|
|
|
|
e.stopPropagation()
|
|
|
|
}
|
|
|
|
|
2017-10-12 15:52:46 +00:00
|
|
|
// Ignore hotkeys on contentEditable elements
|
|
|
|
if(activeElement.getAttribute("contenteditable") === "true") {
|
|
|
|
// Disallow Enter key in contenteditables and make it blur the element instead
|
2018-03-22 00:32:04 +00:00
|
|
|
if(e.keyCode === 13) {
|
2017-10-12 15:52:46 +00:00
|
|
|
if("blur" in activeElement) {
|
2019-11-18 02:04:13 +00:00
|
|
|
(activeElement["blur"] as () => void)()
|
2017-10-12 15:52:46 +00:00
|
|
|
}
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2018-04-20 12:33:11 +00:00
|
|
|
return preventDefault()
|
2017-06-30 21:52:42 +00:00
|
|
|
}
|
2017-11-09 16:18:15 +00:00
|
|
|
|
2017-06-30 21:52:42 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-03-16 16:34:29 +00:00
|
|
|
// "Ctrl" + "," = Settings
|
2018-03-22 00:32:04 +00:00
|
|
|
if(e.ctrlKey && e.keyCode === 188) {
|
2018-03-16 16:34:29 +00:00
|
|
|
this.app.load("/settings")
|
2018-04-20 12:33:11 +00:00
|
|
|
return preventDefault()
|
2018-03-16 16:34:29 +00:00
|
|
|
}
|
|
|
|
|
2018-04-20 12:33:11 +00:00
|
|
|
// The following keycodes should not be activated while Ctrl or Alt is held down
|
|
|
|
if(e.ctrlKey || e.altKey) {
|
2018-03-16 16:34:29 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-03-07 13:52:56 +00:00
|
|
|
// "F" = Search
|
2018-03-22 00:32:04 +00:00
|
|
|
if(e.keyCode === 70) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const search = document.getElementById("search") as HTMLInputElement
|
2017-06-21 12:00:52 +00:00
|
|
|
|
|
|
|
search.focus()
|
|
|
|
search.select()
|
2018-04-20 12:33:11 +00:00
|
|
|
return preventDefault()
|
2017-06-21 12:00:52 +00:00
|
|
|
}
|
2017-10-17 16:16:44 +00:00
|
|
|
|
2018-03-07 13:52:56 +00:00
|
|
|
// "S" = Toggle sidebar
|
2018-03-22 00:32:04 +00:00
|
|
|
if(e.keyCode === 83) {
|
2018-03-07 13:52:56 +00:00
|
|
|
this.sideBar.toggle()
|
2018-04-20 12:33:11 +00:00
|
|
|
return preventDefault()
|
2018-03-07 13:52:56 +00:00
|
|
|
}
|
|
|
|
|
2018-03-16 16:03:59 +00:00
|
|
|
// "+" = Audio speed up
|
2019-11-18 02:04:13 +00:00
|
|
|
if(e.key === "+") {
|
2018-03-24 22:04:31 +00:00
|
|
|
this.audioPlayer.addSpeed(0.05)
|
2018-04-20 12:33:11 +00:00
|
|
|
return preventDefault()
|
2018-03-16 16:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// "-" = Audio speed down
|
2019-11-18 02:04:13 +00:00
|
|
|
if(e.key === "-") {
|
2018-03-24 22:04:31 +00:00
|
|
|
this.audioPlayer.addSpeed(-0.05)
|
2018-04-20 12:33:11 +00:00
|
|
|
return preventDefault()
|
2018-03-16 16:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// "J" = Previous track
|
2018-03-22 00:32:04 +00:00
|
|
|
if(e.keyCode === 74) {
|
2018-03-24 22:04:31 +00:00
|
|
|
this.audioPlayer.previous()
|
2018-04-20 12:33:11 +00:00
|
|
|
return preventDefault()
|
2018-03-16 16:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// "K" = Play/pause
|
2018-03-22 00:32:04 +00:00
|
|
|
if(e.keyCode === 75) {
|
2018-03-24 22:04:31 +00:00
|
|
|
this.audioPlayer.playPause()
|
2018-04-20 12:33:11 +00:00
|
|
|
return preventDefault()
|
2018-03-16 16:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// "L" = Next track
|
2018-03-22 00:32:04 +00:00
|
|
|
if(e.keyCode === 76) {
|
2018-03-24 22:04:31 +00:00
|
|
|
this.audioPlayer.next()
|
2018-04-20 12:33:11 +00:00
|
|
|
return preventDefault()
|
|
|
|
}
|
2018-03-16 16:03:59 +00:00
|
|
|
|
2018-12-09 03:37:54 +00:00
|
|
|
// Space = Toggle play
|
|
|
|
if(e.keyCode === 32) {
|
|
|
|
// this.audioPlayer.playPause()
|
|
|
|
this.videoPlayer.playPause()
|
|
|
|
return preventDefault()
|
|
|
|
}
|
|
|
|
|
2018-04-20 12:33:11 +00:00
|
|
|
// Number keys activate sidebar menus
|
|
|
|
for(let i = 48; i <= 57; i++) {
|
|
|
|
if(e.keyCode === i) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const index = i === 48 ? 9 : i - 49
|
|
|
|
const links = [...findAll("sidebar-link")]
|
2018-04-20 12:33:11 +00:00
|
|
|
|
|
|
|
if(index < links.length) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const element = links[index] as HTMLElement
|
2018-04-20 12:33:11 +00:00
|
|
|
|
|
|
|
element.click()
|
|
|
|
return preventDefault()
|
|
|
|
}
|
|
|
|
}
|
2018-03-16 16:03:59 +00:00
|
|
|
}
|
2017-06-21 12:00:52 +00:00
|
|
|
}
|
2018-04-17 16:04:16 +00:00
|
|
|
|
|
|
|
// This is called every time an uncaught JavaScript error is thrown
|
2019-11-18 02:04:13 +00:00
|
|
|
private async onError(evt: ErrorEvent) {
|
2019-11-17 09:25:14 +00:00
|
|
|
const report = {
|
2018-04-17 16:04:16 +00:00
|
|
|
message: evt.message,
|
|
|
|
stack: evt.error.stack,
|
|
|
|
fileName: evt.filename,
|
|
|
|
lineNumber: evt.lineno,
|
|
|
|
columnNumber: evt.colno,
|
|
|
|
}
|
|
|
|
|
2019-04-22 06:59:08 +00:00
|
|
|
try {
|
|
|
|
await this.post("/api/new/clienterrorreport", report)
|
|
|
|
console.log("Successfully reported the error to the website staff.")
|
|
|
|
} catch(err) {
|
|
|
|
console.warn("Failed reporting the error to the website staff:", err)
|
|
|
|
}
|
2018-04-17 16:04:16 +00:00
|
|
|
}
|
2019-11-17 09:09:39 +00:00
|
|
|
}
|