Add support for mobile devices
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Jack Hadrill 2022-03-22 19:57:47 +00:00
parent ab71601ae9
commit c5914d3a28
23 changed files with 160 additions and 89 deletions

View File

@ -11,7 +11,7 @@
<meta name="application-name" content="Mercury"> <meta name="application-name" content="Mercury">
<meta name="msapplication-TileColor" content="#da532c"> <meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, height=500px, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, viewport-fit=cover, user-scalable=no" />
<title>Mercury</title> <title>Mercury</title>
</head> </head>
<body> <body>

View File

@ -11,10 +11,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="container-fluid vh-100"> <div class="d-flex flex-row h-100">
<div class="row vh-100"> <router-view />
<router-view />
</div>
</div> </div>
</template> </template>
@ -23,12 +21,19 @@ import { onMounted, ref, watch } from 'vue'
import { Modal } from 'bootstrap' import { Modal } from 'bootstrap'
import { useMercuryStore } from './stores/mercuryStore' import { useMercuryStore } from './stores/mercuryStore'
import UserSettings from './components/userSettings/UserSettings.vue' import UserSettings from './components/userSettings/UserSettings.vue'
import { useWindowSize } from '@vueuse/core';
const mercuryStore = useMercuryStore() const mercuryStore = useMercuryStore()
const configured = mercuryStore.configured() const configured = mercuryStore.configured()
const settingsModal = ref() const settingsModal = ref()
const { width, height } = useWindowSize()
watch(width, () => {
mercuryStore.mobile = window.innerWidth < 768 ? true : false
mercuryStore.showSidebar = !mercuryStore.mobile
}, { immediate: true })
onMounted(() => { onMounted(() => {
const modal = new Modal(settingsModal.value) const modal = new Modal(settingsModal.value)
@ -44,9 +49,39 @@ onMounted(() => {
}, { immediate: true }) }, { immediate: true })
}) })
visualViewport.addEventListener('resize', () => {
window.scrollTo(0, 0)
document.querySelector('#app').style.height = `${window.visualViewport.height}px`
})
document.addEventListener('gesturestart', (e) => {
e.preventDefault()
})
document.addEventListener('touchmove', (e) => {
e.preventDefault()
})
</script> </script>
<style> <style>
html, body, #app {
overflow: hidden;
overscroll-behavior: none;
}
*:not(.overflow-auto, .overflow-auto *) {
touch-action: none;
}
body::-webkit-scrollbar {
display: none;
}
#app {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
a { a {
text-decoration: none; text-decoration: none;
} }

View File

