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>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a
|
||||
class="navbar-brand"
|
||||
href="#"
|
||||
@click.prevent
|
||||
>
|
||||
<span class="navbar-brand">
|
||||
<i class="fas fa-robot fa-fw me-1 text-info" />
|
||||
Twitch-Bot
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<button
|
||||
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 class="layoutContent">
|
||||
<router-view />
|
||||
|
||||
<toaster />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,9 +59,10 @@
|
|||
import { defineComponent } from 'vue'
|
||||
|
||||
import HeadNav from './_headNav.vue'
|
||||
import Toaster from './_toaster.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { HeadNav },
|
||||
components: { HeadNav, Toaster },
|
||||
|
||||
name: 'TwitchBotEditorApp',
|
||||
})
|
||||
|
|
|
@ -11,16 +11,19 @@
|
|||
Login with Twitch
|
||||
</button>
|
||||
</div>
|
||||
<toaster />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BusEventTypes from '../helpers/busevents'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import HeadNav from './_headNav.vue'
|
||||
import Toaster from './_toaster.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { HeadNav },
|
||||
components: { HeadNav, Toaster },
|
||||
|
||||
computed: {
|
||||
authURL() {
|
||||
|
@ -49,7 +52,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.$root.bus.on('login-processing', (loading: boolean) => {
|
||||
this.$root.bus.on(BusEventTypes.LoginProcessing, (loading: boolean) => {
|
||||
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 mitt from 'mitt'
|
||||
|
||||
import BusEventTypes from './helpers/busevents'
|
||||
import ConfigNotifyListener from './helpers/configNotify'
|
||||
import { ToastContent } from './components/_toaster.vue'
|
||||
|
||||
import router from './router'
|
||||
import App from './components/app.vue'
|
||||
|
@ -36,20 +38,17 @@ const app = createApp({
|
|||
|
||||
// We renew 720sec before expiration (0.8 * 1h)
|
||||
return new Date(this.tokenExpiresAt.getTime() - 720000)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
data(): Object {
|
||||
return {
|
||||
now: new Date(),
|
||||
tickers: {},
|
||||
token: '',
|
||||
tokenExpiresAt: null as Date | null,
|
||||
tokenUser: '',
|
||||
|
||||
userInfo: null as null | {},
|
||||
|
||||
tickers: {},
|
||||
|
||||
vars: {},
|
||||
}
|
||||
},
|
||||
|
@ -64,8 +63,10 @@ const app = createApp({
|
|||
*/
|
||||
check403(resp: Response): Response {
|
||||
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()
|
||||
throw new Error('user has been logged out')
|
||||
}
|
||||
|
@ -76,7 +77,9 @@ const app = createApp({
|
|||
loadVars(): Promise<void | Response> {
|
||||
return fetch('editor/vars.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 {
|
||||
|
@ -128,18 +131,22 @@ const app = createApp({
|
|||
mounted(): void {
|
||||
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)
|
||||
|
||||
// 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()
|
||||
|
||||
const params = new URLSearchParams(window.location.hash.replace(/^[#/]+/, ''))
|
||||
const authToken = params.get('access_token')
|
||||
if (authToken) {
|
||||
this.$root.bus.emit('login-processing', true)
|
||||
this.$root.bus.emit(BusEventTypes.LoginProcessing, true)
|
||||
fetch('config-editor/login', {
|
||||
body: JSON.stringify({ token: authToken }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
@ -147,7 +154,11 @@ const app = createApp({
|
|||
})
|
||||
.then((resp: Response): any => {
|
||||
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}`)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue