mirror of
https://github.com/Luzifer/browserphone.git
synced 2024-11-08 13:50:05 +00:00
Add call duration, keep notifications
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
1a9aae04c3
commit
aeb624bcd3
6 changed files with 1906 additions and 65 deletions
150
.eslintrc.js
Normal file
150
.eslintrc.js
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* Hack to automatically load globally installed eslint modules
|
||||||
|
* on Archlinux systems placed in /usr/lib/node_modules
|
||||||
|
*
|
||||||
|
* Source: https://github.com/eslint/eslint/issues/11914#issuecomment-569108633
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Module = require('module')
|
||||||
|
|
||||||
|
const hacks = [
|
||||||
|
'babel-eslint',
|
||||||
|
'eslint-plugin-vue',
|
||||||
|
]
|
||||||
|
|
||||||
|
const ModuleFindPath = Module._findPath
|
||||||
|
Module._findPath = (request, paths, isMain) => {
|
||||||
|
const r = ModuleFindPath(request, paths, isMain)
|
||||||
|
if (!r && hacks.includes(request)) {
|
||||||
|
return require.resolve(`/usr/lib/node_modules/${request}`)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ESLint configuration derived as differences from eslint:recommended
|
||||||
|
* with changes I found useful to ensure code quality and equal formatting
|
||||||
|
* https://eslint.org/docs/user-guide/configuring
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/recommended',
|
||||||
|
'eslint:recommended', // https://eslint.org/docs/rules/
|
||||||
|
],
|
||||||
|
|
||||||
|
globals: {
|
||||||
|
process: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
// required to lint *.vue files
|
||||||
|
'vue',
|
||||||
|
],
|
||||||
|
|
||||||
|
reportUnusedDisableDirectives: true,
|
||||||
|
|
||||||
|
root: true,
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'array-bracket-newline': ['error', { multiline: true }],
|
||||||
|
'array-bracket-spacing': ['error'],
|
||||||
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
|
'arrow-parens': ['error', 'as-needed'],
|
||||||
|
'arrow-spacing': ['error', { after: true, before: true }],
|
||||||
|
'block-spacing': ['error'],
|
||||||
|
'brace-style': ['error', '1tbs'],
|
||||||
|
'comma-dangle': ['error', 'always-multiline'],
|
||||||
|
'comma-spacing': ['error'],
|
||||||
|
'comma-style': ['error', 'last'],
|
||||||
|
'curly': ['error'],
|
||||||
|
'default-case-last': ['error'],
|
||||||
|
'default-param-last': ['error'],
|
||||||
|
'dot-location': ['error', 'property'],
|
||||||
|
'dot-notation': ['error'],
|
||||||
|
'eol-last': ['error', 'always'],
|
||||||
|
'eqeqeq': ['error', 'always', { null: 'ignore' }],
|
||||||
|
'func-call-spacing': ['error', 'never'],
|
||||||
|
'function-paren-newline': ['error', 'multiline'],
|
||||||
|
'generator-star-spacing': ['off'], // allow async-await
|
||||||
|
'implicit-arrow-linebreak': ['error'],
|
||||||
|
'indent': ['error', 2],
|
||||||
|
'key-spacing': ['error', { afterColon: true, beforeColon: false, mode: 'strict' }],
|
||||||
|
'keyword-spacing': ['error'],
|
||||||
|
'linebreak-style': ['error', 'unix'],
|
||||||
|
'lines-between-class-members': ['error'],
|
||||||
|
'multiline-comment-style': ['warn'],
|
||||||
|
'newline-per-chained-call': ['error'],
|
||||||
|
'no-alert': ['error'],
|
||||||
|
'no-console': ['off'],
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // allow debugger during development
|
||||||
|
'no-duplicate-imports': ['error'],
|
||||||
|
'no-else-return': ['error'],
|
||||||
|
'no-empty-function': ['error'],
|
||||||
|
'no-extra-parens': ['error'],
|
||||||
|
'no-implicit-coercion': ['error'],
|
||||||
|
'no-lonely-if': ['error'],
|
||||||
|
'no-multi-spaces': ['error'],
|
||||||
|
'no-multiple-empty-lines': ['warn', { max: 2, maxBOF: 0, maxEOF: 0 }],
|
||||||
|
'no-promise-executor-return': ['error'],
|
||||||
|
'no-return-assign': ['error'],
|
||||||
|
'no-script-url': ['error'],
|
||||||
|
'no-template-curly-in-string': ['error'],
|
||||||
|
'no-trailing-spaces': ['error'],
|
||||||
|
'no-unneeded-ternary': ['error'],
|
||||||
|
'no-unreachable-loop': ['error'],
|
||||||
|
'no-unsafe-optional-chaining': ['error'],
|
||||||
|
'no-useless-return': ['error'],
|
||||||
|
'no-var': ['error'],
|
||||||
|
'no-warning-comments': ['error'],
|
||||||
|
'no-whitespace-before-property': ['error'],
|
||||||
|
'object-curly-newline': ['error', { consistent: true }],
|
||||||
|
'object-curly-spacing': ['error', 'always'],
|
||||||
|
'object-shorthand': ['error'],
|
||||||
|
'padded-blocks': ['error', 'never'],
|
||||||
|
'prefer-arrow-callback': ['error'],
|
||||||
|
'prefer-const': ['error'],
|
||||||
|
'prefer-object-spread': ['error'],
|
||||||
|
'prefer-rest-params': ['error'],
|
||||||
|
'prefer-template': ['error'],
|
||||||
|
'quote-props': ['error', 'consistent-as-needed', { keywords: false }],
|
||||||
|
'quotes': ['error', 'single', { allowTemplateLiterals: true }],
|
||||||
|
'require-atomic-updates': ['error'],
|
||||||
|
'require-await': ['error'],
|
||||||
|
'semi': ['error', 'never'],
|
||||||
|
'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: false, ignoreMemberSort: false }],
|
||||||
|
'sort-keys': ['error', 'asc', { caseSensitive: true, natural: false }],
|
||||||
|
'space-before-blocks': ['error', 'always'],
|
||||||
|
'space-before-function-paren': ['error', 'never'],
|
||||||
|
'space-in-parens': ['error', 'never'],
|
||||||
|
'space-infix-ops': ['error'],
|
||||||
|
'space-unary-ops': ['error', { nonwords: false, words: true }],
|
||||||
|
'spaced-comment': ['warn', 'always'],
|
||||||
|
'switch-colon-spacing': ['error'],
|
||||||
|
'template-curly-spacing': ['error', 'never'],
|
||||||
|
'unicode-bom': ['error', 'never'],
|
||||||
|
'vue/new-line-between-multi-line-property': ['error'],
|
||||||
|
'vue/no-empty-component-block': ['error'],
|
||||||
|
'vue/no-reserved-component-names': ['error'],
|
||||||
|
'vue/no-template-target-blank': ['error'],
|
||||||
|
'vue/no-unused-properties': ['error'],
|
||||||
|
'vue/no-unused-refs': ['error'],
|
||||||
|
'vue/no-useless-mustaches': ['error'],
|
||||||
|
'vue/order-in-components': ['off'], // Collides with sort-keys
|
||||||
|
'vue/require-name-property': ['error'],
|
||||||
|
'vue/v-for-delimiter-style': ['error'],
|
||||||
|
'vue/v-on-function-call': ['error'],
|
||||||
|
'wrap-iife': ['error'],
|
||||||
|
'yoda': ['error'],
|
||||||
|
},
|
||||||
|
}
|
4
Makefile
Normal file
4
Makefile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
default:
|
||||||
|
|
||||||
|
lint:
|
||||||
|
eslint --ext=.js,.vue src/
|
1588
package-lock.json
generated
1588
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -5,7 +5,10 @@
|
||||||
"@babel/preset-env": "^7.18.10",
|
"@babel/preset-env": "^7.18.10",
|
||||||
"@babel/runtime": "^7.18.9",
|
"@babel/runtime": "^7.18.9",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
"css-loader": "^6.7.1",
|
"css-loader": "^6.7.1",
|
||||||
|
"eslint": "^7.19.0",
|
||||||
|
"eslint-plugin-vue": "^7.20.0",
|
||||||
"node-sass": "^7.0.1",
|
"node-sass": "^7.0.1",
|
||||||
"sass-loader": "^13.0.2",
|
"sass-loader": "^13.0.2",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
|
|
222
src/app.vue
222
src/app.vue
|
@ -1,49 +1,94 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<b-navbar toggleable="lg" type="dark" variant="primary">
|
<b-navbar
|
||||||
<b-navbar-brand href="#">BrowserPhone</b-navbar-brand>
|
toggleable="lg"
|
||||||
|
type="dark"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
<b-navbar-brand href="#">
|
||||||
|
BrowserPhone
|
||||||
|
</b-navbar-brand>
|
||||||
|
|
||||||
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
|
<b-navbar-toggle target="nav-collapse" />
|
||||||
|
|
||||||
<b-collapse id="nav-collapse" is-nav>
|
<b-collapse
|
||||||
|
id="nav-collapse"
|
||||||
|
is-nav
|
||||||
|
>
|
||||||
<b-navbar-nav class="ml-auto">
|
<b-navbar-nav class="ml-auto">
|
||||||
<b-nav-text v-if="identity">Signed in as <strong>{{ identity }}</strong></b-nav-text>
|
<b-nav-text v-if="identity">
|
||||||
|
Signed in as <strong>{{ identity }}</strong>
|
||||||
|
</b-nav-text>
|
||||||
|
<b-nav-item @click="setupPhone">
|
||||||
|
Reload
|
||||||
|
</b-nav-item>
|
||||||
</b-navbar-nav>
|
</b-navbar-nav>
|
||||||
</b-collapse>
|
</b-collapse>
|
||||||
</b-navbar>
|
</b-navbar>
|
||||||
|
|
||||||
<b-container>
|
<b-container>
|
||||||
<b-row class="justify-content-center mt-5">
|
<b-row class="justify-content-center mt-5">
|
||||||
<b-col cols="12" md="6">
|
<b-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
<b-card>
|
<b-card>
|
||||||
<b-form-input
|
<template v-if="phone.started">
|
||||||
autofocus
|
<b-form-input
|
||||||
class="my-2 number border-info"
|
slot="header"
|
||||||
@keypress="evt => keypress(evt)"
|
v-model="number"
|
||||||
size="lg"
|
autofocus
|
||||||
slot="header"
|
class="my-2 number border-info"
|
||||||
type="tel"
|
size="lg"
|
||||||
v-model="number"
|
type="tel"
|
||||||
></b-form-input>
|
@keypress="evt => keypress(evt)"
|
||||||
|
/>
|
||||||
|
|
||||||
<b-row class="justify-content-center">
|
<b-row class="justify-content-center">
|
||||||
<b-col v-for="key in keys" cols="4" class="text-center mb-3" :key="key">
|
<b-col
|
||||||
<b-btn size="lg" @click="keyDown(key)">{{ key }}</b-btn>
|
v-for="key in keys"
|
||||||
</b-col>
|
:key="key"
|
||||||
|
cols="4"
|
||||||
|
class="text-center mb-3"
|
||||||
|
>
|
||||||
|
<b-btn
|
||||||
|
size="lg"
|
||||||
|
@click="keyDown(key)"
|
||||||
|
>
|
||||||
|
{{ key }}
|
||||||
|
</b-btn>
|
||||||
|
</b-col>
|
||||||
|
|
||||||
<b-col cols="4" class="text-center mb-2">
|
<b-col
|
||||||
<b-btn size="lg" :variant="ongoingCall ? 'danger' : 'success'" @click="toggleCall" :disabled="!phoneReady">
|
cols="4"
|
||||||
<i :class="{ 'fas': true, 'fa-phone': !ongoingCall, 'fa-phone-slash': ongoingCall }"></i>
|
class="text-center mb-2"
|
||||||
</b-btn>
|
>
|
||||||
</b-col>
|
<b-btn
|
||||||
</b-row>
|
size="lg"
|
||||||
|
:variant="ongoingCall ? 'danger' : 'success'"
|
||||||
|
:disabled="!phoneReady"
|
||||||
|
@click="toggleCall"
|
||||||
|
>
|
||||||
|
<i :class="{ 'fas': true, 'fa-phone': !ongoingCall, 'fa-phone-slash': ongoingCall }" />
|
||||||
|
</b-btn>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div slot="footer" v-if="state">
|
<template v-else>
|
||||||
{{ state }}
|
<b-button @click="setupPhone">
|
||||||
|
Initialize
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
slot="footer"
|
||||||
|
class="d-flex justify-content-between"
|
||||||
|
>
|
||||||
|
<span><template v-if="state.time">{{ stateDate }}</template> <template v-if="state.msg">{{ state.msg }}</template></span>
|
||||||
|
<span>{{ callDuration }}</span>
|
||||||
</div>
|
</div>
|
||||||
</b-card>
|
</b-card>
|
||||||
|
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
</b-container>
|
</b-container>
|
||||||
|
@ -52,14 +97,27 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import config from './config.js'
|
||||||
import { Device } from '@twilio/voice-sdk'
|
import { Device } from '@twilio/voice-sdk'
|
||||||
|
|
||||||
import config from './config.js'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
callDuration() {
|
||||||
|
if (!this.phone.callConnected) {
|
||||||
|
return '--:--'
|
||||||
|
}
|
||||||
|
|
||||||
|
let secs = Math.round((new Date() - this.phone.callConnected) / 1000)
|
||||||
|
const parts = []
|
||||||
|
for (const div of [3600, 60, 1]) {
|
||||||
|
const n = Math.floor(secs / div)
|
||||||
|
secs -= n * div
|
||||||
|
parts.append(n.toFixed(0).padStart(2, '0'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(':')
|
||||||
|
},
|
||||||
|
|
||||||
keys() {
|
keys() {
|
||||||
return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#']
|
return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#']
|
||||||
},
|
},
|
||||||
|
@ -73,7 +131,20 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
phoneReady() {
|
phoneReady() {
|
||||||
return this.phone.conn && this.phone.device && this.phone.registered
|
return this.phone.device && this.phone.registered
|
||||||
|
},
|
||||||
|
|
||||||
|
stateDate() {
|
||||||
|
if (!this.state.time) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
this.state.time.getHours().toFixed(0)
|
||||||
|
.padStart(2, '0'),
|
||||||
|
this.state.time.getMinutes().toFixed(0)
|
||||||
|
.padStart(2, '0'),
|
||||||
|
].join(':')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -85,14 +156,23 @@ export default {
|
||||||
max: 30000,
|
max: 30000,
|
||||||
multiplier: 1.25,
|
multiplier: 1.25,
|
||||||
},
|
},
|
||||||
|
|
||||||
identity: null,
|
identity: null,
|
||||||
|
now: new Date(),
|
||||||
number: '',
|
number: '',
|
||||||
phone: {
|
phone: {
|
||||||
|
callConnected: null,
|
||||||
conn: null,
|
conn: null,
|
||||||
device: null,
|
device: null,
|
||||||
registered: false,
|
registered: false,
|
||||||
|
started: false,
|
||||||
},
|
},
|
||||||
state: '',
|
|
||||||
|
state: {
|
||||||
|
msg: '',
|
||||||
|
time: null,
|
||||||
|
},
|
||||||
|
|
||||||
wakeLock: {
|
wakeLock: {
|
||||||
obj: null,
|
obj: null,
|
||||||
request: null,
|
request: null,
|
||||||
|
@ -101,12 +181,14 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
announceStatus(state, autoClear = true) {
|
announceStatus(state, autoClear = false) {
|
||||||
this.state = state
|
this.state.msg = state
|
||||||
|
this.state.time = new Date()
|
||||||
|
|
||||||
if (autoClear) {
|
if (autoClear) {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.state = ''
|
this.state.msg = ''
|
||||||
|
this.state.time = null
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -116,15 +198,21 @@ export default {
|
||||||
|
|
||||||
this.phone.conn.on('accept', conn => {
|
this.phone.conn.on('accept', conn => {
|
||||||
this.phone.conn = conn
|
this.phone.conn = conn
|
||||||
|
this.phone.callConnected = new Date()
|
||||||
this.announceStatus('Call connected')
|
this.announceStatus('Call connected')
|
||||||
this.setWakeLock(true)
|
this.setWakeLock(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.phone.conn.on('disconnect', () => {
|
this.phone.conn.on('disconnect', () => {
|
||||||
this.phone.conn = null
|
this.phone.conn = null
|
||||||
|
this.phone.callConnected = null
|
||||||
this.announceStatus('Call disconnected')
|
this.announceStatus('Call disconnected')
|
||||||
this.setWakeLock(false)
|
this.setWakeLock(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.phone.conn.on('error', err => {
|
||||||
|
console.error('error in call', err)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
keyDown(key) {
|
keyDown(key) {
|
||||||
|
@ -142,9 +230,36 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async setWakeLock(lock) {
|
||||||
|
if (!navigator || !navigator.getWakeLock) {
|
||||||
|
// No wake-lock functionality present in this browser
|
||||||
|
console.debug('Browser has no wake-lock support')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.wakeLock.obj) {
|
||||||
|
this.wakeLock.obj = await navigator.getWakeLock('screen')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lock && !this.wakeLock.request) {
|
||||||
|
this.wakeLock.request = this.wakeLock.obj.createRequest()
|
||||||
|
this.announceStatus('Wake-lock aquired: Keeping display active...')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lock && this.wakeLock.request) {
|
||||||
|
this.wakeLock.request.cancel()
|
||||||
|
this.wakeLock.request = null
|
||||||
|
this.announceStatus('Wake-lock disabled: Screen may sleep...')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setupPhone() {
|
setupPhone() {
|
||||||
this.backoff.current = Math.min(this.backoff.current * this.backoff.multiplier, this.backoff.max)
|
this.backoff.current = Math.min(this.backoff.current * this.backoff.multiplier, this.backoff.max)
|
||||||
const opts = { codecPreferences: ['opus', 'pcmu'], fakeLocalDTMF: true }
|
const opts = {
|
||||||
|
codecPreferences: ['opus', 'pcmu'],
|
||||||
|
fakeLocalDTMF: true,
|
||||||
|
}
|
||||||
|
|
||||||
if (this.phone.device) {
|
if (this.phone.device) {
|
||||||
this.phone.device.destroy()
|
this.phone.device.destroy()
|
||||||
|
@ -182,30 +297,6 @@ export default {
|
||||||
.catch(err => console.error(err))
|
.catch(err => console.error(err))
|
||||||
},
|
},
|
||||||
|
|
||||||
async setWakeLock(lock) {
|
|
||||||
if (!navigator || !navigator.getWakeLock) {
|
|
||||||
// No wake-lock functionality present in this browser
|
|
||||||
console.debug('Browser has no wake-lock support')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.wakeLock.obj) {
|
|
||||||
this.wakeLock.obj = await navigator.getWakeLock('screen')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lock && !this.wakeLock.request) {
|
|
||||||
this.wakeLock.request = this.wakeLock.obj.createRequest()
|
|
||||||
this.announceStatus('Wake-lock aquired: Keeping display active...')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lock && this.wakeLock.request) {
|
|
||||||
this.wakeLock.request.cancel()
|
|
||||||
this.wakeLock.request = null
|
|
||||||
this.announceStatus('Wake-lock disabled: Screen may sleep...')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleCall() {
|
toggleCall() {
|
||||||
if (this.pendingCall) {
|
if (this.pendingCall) {
|
||||||
this.phone.conn.accept()
|
this.phone.conn.accept()
|
||||||
|
@ -232,10 +323,13 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
window.setInterval(() => {
|
||||||
|
this.now = new Date()
|
||||||
|
}, 250)
|
||||||
this.announceStatus('Phone loaded...')
|
this.announceStatus('Phone loaded...')
|
||||||
this.setupPhone()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
name: 'BrowserPhoneApp',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable sort-imports */
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
import BootstrapVue from 'bootstrap-vue'
|
import BootstrapVue from 'bootstrap-vue'
|
||||||
|
@ -10,8 +11,9 @@ import app from './app.vue'
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
new Vue({
|
window.app = new Vue({
|
||||||
components: { app },
|
components: { app },
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
name: 'BrowserPhone',
|
||||||
render: c => c('app'),
|
render: c => c('app'),
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue