Refactor scripts

This commit is contained in:
Eduard Urbach 2019-11-18 11:04:13 +09:00
parent 7e25ee6faf
commit 1ddcd4d570
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
33 changed files with 670 additions and 749 deletions

View File

@ -1,5 +1,5 @@
import requestIdleCallback from "scripts/Utils/requestIdleCallback"
import AnimeNotifier from "../AnimeNotifier"
import { requestIdleCallback } from "../Utils"
// Load
export function load(arn: AnimeNotifier, element: HTMLElement) {

View File

@ -1,5 +1,6 @@
import emptyPixel from "scripts/Utils/emptyPixel"
import findAll from "scripts/Utils/findAll"
import AnimeNotifier from "../AnimeNotifier"
import { findAll } from "scripts/Utils"
// Filter anime on explore page
export function filterAnime(arn: AnimeNotifier, _: HTMLInputElement) {
@ -12,7 +13,7 @@ export function filterAnime(arn: AnimeNotifier, _: HTMLInputElement) {
for(const element of findAll("anime-grid-image")) {
const img = element as HTMLImageElement
img.src = arn.emptyPixel()
img.src = emptyPixel
img.classList.remove("element-found")
img.classList.remove("element-color-preview")
}

View File

@ -2,24 +2,24 @@ import AnimeNotifier from "../AnimeNotifier"
// Enable notifications
export async function enableNotifications(arn: AnimeNotifier, _: HTMLElement) {
if(!arn.user || !arn.user.dataset.id) {
if(!arn.user) {
return
}
arn.statusMessage.showInfo("Enabling instant notifications...")
await arn.pushManager.subscribe(arn.user.dataset.id)
await arn.pushManager.subscribe(arn.user.id)
arn.updatePushUI()
arn.statusMessage.showInfo("Enabled instant notifications for this device.")
}
// Disable notifications
export async function disableNotifications(arn: AnimeNotifier, _: HTMLElement) {
if(!arn.user || !arn.user.dataset.id) {
if(!arn.user) {
return
}
arn.statusMessage.showInfo("Disabling instant notifications...")
await arn.pushManager.unsubscribe(arn.user.dataset.id)
await arn.pushManager.unsubscribe(arn.user.id)
arn.updatePushUI()
arn.statusMessage.showInfo("Disabled instant notifications for this device.")
}

View File

@ -1,6 +1,7 @@
import Diff from "scripts/Diff"
import delay from "scripts/Utils/delay"
import requestIdleCallback from "scripts/Utils/requestIdleCallback"
import AnimeNotifier from "../AnimeNotifier"
import { delay, requestIdleCallback } from "../Utils"
import Diff from "scripts/Diff";
// Search page reference
let emptySearchHTML = ""
@ -111,7 +112,7 @@ export async function search(arn: AnimeNotifier, search: HTMLInputElement, evt?:
searchPageTitle.textContent = document.title
if(!term || term.length < 1) {
await arn.innerHTML(searchPage, emptySearchHTML)
await Diff.innerHTML(searchPage, emptySearchHTML)
arn.app.emit("DOMContentLoaded")
return
}
@ -168,7 +169,7 @@ function showResponseInElement(arn: AnimeNotifier, url: string, typeName: string
correctResponseRendered[typeName] = true
}
await arn.innerHTML(element, html)
await Diff.innerHTML(element, html)
arn.onNewContent(element)
}
}

View File

@ -1,5 +1,5 @@
import hexToHSL from "scripts/Utils/hexToHSL"
import AnimeNotifier from "../AnimeNotifier"
import { hexToHSL } from "scripts/Utils"
let currentThemeName = "light"
let previewTimeoutID: number = 0
@ -7,8 +7,8 @@ let previewTimeoutID: number = 0
// let themeWheelTimeoutID: number = 0
const themes = {
"light": {},
"dark": {
light: {},
dark: {
"link-color-h": "45",
"link-color-s": "100%",
"link-color-l": "66%",
@ -117,7 +117,7 @@ export function applyThemeAndPreview(arn: AnimeNotifier, themeName: string) {
clearTimeout(previewTimeoutID)
// If it's the free light theme or a PRO user, nothing to do here
if(currentThemeName === "light" || (arn.user && arn.user.dataset.pro == "true")) {
if(currentThemeName === "light" || (arn.user && arn.user.IsPro())) {
return
}

View File

@ -1,12 +1,13 @@
import bytesHumanReadable from "scripts/Utils/bytesHumanReadable"
import uploadWithProgress from "scripts/Utils/uploadWithProgress"
import AnimeNotifier from "../AnimeNotifier"
import { bytesHumanReadable, uploadWithProgress } from "../Utils"
// Select file
export function selectFile(arn: AnimeNotifier, button: HTMLButtonElement) {
const fileType = button.dataset.type
const endpoint = button.dataset.endpoint
if(endpoint === "/api/upload/user/cover" && arn.user && arn.user.dataset.pro !== "true") {
if(endpoint === "/api/upload/user/cover" && arn.user && !arn.user.IsPro()) {
alert("Please buy a PRO account to use this feature.")
return
}

View File

@ -1,41 +1,39 @@
export default class Analytics {
push() {
const analytics = {
general: {
timezoneOffset: new Date().getTimezoneOffset()
},
screen: {
width: screen.width,
height: screen.height,
availableWidth: screen.availWidth,
availableHeight: screen.availHeight,
pixelRatio: window.devicePixelRatio
},
system: {
cpuCount: navigator.hardwareConcurrency,
platform: navigator.platform
},
connection: {
downLink: 0,
roundTripTime: 0,
effectiveType: ""
}
export function uploadAnalytics() {
const analytics = {
general: {
timezoneOffset: new Date().getTimezoneOffset()
},
screen: {
availableHeight: screen.availHeight,
availableWidth: screen.availWidth,
height: screen.height,
pixelRatio: window.devicePixelRatio,
width: screen.width
},
system: {
cpuCount: navigator.hardwareConcurrency,
platform: navigator.platform
},
connection: {
downLink: 0,
effectiveType: "",
roundTripTime: 0
}
if("connection" in navigator) {
const connection = navigator["connection"] as any
analytics.connection = {
downLink: connection.downlink,
roundTripTime: connection.rtt,
effectiveType: connection.effectiveType
}
}
fetch("/dark-flame-master", {
method: "POST",
credentials: "same-origin",
body: JSON.stringify(analytics)
})
}
if("connection" in navigator) {
const connection = navigator["connection"] as any
analytics.connection = {
downLink: connection.downlink,
roundTripTime: connection.rtt,
effectiveType: connection.effectiveType
}
}
fetch("/dark-flame-master", {
method: "POST",
credentials: "same-origin",
body: JSON.stringify(analytics)
})
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import Diff from "./Diff"
import LoadOptions from "./LoadOptions"
import { delay } from "./Utils"
import delay from "./Utils/delay"
export default class Application {
public originalPath: string

View File

@ -1,4 +1,4 @@
import { plural } from "./Utils"
import plural from "./Utils/plural"
const oneSecond = 1000
const oneMinute = 60 * oneSecond

View File

@ -1,39 +0,0 @@
import Diff from "./Diff"
export default class InfiniteScroller {
container: HTMLElement
threshold: number
constructor(container, threshold) {
this.container = container
this.threshold = threshold
const check = () => {
if(this.container.scrollTop + this.container.clientHeight >= this.container.scrollHeight - threshold) {
this.loadMore()
}
}
this.container.addEventListener("scroll", _ => {
// Wait for mutations to finish before checking if we need infinite scroll to trigger.
if(Diff.mutations.mutations.length > 0) {
Diff.mutations.wait(() => check())
return
}
// Otherwise, queue up the check immediately.
// Don't call check() directly to make scrolling as smooth as possible.
Diff.mutations.queue(check)
})
}
loadMore() {
const button = document.getElementById("load-more-button")
if(!button) {
return
}
button.click()
}
}

View File

@ -9,15 +9,15 @@ const timeCapacity = 6.5
// It checks the time used to process these mutations and if the time is over the
// defined time capacity, it will pause and continue the mutations in the next frame.
export default class MutationQueue {
mutations: Array<() => void>
onClearCallBacks: Array<() => void>
private mutations: Array<() => void>
private onClearCallBacks: Array<() => void>
constructor() {
this.mutations = []
this.onClearCallBacks = []
}
queue(mutation: () => void) {
public queue(mutation: () => void) {
this.mutations.push(mutation)
if(this.mutations.length === 1) {
@ -25,7 +25,20 @@ export default class MutationQueue {
}
}
mutateAll() {
public wait(callBack: () => void) {
if(this.mutations.length === 0) {
callBack()
return
}
this.onClearCallBacks.push(callBack)
}
public length() {
return this.mutations.length
}
private mutateAll() {
const start = performance.now()
for(let i = 0; i < this.mutations.length; i++) {
@ -45,7 +58,7 @@ export default class MutationQueue {
this.clear()
}
clear() {
private clear() {
this.mutations.length = 0
if(this.onClearCallBacks.length > 0) {
@ -56,13 +69,4 @@ export default class MutationQueue {
this.onClearCallBacks.length = 0
}
}
wait(callBack: () => void) {
if(this.mutations.length === 0) {
callBack()
return
}
this.onClearCallBacks.push(callBack)
}
}

View File

@ -0,0 +1,3 @@
export default class ServerEvent {
public data: string
}

View File

@ -0,0 +1,95 @@
import AnimeNotifier from "../AnimeNotifier"
import plural from "../Utils/plural"
import ServerEvent from "./ServerEvent"
const reconnectDelay = 3000
let supported: boolean
let eventSource: EventSource
let arn: AnimeNotifier
let etags: Map<string, string>
export default function receiveServerEvents(animeNotifier: AnimeNotifier) {
supported = ("EventSource" in window)
if(!supported) {
return
}
arn = animeNotifier
etags = new Map<string, string>()
connect()
}
function connect() {
if(eventSource) {
eventSource.close()
}
eventSource = new EventSource("/api/sse/events", {
withCredentials: true
})
eventSource.addEventListener("ping", (e: any) => ping(e))
eventSource.addEventListener("etag", (e: any) => etag(e))
eventSource.addEventListener("activity", (e: any) => activity(e))
eventSource.addEventListener("notificationCount", (e: any) => notificationCount(e))
eventSource.onerror = () => {
setTimeout(() => connect(), reconnectDelay)
}
}
function ping(_: ServerEvent) {
console.log("ping")
}
function etag(e: ServerEvent) {
const data = JSON.parse(e.data)
const oldETag = etags.get(data.url)
const newETag = data.etag
if(oldETag && newETag && oldETag !== newETag) {
arn.statusMessage.showInfo("A new version of the website is available. Please refresh the page.", -1)
}
etags.set(data.url, newETag)
}
function activity(e: ServerEvent) {
if(!location.pathname.startsWith("/activity")) {
return
}
const isFollowingUser = JSON.parse(e.data)
// If we're on the followed only feed and we receive an activity
// about a user we don't follow, ignore the message.
if(location.pathname.startsWith("/activity/followed") && !isFollowingUser) {
return
}
const button = document.getElementById("load-new-activities")
if(!button || !button.dataset.count) {
return
}
const buttonText = document.getElementById("load-new-activities-text")
if(!buttonText) {
return
}
const newCount = parseInt(button.dataset.count, 10) + 1
button.dataset.count = newCount.toString()
buttonText.textContent = plural(newCount, "new activity")
}
function notificationCount(e: ServerEvent) {
if(!arn.notificationManager) {
return
}
arn.notificationManager.setCounter(parseInt(e.data, 10))
}

View File

@ -1,100 +0,0 @@
import AnimeNotifier from "./AnimeNotifier"
import { plural } from "./Utils"
const reconnectDelay = 3000
class ServerEvent {
data: string
}
export default class ServerEvents {
supported: boolean
eventSource: EventSource
arn: AnimeNotifier
etags: Map<string, string>
constructor(arn: AnimeNotifier) {
this.supported = ("EventSource" in window)
if(!this.supported) {
return
}
this.arn = arn
this.etags = new Map<string, string>()
this.connect()
}
connect() {
if(this.eventSource) {
this.eventSource.close()
}
this.eventSource = new EventSource("/api/sse/events", {
withCredentials: true
})
this.eventSource.addEventListener("ping", (e: any) => this.ping(e))
this.eventSource.addEventListener("etag", (e: any) => this.etag(e))
this.eventSource.addEventListener("activity", (e: any) => this.activity(e))
this.eventSource.addEventListener("notificationCount", (e: any) => this.notificationCount(e))
this.eventSource.onerror = () => {
setTimeout(() => this.connect(), reconnectDelay)
}
}
ping(_: ServerEvent) {
console.log("ping")
}
etag(e: ServerEvent) {
const data = JSON.parse(e.data)
const oldETag = this.etags.get(data.url)
const newETag = data.etag
if(oldETag && newETag && oldETag != newETag) {
this.arn.statusMessage.showInfo("A new version of the website is available. Please refresh the page.", -1)
}
this.etags.set(data.url, newETag)
}
activity(e: ServerEvent) {
if(!location.pathname.startsWith("/activity")) {
return
}
const isFollowingUser = JSON.parse(e.data)
// If we're on the followed only feed and we receive an activity
// about a user we don't follow, ignore the message.
if(location.pathname.startsWith("/activity/followed") && !isFollowingUser) {
return
}
const button = document.getElementById("load-new-activities")
if(!button || !button.dataset.count) {
return
}
const buttonText = document.getElementById("load-new-activities-text")
if(!buttonText) {
return
}
const newCount = parseInt(button.dataset.count) + 1
button.dataset.count = newCount.toString()
buttonText.textContent = plural(newCount, "new activity")
}
notificationCount(e: ServerEvent) {
if(!this.arn.notificationManager) {
return
}
this.arn.notificationManager.setCounter(parseInt(e.data))
}
}

View File

@ -1,66 +1,25 @@
import AnimeNotifier from "./AnimeNotifier"
export default class ServiceWorkerManager {
arn: AnimeNotifier
uri: string
private arn: AnimeNotifier
private uri: string
constructor(arn: AnimeNotifier, uri: string) {
this.arn = arn
this.uri = uri
}
register() {
public register() {
if(!("serviceWorker" in navigator)) {
console.warn("service worker not supported, skipping registration")
return
}
navigator.serviceWorker.register(this.uri)
navigator.serviceWorker.addEventListener("message", evt => {
this.onMessage(evt)
})
// This will send a message to the service worker that the DOM has been loaded
const sendContentLoadedEvent = () => {
if(!navigator.serviceWorker.controller) {
return
}
// A reloadContent call should never trigger another reload
if(this.arn.app.currentPath === this.arn.lastReloadContentPath) {
this.arn.lastReloadContentPath = ""
return
}
let url = ""
// If mainPageLoaded is set, it means every single request is now an AJAX request for the /_/ prefixed page
if(this.arn.mainPageLoaded) {
url = window.location.origin + "/_" + window.location.pathname
} else {
this.arn.mainPageLoaded = true
url = window.location.href
}
// console.log("checking for updates:", message.url)
this.postMessage({
type: "loaded",
url
})
}
// For future loaded events
document.addEventListener("DOMContentLoaded", sendContentLoadedEvent)
// If the page is loaded already, send the loaded event right now.
if(document.readyState !== "loading") {
sendContentLoadedEvent()
}
navigator.serviceWorker.addEventListener("message", evt => this.onMessage(evt))
}
postMessage(message: any) {
public postMessage(message: any) {
const controller = navigator.serviceWorker.controller
if(!controller) {
@ -70,7 +29,7 @@ export default class ServiceWorkerManager {
controller.postMessage(JSON.stringify(message))
}
onMessage(evt: MessageEvent) {
private onMessage(evt: MessageEvent) {
const message = JSON.parse(evt.data)
switch(message.type) {
@ -80,25 +39,6 @@ export default class ServiceWorkerManager {
}
break
// case "new content":
// if(message.url.includes("/_/")) {
// // Content reload
// this.arn.contentLoadedActions.then(() => {
// this.arn.reloadContent(true)
// })
// } else {
// // Full page reload
// this.arn.contentLoadedActions.then(() => {
// this.arn.reloadPage()
// })
// }
// break
// case "offline":
// this.arn.statusMessage.showError("You are viewing an offline version of the site now.")
// break
}
}
}

View File

@ -1,4 +1,4 @@
import { delay } from "./Utils"
import delay from "./Utils/delay"
export default class StatusMessage {
private container: HTMLElement

19
scripts/User.ts Normal file
View File

@ -0,0 +1,19 @@
export default class User {
public id: string
private proExpires: string
public constructor(id: string) {
this.id = id
this.sync()
}
public IsPro(): boolean {
return new Date() > new Date(this.proExpires)
}
private async sync() {
const response = await fetch(`/api/user/${this.id}`)
const json = await response.json()
Object.assign(this, json)
}
}

View File

@ -1,4 +1,4 @@
export function bytesHumanReadable(fileSize: number): string {
export default function bytesHumanReadable(fileSize: number): string {
let unit = "bytes"
if(fileSize >= 1024) {

View File

@ -1,3 +1,3 @@
export function delay<T>(millis: number, value?: T): Promise<T> {
export default function delay<T>(millis: number, value?: T): Promise<T> {
return new Promise(resolve => setTimeout(() => resolve(value), millis))
}

View File

@ -0,0 +1 @@
export default "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="

View File

@ -1,15 +1,7 @@
export function* findAll(className: string): IterableIterator<HTMLElement> {
export default function* findAll(className: string): IterableIterator<HTMLElement> {
const elements = document.getElementsByClassName(className)
for(let i = 0; i < elements.length; ++i) {
yield elements[i] as HTMLElement
}
}
export function* findAllInside(className: string, root: HTMLElement): IterableIterator<HTMLElement> {
const elements = root.getElementsByClassName(className)
for(let i = 0; i < elements.length; ++i) {
yield elements[i] as HTMLElement
for(const element of elements) {
yield element as HTMLElement
}
}

View File

@ -0,0 +1,7 @@
export default function* findAllInside(className: string, root: HTMLElement): IterableIterator<HTMLElement> {
const elements = root.getElementsByClassName(className)
for(const element of elements) {
yield element as HTMLElement
}
}

View File

@ -1,4 +1,4 @@
export function hexToHSL(hex: string) {
export default function hexToHSL(hex: string) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
if(!result) {
@ -20,7 +20,7 @@ export function hexToHSL(hex: string) {
let s = 0
const l = (max + min) / 2
if(max == min) {
if(max === min) {
h = s = 0
} else {
const d = max - min

View File

@ -1,9 +0,0 @@
export * from "./supportsWebP"
export * from "./delay"
export * from "./findAll"
export * from "./hexToHSL"
export * from "./plural"
export * from "./requestIdleCallback"
export * from "./swapElements"
export * from "./uploadWithProgress"
export * from "./bytesHumanReadable"

View File

@ -2,7 +2,7 @@ const specialized = {
"new activity": "new activities"
}
export function plural(count: number, singular: string): string {
export default function plural(count: number, singular: string): string {
if(count === 1 || count === -1) {
return count + " " + singular
}

View File

@ -1,4 +1,4 @@
export function requestIdleCallback(func: Function) {
export default function requestIdleCallback(func: Function) {
if("requestIdleCallback" in window) {
(window["requestIdleCallback"] as Function)(func)
} else {

View File

@ -1,4 +1,4 @@
export async function supportsWebP(): Promise<boolean> {
export default async function supportsWebP(): Promise<boolean> {
if(!window.createImageBitmap) {
return false
}

View File

@ -1,5 +1,5 @@
// swapElements assumes that both elements have valid parent nodes.
export function swapElements(a: Node, b: Node) {
export default function swapElements(a: Node, b: Node) {
const bParent = b.parentNode as Node
const bNext = b.nextSibling

View File

@ -1,4 +1,4 @@
export function uploadWithProgress(url, options: RequestInit, onProgress: ((ev: ProgressEvent) => any) | null): Promise<string> {
export default function uploadWithProgress(url, options: RequestInit, onProgress: ((ev: ProgressEvent) => any) | null): Promise<string> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()

37
scripts/infiniteScroll.ts Normal file
View File

@ -0,0 +1,37 @@
import Diff from "./Diff"
let container: HTMLElement
let threshold: number
export default function infiniteScroll(scrollContainer: HTMLElement, scrollThreshold: number) {
container = scrollContainer
threshold = scrollThreshold
container.addEventListener("scroll", _ => {
// Wait for mutations to finish before checking if we need infinite scroll to trigger.
if(Diff.mutations.length() > 0) {
Diff.mutations.wait(check)
return
}
// Otherwise, queue up the check immediately.
// Don't call check() directly to make scrolling as smooth as possible.
Diff.mutations.queue(() => check())
})
}
function check() {
if(container.scrollTop + container.clientHeight >= container.scrollHeight - threshold) {
loadMore()
}
}
function loadMore() {
const button = document.getElementById("load-more-button")
if(!button) {
return
}
button.click()
}

View File

@ -1,7 +1,3 @@
import AnimeNotifier from "./AnimeNotifier"
import Application from "./Application"
const app = new Application()
const arn = new AnimeNotifier(app)
arn.init()
new AnimeNotifier().init()

View File

@ -12,7 +12,12 @@
"trailing-comma": false,
"prefer-const": true,
"no-var-keyword": true,
"eofline": true
"eofline": true,
"no-console": false,
"object-literal-sort-keys": false,
"no-string-literal": false,
"max-line-length": false,
"no-string-throw": false
},
"rulesDirectory": []
}