@ -1,7 +1,7 @@
export const animateCSS = (element, animation, prefix = 'animate__') => new Promise((resolve, reject) => { export const animateCSS = (element, animation, prefix = 'animate__') => new Promise((resolve, reject) => {
const animationName = `${prefix}${animation}`; const animationName = `${prefix}${animation}`
element.classList.add(`${prefix}animated`, animationName); element.classList.add(`${prefix}animated`, animationName)
function handleAnimationEnd(event) { function handleAnimationEnd(event) {
event.stopPropagation() event.stopPropagation()

View File

@ -1,16 +1,16 @@
<template> <template>
<h3>Add channel</h3> <h3>Add channel</h3>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<input v-model="name" type="text" class="form-control" placeholder="Channel name" aria-label="Channel name"> <input v-model="name" type="text" class="form-control" placeholder="Channel name" aria-label="Channel name">
</div>
<div class="col">
<input v-model="key" type="text" class="form-control" placeholder="Channel key (base64)" aria-label="Channel key">
</div>
</div> </div>
<div class="d-flex flex-row mt-3"> <div class="col">
<button @click="addChannel" type="submit" class="btn btn-primary ms-auto">Add channel</button> <input v-model="key" type="text" class="form-control" placeholder="Channel key (base64)" aria-label="Channel key">
</div> </div>
</div>
<div class="d-flex flex-row mt-3">
<button @click="addChannel" type="submit" class="btn btn-primary ms-auto">Add channel</button>
</div>
</template> </template>
<script setup> <script setup>
@ -55,4 +55,4 @@ function addChannel() {
name.value = '' name.value = ''
key.value = '' key.value = ''
} }
</script> </script>

View File

@ -33,4 +33,4 @@ const channelStore = useChannelStore()
i { i {
cursor: pointer; cursor: pointer;
} }
</style> </style>

View File

@ -21,4 +21,4 @@ onBeforeRouteUpdate(() => {
i { i {
cursor: pointer; cursor: pointer;
} }
</style> </style>

View File

@ -16,4 +16,4 @@ const color = computed(() => { return user.color ?? Colors.Secondary })
span:hover { span:hover {
color: white !important; color: white !important;
} }
</style> </style>

View File

@ -1,20 +1,23 @@
<template> <template>
<div class="col g-0"> <div class="flex-fill h-100">
<div class="d-flex flex-column vh-100"> <div class="d-flex flex-column h-100">
<header class="bg-light px-3 py-2 d-flex flex-row align-items-center"> <header class="bg-light px-3 py-2 d-flex flex-row align-items-center">
<span class="me-3 fs-3"> <i v-if="mobile && !showSidebar" @click="showSidebar = true" class="me-3 fs-3 bi bi-three-dots"></i>
<h2 class="mb-0">
<slot name="title"></slot> <slot name="title"></slot>
</span> </h2>
<slot name="header"></slot> <div class="ms-2">
<slot name="header"></slot>
</div>
<span v-if="!home" class="ms-auto fs-4"> <span v-if="!home" class="ms-auto fs-4">
<router-link :to="{ name: 'Home' }"><i class="text-dark bi bi-house-fill"></i></router-link> <router-link :to="{ name: 'Home' }"><i class="text-dark bi bi-house-fill"></i></router-link>
</span> </span>
</header> </header>
<div class="px-3 py-2 overflow-auto"> <div id="content" class="px-3 py-2 overflow-auto mb-auto">
<slot name="content"></slot> <slot name="content"></slot>
<div ref="bottom"></div> <div ref="bottom"></div>
</div> </div>
<div class="px-3 py-2 mt-auto"> <div class="px-3 py-2">
<slot name="footer"></slot> <slot name="footer"></slot>
</div> </div>
</div> </div>
@ -24,11 +27,15 @@
<script setup> <script setup>
import { computed, onMounted, onUpdated, ref } from 'vue' import { computed, onMounted, onUpdated, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useMercuryStore } from '../../stores/mercuryStore'
const props = defineProps({ autoScroll: Boolean }) const props = defineProps({ autoScroll: Boolean })
const route = useRoute() const route = useRoute()
const home = computed(() => { return route.name == 'Home' }) const home = computed(() => { return route.name == 'Home' })
const mercuryStore = useMercuryStore()
const { mobile, showSidebar } = storeToRefs(mercuryStore)
// Scroll to bottom. // Scroll to bottom.
const bottom = ref(null) const bottom = ref(null)
@ -37,9 +44,16 @@ onUpdated(() => {
bottom.value.scrollIntoView({ behavior: "smooth", block: "center" }) bottom.value.scrollIntoView({ behavior: "smooth", block: "center" })
} }
}) })
onMounted(() => { onMounted(() => {
if (props.autoScroll) { if (props.autoScroll) {
bottom.value.scrollIntoView({ block: "center" }) bottom.value.scrollIntoView({ block: "center" })
} }
}) })
</script> </script>
<style scoped>
#content {
overscroll-behavior: none;
}
</style>

View File

@ -8,9 +8,9 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useUserStore } from '../../stores/userStore'
import Constants from '../../common/constants' import Constants from '../../common/constants'
import { useMercuryStore } from '../../stores/mercuryStore'; import { useUserStore } from '../../stores/userStore'
import { useMercuryStore } from '../../stores/mercuryStore'
const props = defineProps(['message']) const props = defineProps(['message'])
const message = props.message const message = props.message

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input ref="input" @keyup.enter="sendMessage" v-model="message" type="text" class="form-control" placeholder="Message" aria-label="Message" autofocus autocomplete="off"> <input ref="inputField" @keyup.enter="sendMessage" v-model="message" type="text" class="form-control" placeholder="Message" aria-label="Message" autocomplete="off">
<button @click="sendMessage" class="btn btn-primary" type="button"><i class="bi bi-send-fill"></i></button> <button @click="sendMessage" class="btn btn-primary" type="button"><i class="bi bi-send-fill"></i></button>
</div> </div>
</template> </template>
@ -21,11 +21,11 @@ const messageStore = useMessageStore()
const channel = computed(() => { return props.channel }) const channel = computed(() => { return props.channel })
const message = ref("") const message = ref("")
const input = ref(null) const inputField = ref(null)
const sendMessage = () => { const sendMessage = () => {
if (message.value.trim() === '') { if (message.value.trim() === '') {
animateCSS(input.value, 'headShake') animateCSS(inputField.value, 'headShake')
} else { } else {
ws.send(packers[MessageTypes.Basic](encoder.encode(message.value), channel.value.rawKey)) ws.send(packers[MessageTypes.Basic](encoder.encode(message.value), channel.value.rawKey))
messageStore.addMessage(channel.value.id, -1, message.value) messageStore.addMessage(channel.value.id, -1, message.value)
@ -35,6 +35,5 @@ const sendMessage = () => {
onBeforeRouteUpdate(() => { onBeforeRouteUpdate(() => {
message.value = '' message.value = ''
input.value.focus()
}) })
</script> </script>

View File

@ -1,4 +1,5 @@
<template> <template>
<h5 class="text-muted">Channels</h5>
<nav class="d-flex flex-column"> <nav class="d-flex flex-column">
<router-link v-for="channel in channelStore.channels" :key="channel.id" :to="{ name: 'Channel', params: { channelId: channel.id }}" class="text-light"> <router-link v-for="channel in channelStore.channels" :key="channel.id" :to="{ name: 'Channel', params: { channelId: channel.id }}" class="text-light">
{{ channel.name }} {{ channel.name }}

View File

@ -0,0 +1,14 @@
<template>
<div class="d-flex flex-column mb-3">
<span v-if="connected" class="text-light"><i class="bi bi-circle-fill text-success"></i> Connected</span>
<span v-else class="text-light"><i class="bi bi-circle-fill text-warning"></i> Connecting...</span>
<router-link :to="{ name: 'Settings' }" class="text-muted"><i class="bi bi-gear-fill"></i> Settings</router-link>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia'
import { useMercuryStore } from '../../stores/mercuryStore'
const mercuryStore = useMercuryStore()
const { connected } = storeToRefs(mercuryStore)
</script>

View File

@ -1,19 +1,19 @@
<template> <template>
<div class="col-2 bg-dark"> <div v-show="showSidebar" id="sidebar" :class="{ mobile: mobile }" class="px-3 bg-dark h-100">
<div class="d-flex flex-column vh-100"> <div class="d-flex flex-column h-100">
<div class="d-flex flex-row logo align-items-center py-2"> <div>
<img src="../../assets/logo-dark.png" class="me-2" /> <div class="d-flex flex-row logo align-items-center py-2">
<span class="fs-3 text-light">Mercury</span> <img src="../../assets/logo-dark.png" class="me-2" />
</div> <h2 class="mb-0 text-light">Mercury</h2>
<div v-if="configured"> <i @click="showSidebar = false" v-if="mobile && showSidebar" class="ms-auto fs-3 text-white bi bi-x-circle"></i>
<h5 class="text-muted">Channels</h5> </div>
</div>
<div id="navigation" class="overflow-auto mt-3 mb-auto">
<Channels /> <Channels />
<slot name="top"></slot> <slot name="top"></slot>
</div> </div>
<div class="d-flex flex-column mt-auto mb-3"> <div class="mt-3">
<span v-if="connected" class="text-light"><i class="bi bi-circle-fill text-success"></i> Connected</span> <Footer />
<span v-else class="text-light"><i class="bi bi-circle-fill text-warning"></i> Connecting...</span>
<router-link :to="{ name: 'Settings' }" class="text-muted"><i class="bi bi-gear-fill"></i> Settings</router-link>
<slot name="bottom"></slot> <slot name="bottom"></slot>
</div> </div>
</div> </div>
@ -21,13 +21,23 @@
</template> </template>
<script setup> <script setup>
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia'
import { watch } from 'vue';
import { useMercuryStore } from '../../stores/mercuryStore' import { useMercuryStore } from '../../stores/mercuryStore'
import Channels from './Channels.vue' import Channels from './Channels.vue'
import Footer from './Footer.vue'
const mercuryStore = useMercuryStore() const mercuryStore = useMercuryStore()
const { connected } = storeToRefs(mercuryStore) const { mobile, showSidebar } = storeToRefs(mercuryStore)
const configured = mercuryStore.configured() </script>
</script> <style scoped>
#sidebar {
min-width: 250px;
max-width: 250px;
overflow-wrap: break-word;
}
#navigation {
overscroll-behavior: none;
}
</style>

View File

@ -1,9 +1,7 @@
<template> <template>
<div class="d-flex flex-column"> <User :user="mercuryStore.user" />
<User :user="mercuryStore.user" /> <div v-for="user in users" :key="user.id">
<div v-for="user in users" :key="user.id" class="d-flex flex-row align-items-center"> <User :user="user" />
<User :user="user" />
</div>
</div> </div>
</template> </template>
@ -11,7 +9,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useMercuryStore } from '../../stores/mercuryStore' import { useMercuryStore } from '../../stores/mercuryStore'
import { useUserStore } from '../../stores/userStore' import { useUserStore } from '../../stores/userStore'
import User from './User.vue' import User from '../common/User.vue'
const props = defineProps(['channel']) const props = defineProps(['channel'])
const channelId = computed(() => { return props.channel.id }) const channelId = computed(() => { return props.channel.id })

View File

@ -1,5 +1,4 @@
<template> <template>
<div ref="picker" class="btn w-100"><div class="label">Set username color</div></div> <div ref="picker" class="btn w-100"><div class="label">Set username color</div></div>
</template> </template>

View File

@ -36,5 +36,4 @@ function save() {
mercuryStore.setUserColor(color.value) mercuryStore.setUserColor(color.value)
requestUserData() requestUserData()
} }
</script>
</script>

View File

@ -148,13 +148,13 @@ export default {
switch (incomingPacket.messageType) { switch (incomingPacket.messageType) {
case MessageTypes.Basic: case MessageTypes.Basic:
handleBasicMessage(incomingPacket) handleBasicMessage(incomingPacket)
break; break
case MessageTypes.UserDataRequest: case MessageTypes.UserDataRequest:
handleUserDataRequest(incomingPacket) handleUserDataRequest(incomingPacket)
break; break
case MessageTypes.UserDataResponse: case MessageTypes.UserDataResponse:
handleUserDataResponse(incomingPacket) handleUserDataResponse(incomingPacket)
break; break
default: default:
console.error(`Received unknown message type ${incomingPacket.messageType}`); console.error(`Received unknown message type ${incomingPacket.messageType}`);

View File

@ -23,4 +23,4 @@ export default createRouter({
component: SettingsView component: SettingsView
} }
] ]
}) })

