add contextmenu

This commit is contained in:
polv committed 2021-10-24 18:16:46 +07:00
1 parent 1c53acbe2c
commit f0bd330d04
18 files changed
+912 -131

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
import _axios, { AxiosInstance } from 'axios'
class Api {
axios: AxiosInstance
_isAuthorized = true
_user = ''
constructor() {
this.axios = _axios.create()
}
async setWanikaniApiKey(apiKey?: string) {
const setApiKey = () => {
if (apiKey) {
this.axios.defaults.headers.common.Authorization = `Bearer ${apiKey}`
} else {
delete this.axios.defaults.headers.common.Authorization
}
}
setApiKey()
const r = await this.user_getSettings()
if (r) return r
apiKey = ''
setApiKey()
return null
}
async isAuthorized(): Promise<boolean> {
if (this._isAuthorized && !this._user) {
const r = await this.user_getSettings()
if (r) {
this._user = r.identifier
this._isAuthorized = true
}
}
return this._isAuthorized
}
async user_getSettings() {
const r = await this.axios
.get('/api/user', {
params: {
select: 'level,levelMin,identifier',
},
})
.then(
(r) =>
r.data as {
level: number
levelMin?: number
identifier: string
}
)
.then((r) => ({
...r,
levelMin: r.levelMin || 1,
}))
.catch(() => null)
if (!r) {
return
}
return r
}
async user_updateSettings(u: { levelMin: number }) {
return await this.axios.patch('/api/user', u)
}
async quiz_getMany(params: {
entries: string[]
select: string[]
type?: string
direction?: string
}) {
return await this.axios.post('/api/quiz/getMany', params).then(
(r) =>
r.data as {
result: {
id: string
entry: string
}[]
}
)
}
async quiz_create(params: { entries: string[]; type: string }) {
return await this.axios.put('/api/quiz', params).then(
(r) =>
r.data as {
result: {
ids: string[]
}[]
}
)
}
async quiz_deleteMany(params: { entries: string[]; type: string }) {
return await this.axios.post('/api/quiz/deleteMany', params)
}
async extra_delete(params: { id: string }) {
return await this.axios.delete('/extra/delete', {
params,
})
}
}
export const api = new Api()
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

+82
View File
@@ -0,0 +1,82 @@
const allVoices: Record<string, string> = {}
// eslint-disable-next-line array-callback-return
speechSynthesis.getVoices().map((v) => {
allVoices[v.lang] = v.lang
})
speechSynthesis.onvoiceschanged = () => {
// eslint-disable-next-line array-callback-return
speechSynthesis.getVoices().map((v) => {
allVoices[v.lang] = v.lang
})
}
export async function speak(
s: string,
opts: {
lang?: string
forceOffline?: boolean
} = {}
) {
if (!opts.forceOffline) {
const audio = new Audio(
`/api/tts?q=${encodeURIComponent(s)}&lang=${encodeURIComponent(
opts.lang || 'ja'
)}`
)
await audio.play().catch(() =>
speak(s, {
...opts,
forceOffline: true,
})
)
return
}
let lang = 'zh'
const voices = Object.keys(allVoices)
const stage1 = () => voices.filter((v) => v === lang)[0]
const stage2 = () => {
const m1 = lang.substr(0, 2)
const m2 = lang.substr(3, 2)
const r1 = new RegExp(`^${m1}[-_]${m2}`, 'i')
return voices.filter((v) => r1.test(v))[0]
}
const stage3 = () => {
const m1 = lang.substr(0, 2).toLocaleLowerCase()
return voices.filter((v) => v.toLocaleLowerCase().startsWith(m1))[0]
}
lang = stage1() || stage2() || stage3() || ''
if (lang) {
const utterance = new SpeechSynthesisUtterance(s)
utterance.lang = lang
speechSynthesis.speak(utterance)
return new Promise<void>((resolve) => {
utterance.onend = () => {
resolve()
}
})
}
}
window.addEventListener('keydown', (ev) => {
if (
ev.target instanceof HTMLElement &&
['INPUT', 'TEXTAREA'].includes(ev.target.tagName.toLocaleUpperCase())
) {
return
}
if (ev.key === 's') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const s = window.getSelection()!.toString()
if (s) {
speak(s)
}
}
})
+76
View File
@@ -0,0 +1,76 @@
export const doClick = async (el?: Element | null, timeout?: number) => {
if (!el) return
if (el.classList?.add) {
el.classList.add('active')
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { click } = el as any
if (typeof click === 'function') {
const r = click()
if (r instanceof Promise) {
await r
} else if (timeout) {
await new Promise((resolve) => setTimeout(resolve, timeout))
} else {
return
}
}
if (el.classList?.remove) {
el.classList.remove('active')
}
}
export const doMapKeypress = (
evt: KeyboardEvent,
map: Record<string, (() => void) | string | HTMLElement>
) => {
let action = map[evt.key]
while (typeof action === 'string') {
action = map[action]
}
if (typeof action === 'function') {
action()
} else if (action instanceof HTMLElement) {
doClick(action)
}
}
/**
* Shuffles array in place. ES6 version
* @param {Array} a items An array containing the items.
*/
export function shuffle<T>(a: T[]) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[a[i], a[j]] = [a[j], a[i]]
}
return a
}
export function sample<T>(arr: T[], size: number): T[] {
const allN = Array(arr.length)
.fill(null)
.map((_, i) => i)
const outN: number[] = []
while (allN.length && outN.length < size) {
outN.push(...allN.splice(Math.floor(Math.random() * allN.length), 1))
}
return outN.map((n) => arr[n])
}
export function capitalize(s: string): string {
if (s.includes(' ')) {
return s
.split(' ')
.map((s0) => capitalize(s0))
.join(' ')
}
if (!s) return s
return s[0].toLocaleUpperCase() + s.substr(1)
}