mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-20 20:01:17 +00:00
Add toasts to communicate with user
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
529f08db18
commit
e81ffd5acf
8 changed files with 191 additions and 21 deletions
|
@ -1,14 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a
|
<span class="navbar-brand">
|
||||||
class="navbar-brand"
|
|
||||||
href="#"
|
|
||||||
@click.prevent
|
|
||||||
>
|
|
||||||
<i class="fas fa-robot fa-fw me-1 text-info" />
|
<i class="fas fa-robot fa-fw me-1 text-info" />
|
||||||
Twitch-Bot
|
Twitch-Bot
|
||||||
</a>
|
</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="navbar-toggler"
|
class="navbar-toggler"
|
||||||
|
|
82
src/components/_toast.vue
Normal file
82
src/components/_toast.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="toast"
|
||||||
|
:class="classForToast(toast)"
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
aria-atomic="true"
|
||||||
|
>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">
|
||||||
|
{{ toast.text }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="classForCloseButton(toast)"
|
||||||
|
data-bs-dismiss="toast"
|
||||||
|
aria-label="Close"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue'
|
||||||
|
import { Toast } from 'bootstrap'
|
||||||
|
import { ToastContent } from './_toaster.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hdl: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['hidden'],
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
classForCloseButton(toast: ToastContent): string {
|
||||||
|
const classes = [
|
||||||
|
'btn-close',
|
||||||
|
'me-2',
|
||||||
|
'm-auto',
|
||||||
|
]
|
||||||
|
|
||||||
|
if (toast.color) {
|
||||||
|
classes.push('btn-close-white')
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
},
|
||||||
|
|
||||||
|
classForToast(toast: ToastContent): string {
|
||||||
|
const classes = [
|
||||||
|
'toast',
|
||||||
|
'align-items-center',
|
||||||
|
]
|
||||||
|
|
||||||
|
if (toast.color) {
|
||||||
|
classes.push('border-0', `text-bg-${toast.color}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$refs.toast.addEventListener('hidden.bs.toast', () => this.$emit('hidden'))
|
||||||
|
this.hdl = new Toast(this.$refs.toast)
|
||||||
|
this.hdl.show()
|
||||||
|
},
|
||||||
|
|
||||||
|
name: 'TwitchBotEditorToast',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
toast: {
|
||||||
|
required: true,
|
||||||
|
type: Object as PropType<ToastContent>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
47
src/components/_toaster.vue
Normal file
47
src/components/_toaster.vue
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<div class="toast-container bottom-0 end-0 p-3">
|
||||||
|
<toast
|
||||||
|
v-for="toast in toasts"
|
||||||
|
:key="toast.id"
|
||||||
|
:toast="toast"
|
||||||
|
@hidden="removeToast(toast.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import BusEventTypes from '../helpers/busevents'
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import Toast from './_toast.vue'
|
||||||
|
|
||||||
|
export type ToastContent = {
|
||||||
|
id: string
|
||||||
|
color: string | undefined
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { Toast },
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
toasts: [] as ToastContent[],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
removeToast(id: string) {
|
||||||
|
this.toasts = this.toasts.filter((t: ToastContent) => t.id !== id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.bus.on(BusEventTypes.Toast, (toast: ToastContent) => this.toasts.push({
|
||||||
|
...toast,
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
|
name: 'TwitchBotEditorToaster',
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -48,6 +48,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="layoutContent">
|
<div class="layoutContent">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
|
||||||
|
<toaster />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,9 +59,10 @@
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
import HeadNav from './_headNav.vue'
|
import HeadNav from './_headNav.vue'
|
||||||
|
import Toaster from './_toaster.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { HeadNav },
|
components: { HeadNav, Toaster },
|
||||||
|
|
||||||
name: 'TwitchBotEditorApp',
|
name: 'TwitchBotEditorApp',
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,16 +11,19 @@
|
||||||
Login with Twitch
|
Login with Twitch
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<toaster />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import BusEventTypes from '../helpers/busevents'
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
import HeadNav from './_headNav.vue'
|
import HeadNav from './_headNav.vue'
|
||||||
|
import Toaster from './_toaster.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { HeadNav },
|
components: { HeadNav, Toaster },
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
authURL() {
|
authURL() {
|
||||||
|
@ -49,7 +52,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$root.bus.on('login-processing', (loading: boolean) => {
|
this.$root.bus.on(BusEventTypes.LoginProcessing, (loading: boolean) => {
|
||||||
this.loading = loading
|
this.loading = loading
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
13
src/helpers/busevents.ts
Normal file
13
src/helpers/busevents.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
|
||||||
|
enum BusEventTypes {
|
||||||
|
ChangePending = 'changePending',
|
||||||
|
ConfigReload = 'configReload',
|
||||||
|
Error = 'error',
|
||||||
|
FetchError = 'fetchError',
|
||||||
|
LoadingData = 'loadingData',
|
||||||
|
LoginProcessing = 'loginProcessing',
|
||||||
|
Toast = 'toast',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BusEventTypes
|
15
src/helpers/template.ts
Normal file
15
src/helpers/template.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export const BuiltinTemplateFunctions = [
|
||||||
|
'and',
|
||||||
|
'call',
|
||||||
|
'html',
|
||||||
|
'index',
|
||||||
|
'slice',
|
||||||
|
'js',
|
||||||
|
'len',
|
||||||
|
'not',
|
||||||
|
'or',
|
||||||
|
'print',
|
||||||
|
'printf',
|
||||||
|
'println',
|
||||||
|
'urlquery',
|
||||||
|
]
|
35
src/main.ts
35
src/main.ts
|
@ -9,7 +9,9 @@ import 'bootstrap/dist/js/bootstrap.bundle' // Popper & Bootstrap globally avail
|
||||||
import { createApp, h } from 'vue'
|
import { createApp, h } from 'vue'
|
||||||
import mitt from 'mitt'
|
import mitt from 'mitt'
|
||||||
|
|
||||||
|
import BusEventTypes from './helpers/busevents'
|
||||||
import ConfigNotifyListener from './helpers/configNotify'
|
import ConfigNotifyListener from './helpers/configNotify'
|
||||||
|
import { ToastContent } from './components/_toaster.vue'
|
||||||
|
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import App from './components/app.vue'
|
import App from './components/app.vue'
|
||||||
|
@ -36,20 +38,17 @@ const app = createApp({
|
||||||
|
|
||||||
// We renew 720sec before expiration (0.8 * 1h)
|
// We renew 720sec before expiration (0.8 * 1h)
|
||||||
return new Date(this.tokenExpiresAt.getTime() - 720000)
|
return new Date(this.tokenExpiresAt.getTime() - 720000)
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data(): Object {
|
data(): Object {
|
||||||
return {
|
return {
|
||||||
now: new Date(),
|
now: new Date(),
|
||||||
|
tickers: {},
|
||||||
token: '',
|
token: '',
|
||||||
tokenExpiresAt: null as Date | null,
|
tokenExpiresAt: null as Date | null,
|
||||||
tokenUser: '',
|
tokenUser: '',
|
||||||
|
|
||||||
userInfo: null as null | {},
|
userInfo: null as null | {},
|
||||||
|
|
||||||
tickers: {},
|
|
||||||
|
|
||||||
vars: {},
|
vars: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -64,8 +63,10 @@ const app = createApp({
|
||||||
*/
|
*/
|
||||||
check403(resp: Response): Response {
|
check403(resp: Response): Response {
|
||||||
if (resp.status === 403) {
|
if (resp.status === 403) {
|
||||||
// User token is not valid and therefore should be removed
|
/*
|
||||||
// which essentially triggers a logout
|
* User token is not valid and therefore should be removed
|
||||||
|
* which essentially triggers a logout
|
||||||
|
*/
|
||||||
this.logout()
|
this.logout()
|
||||||
throw new Error('user has been logged out')
|
throw new Error('user has been logged out')
|
||||||
}
|
}
|
||||||
|
@ -76,7 +77,9 @@ const app = createApp({
|
||||||
loadVars(): Promise<void | Response> {
|
loadVars(): Promise<void | Response> {
|
||||||
return fetch('editor/vars.json')
|
return fetch('editor/vars.json')
|
||||||
.then((resp: Response) => resp.json())
|
.then((resp: Response) => resp.json())
|
||||||
.then((data: any) => { this.vars = data })
|
.then((data: any) => {
|
||||||
|
this.vars = data
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
login(token: string, expiresAt: Date, username: string): void {
|
login(token: string, expiresAt: Date, username: string): void {
|
||||||
|
@ -128,18 +131,22 @@ const app = createApp({
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
this.bus.on('logout', this.logout)
|
this.bus.on('logout', this.logout)
|
||||||
|
|
||||||
this.$root.registerTicker('updateRootNow', () => { this.now = new Date() }, 30000)
|
this.$root.registerTicker('updateRootNow', () => {
|
||||||
|
this.now = new Date()
|
||||||
|
}, 30000)
|
||||||
this.$root.registerTicker('renewToken', () => this.renewToken(), 60000)
|
this.$root.registerTicker('renewToken', () => this.renewToken(), 60000)
|
||||||
|
|
||||||
// Start background-listen for config updates
|
// Start background-listen for config updates
|
||||||
new ConfigNotifyListener((msgType: string) => { this.$root.bus.emit(msgType) })
|
new ConfigNotifyListener((msgType: string) => {
|
||||||
|
this.$root.bus.emit(msgType)
|
||||||
|
})
|
||||||
|
|
||||||
this.loadVars()
|
this.loadVars()
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.hash.replace(/^[#/]+/, ''))
|
const params = new URLSearchParams(window.location.hash.replace(/^[#/]+/, ''))
|
||||||
const authToken = params.get('access_token')
|
const authToken = params.get('access_token')
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
this.$root.bus.emit('login-processing', true)
|
this.$root.bus.emit(BusEventTypes.LoginProcessing, true)
|
||||||
fetch('config-editor/login', {
|
fetch('config-editor/login', {
|
||||||
body: JSON.stringify({ token: authToken }),
|
body: JSON.stringify({ token: authToken }),
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
@ -147,7 +154,11 @@ const app = createApp({
|
||||||
})
|
})
|
||||||
.then((resp: Response): any => {
|
.then((resp: Response): any => {
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
this.$root.bus.emit('login-processing', false)
|
this.$root.bus.emit(BusEventTypes.LoginProcessing, false)
|
||||||
|
this.bus.emit(BusEventTypes.Toast, {
|
||||||
|
color: 'danger',
|
||||||
|
text: 'Login failed',
|
||||||
|
} as ToastContent)
|
||||||
throw new Error(`login failed, status=${resp.status}`)
|
throw new Error(`login failed, status=${resp.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue