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="msapplication-TileColor" content="#da532c">
<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>
</head>
<body>

View File

@ -11,10 +11,8 @@
</div>
</div>
</div>
<div class="container-fluid vh-100">
<div class="row vh-100">
<router-view />
</div>
<div class="d-flex flex-row h-100">
<router-view />
</div>
</template>
@ -23,12 +21,19 @@ import { onMounted, ref, watch } from 'vue'
import { Modal } from 'bootstrap'
import { useMercuryStore } from './stores/mercuryStore'
import UserSettings from './components/userSettings/UserSettings.vue'
import { useWindowSize } from '@vueuse/core';
const mercuryStore = useMercuryStore()
const configured = mercuryStore.configured()
const settingsModal = ref()
const { width, height } = useWindowSize()
watch(width, () => {
mercuryStore.mobile = window.innerWidth < 768 ? true : false
mercuryStore.showSidebar = !mercuryStore.mobile
}, { immediate: true })
onMounted(() => {
const modal = new Modal(settingsModal.value)
@ -44,9 +49,39 @@ onMounted(() => {
}, { 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>
<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 {
text-decoration: none;
}

View File

@ -1,7 +1,7 @@
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) {
event.stopPropagation()

View File

@ -1,16 +1,16 @@
<template>
<h3>Add channel</h3>
<div class="row">
<div class="col">
<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 class="row">
<div class="col">
<input v-model="name" type="text" class="form-control" placeholder="Channel name" aria-label="Channel name">
</div>
<div class="d-flex flex-row mt-3">
<button @click="addChannel" type="submit" class="btn btn-primary ms-auto">Add channel</button>
<div class="col">
<input v-model="key" type="text" class="form-control" placeholder="Channel key (base64)" aria-label="Channel key">
</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>
<script setup>

View File

@ -1,20 +1,23 @@
<template>
<div class="col g-0">
<div class="d-flex flex-column vh-100">
<div class="flex-fill h-100">
<div class="d-flex flex-column h-100">
<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>
</span>
<slot name="header"></slot>
</h2>
<div class="ms-2">
<slot name="header"></slot>
</div>
<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>
</span>
</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>
<div ref="bottom"></div>
</div>
<div class="px-3 py-2 mt-auto">
<div class="px-3 py-2">
<slot name="footer"></slot>
</div>
</div>
@ -24,11 +27,15 @@
<script setup>
import { computed, onMounted, onUpdated, ref } from 'vue'
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useMercuryStore } from '../../stores/mercuryStore'
const props = defineProps({ autoScroll: Boolean })
const route = useRoute()
const home = computed(() => { return route.name == 'Home' })
const mercuryStore = useMercuryStore()
const { mobile, showSidebar } = storeToRefs(mercuryStore)
// Scroll to bottom.
const bottom = ref(null)
@ -37,9 +44,16 @@ onUpdated(() => {
bottom.value.scrollIntoView({ behavior: "smooth", block: "center" })
}
})
onMounted(() => {
if (props.autoScroll) {
bottom.value.scrollIntoView({ block: "center" })
}
})
</script>
<style scoped>
#content {
overscroll-behavior: none;
}
</style>

View File

@ -8,9 +8,9 @@
<script setup>
import { computed } from 'vue'
import { useUserStore } from '../../stores/userStore'
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 message = props.message

View File

@ -1,6 +1,6 @@
<template>
<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>
</div>
</template>
@ -21,11 +21,11 @@ const messageStore = useMessageStore()
const channel = computed(() => { return props.channel })
const message = ref("")
const input = ref(null)
const inputField = ref(null)
const sendMessage = () => {
if (message.value.trim() === '') {
animateCSS(input.value, 'headShake')
animateCSS(inputField.value, 'headShake')
} else {
ws.send(packers[MessageTypes.Basic](encoder.encode(message.value), channel.value.rawKey))
messageStore.addMessage(channel.value.id, -1, message.value)
@ -35,6 +35,5 @@ const sendMessage = () => {
onBeforeRouteUpdate(() => {
message.value = ''
input.value.focus()
})
</script>

View File

@ -1,4 +1,5 @@
<template>
<h5 class="text-muted">Channels</h5>
<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">
{{ 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>
<div class="col-2 bg-dark">
<div class="d-flex flex-column vh-100">
<div class="d-flex flex-row logo align-items-center py-2">
<img src="../../assets/logo-dark.png" class="me-2" />
<span class="fs-3 text-light">Mercury</span>
<div v-show="showSidebar" id="sidebar" :class="{ mobile: mobile }" class="px-3 bg-dark h-100">
<div class="d-flex flex-column h-100">
<div>
<div class="d-flex flex-row logo align-items-center py-2">
<img src="../../assets/logo-dark.png" class="me-2" />
<h2 class="mb-0 text-light">Mercury</h2>
<i @click="showSidebar = false" v-if="mobile && showSidebar" class="ms-auto fs-3 text-white bi bi-x-circle"></i>
</div>
</div>
<div v-if="configured">
<h5 class="text-muted">Channels</h5>
<div id="navigation" class="overflow-auto mt-3 mb-auto">
<Channels />
<slot name="top"></slot>
</div>
<div class="d-flex flex-column mt-auto 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 class="mt-3">
<Footer />
<slot name="bottom"></slot>
</div>
</div>
@ -21,13 +21,23 @@
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { watch } from 'vue';
import { storeToRefs } from 'pinia'
import { useMercuryStore } from '../../stores/mercuryStore'
import Channels from './Channels.vue'
import Footer from './Footer.vue'
const mercuryStore = useMercuryStore()
const { connected } = storeToRefs(mercuryStore)
const configured = mercuryStore.configured()
const { mobile, showSidebar } = storeToRefs(mercuryStore)
</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>
<div class="d-flex flex-column">
<User :user="mercuryStore.user" />
<div v-for="user in users" :key="user.id" class="d-flex flex-row align-items-center">
<User :user="user" />
</div>
<User :user="mercuryStore.user" />
<div v-for="user in users" :key="user.id">
<User :user="user" />
</div>
</template>
@ -11,7 +9,7 @@
import { computed } from 'vue'
import { useMercuryStore } from '../../stores/mercuryStore'
import { useUserStore } from '../../stores/userStore'
import User from './User.vue'
import User from '../common/User.vue'
const props = defineProps(['channel'])
const channelId = computed(() => { return props.channel.id })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
<template v-slot:title>Welcome to Mercury!</template>
<template v-slot:content>
<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">
Mercury is a web-based BENNC client.
</h1>
@ -18,11 +18,13 @@
<script setup>
import Sidebar from '../components/sidebar/Sidebar.vue'
import Content from '../components/Content.vue'
import Content from '../components/content/Content.vue'
</script>
<style scoped>
#logo {
width: 25vw;
height: auto;
filter: invert(80%);
}
</style>

View File

@ -11,14 +11,12 @@
<AddChannel />
</template>
</Content>
</template>
<script setup>
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 ConfiguredChannels from '../components/channelSettings/ConfiguredChannels.vue'
import AddChannel from '../components/channelSettings/AddChannel.vue'
</script>