View File

@ -29,4 +29,4 @@ export const useChannelStore = defineStore({
}) })
} }
} }
}) })

View File

@ -9,7 +9,9 @@ export const useMercuryStore = defineStore({
name: '', name: '',
color: null color: null
}), }),
connected: false connected: false,
mobile: false,
showSidebar: true
}), }),
getters: { getters: {
configured: (state) => { configured: (state) => {

View File

@ -7,6 +7,7 @@
</template> </template>
</Sidebar> </Sidebar>
<Content autoScroll> <Content autoScroll>
<template v-slot:title>{{ channel.name }}</template> <template v-slot:title>{{ channel.name }}</template>
<template v-slot:header> <template v-slot:header>
<Key :channel="channel" /> <Key :channel="channel" />
@ -20,16 +21,16 @@
</Content> </Content>
</template> </template>
<template v-else> <template v-else>
<Sidebar /> <Sidebar />
<Content> <Content>
<template v-slot:title>Missing channel</template> <template v-slot:title>Missing channel</template>
<template v-slot:content> <template v-slot:content>
<div class="d-flex flex-column text-center"> <div class="d-flex flex-column text-center">
<i class="error text-warning bi bi-cone-striped"></i> <i class="error text-warning bi bi-cone-striped"></i>
<h2>That channel does not exist!</h2> <h2>That channel does not exist!</h2>
</div> </div>
</template> </template>
</Content> </Content>
</template> </template>
</template> </template>
@ -40,9 +41,9 @@ import { onBeforeRouteUpdate, useRoute } from 'vue-router'
import { useChannelStore } from '../stores/channelStore' import { useChannelStore } from '../stores/channelStore'
import { useMessageStore } from '../stores/messageStore' import { useMessageStore } from '../stores/messageStore'
import Sidebar from '../components/sidebar/Sidebar.vue' import Sidebar from '../components/sidebar/Sidebar.vue'
import Content from '../components/Content.vue' import Content from '../components/content/Content.vue'
import Key from '../components/common/Key.vue' import Key from '../components/common/Key.vue'
import UserList from '../components/userList/UserList.vue' import UserList from '../components/sidebar/UserList.vue'
import Message from '../components/conversation/Message.vue' import Message from '../components/conversation/Message.vue'
import MessageInput from '../components/conversation/MessageInput.vue' import MessageInput from '../components/conversation/MessageInput.vue'
@ -53,14 +54,13 @@ const messageStore = useMessageStore()
const channel = computed(() => { return channelStore.getChannelById(route.params.channelId) }) const channel = computed(() => { return channelStore.getChannelById(route.params.channelId) })
const messages = computed(() => { return messageStore.getMessagesByChannelId(route.params.channelId) }) const messages = computed(() => { return messageStore.getMessagesByChannelId(route.params.channelId) })
// Show the key. // Show the key in the header.
const locked = ref(false) const keyHidden = ref(false)
onBeforeRouteUpdate(async (to, from) => { locked.value = false }) onBeforeRouteUpdate(async (to, from) => { keyHidden.value = false })
const abcdef = ref(null)
</script> </script>
<style scoped> <style scoped>
i.error { i.error {
font-size: 10rem; font-size: 10rem;
} }
</style> </style>

View File

@ -4,7 +4,7 @@
<template v-slot:title>Welcome to Mercury!</template> <template v-slot:title>Welcome to Mercury!</template>
<template v-slot:content> <template v-slot:content>
<div class="d-flex flex-column text-center align-items-center px-5 overflow-hidden"> <div class="d-flex flex-column text-center align-items-center px-5 overflow-hidden">
<img class="animate__animated animate__rollIn py-5 w-25" id="logo" src="../assets/logo-light.png" /> <img class="animate__animated animate__rollIn py-5" id="logo" src="../assets/logo-light.png" />
<h1 class="animate__animated animate__zoomIn"> <h1 class="animate__animated animate__zoomIn">
Mercury is a web-based BENNC client. Mercury is a web-based BENNC client.
</h1> </h1>
@ -18,11 +18,13 @@
<script setup> <script setup>
import Sidebar from '../components/sidebar/Sidebar.vue' import Sidebar from '../components/sidebar/Sidebar.vue'
import Content from '../components/Content.vue' import Content from '../components/content/Content.vue'
</script> </script>
<style scoped> <style scoped>
#logo { #logo {
width: 25vw;
height: auto;
filter: invert(80%); filter: invert(80%);
} }
</style> </style>

View File

@ -11,14 +11,12 @@
<AddChannel /> <AddChannel />
</template> </template>
</Content> </Content>
</template> </template>
<script setup> <script setup>
import Sidebar from '../components/sidebar/Sidebar.vue' import Sidebar from '../components/sidebar/Sidebar.vue'
import Content from '../components/Content.vue' import Content from '../components/content/Content.vue'
import UserSettings from '../components/userSettings/UserSettings.vue' import UserSettings from '../components/userSettings/UserSettings.vue'
import ConfiguredChannels from '../components/channelSettings/ConfiguredChannels.vue' import ConfiguredChannels from '../components/channelSettings/ConfiguredChannels.vue'
import AddChannel from '../components/channelSettings/AddChannel.vue' import AddChannel from '../components/channelSettings/AddChannel.vue'
</script>
</script>