Logs wireup (#116)

* wireup

* Added live logs

* Finish up wireup

* Run linter

* finish most

* Fix most issues

* Finish page

* run lint

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Adrian O.V
2023-05-17 16:05:20 -04:00
committed by GitHub
parent c6e2133e15
commit 0801d7a145
10 changed files with 336 additions and 192 deletions

View File

@@ -40,6 +40,10 @@ a {
}
}
input {
border: none;
}
.multiselect {
color: var(--color-base) !important;
outline: 2px solid transparent;

View File

@@ -17,8 +17,8 @@ pub struct Logs {
/// Get all logs that exist for a given profile
/// This is returned as an array of Log objects, sorted by datetime_string (the folder name, when the log was created)
export async function get_logs(profileUuid) {
return await invoke('logs_get_logs', { profileUuid })
export async function get_logs(profileUuid, clearContents) {
return await invoke('logs_get_logs', { profileUuid, clearContents })
}
/// Get a profile's log by datetime_string (the folder name, when the log was created)

View File

@@ -63,7 +63,13 @@
</div>
<div class="content">
<Promotion />
<router-view :instance="instance" />
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
<component :is="Component" :instance="instance"></component>
</Suspense>
</template>
</RouterView>
</div>
</div>
</template>
@@ -81,7 +87,7 @@ import { useRoute } from 'vue-router'
import { ref, onUnmounted } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { open } from '@tauri-apps/api/dialog'
import { useBreadcrumbs, useSearch } from '@/store/state'
import { useBreadcrumbs, useLoading, useSearch } from '@/store/state'
const route = useRoute()
const searchStore = useSearch()
@@ -96,6 +102,8 @@ breadcrumbs.setContext({
link: route.path,
})
const loadingBar = useLoading()
const uuid = ref(null)
const playing = ref(false)
const loading = ref(false)
@@ -240,15 +248,18 @@ Button {
width: 100%;
color: var(--color-primary);
padding: var(--gap-md);
box-shadow: none;
&.router-link-exact-active {
box-shadow: var(--shadow-inset-lg);
background: var(--color-button-bg);
color: var(--color-contrast);
}
&:hover {
background-color: var(--color-button-bg);
color: var(--color-contrast);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
box-shadow: var(--shadow-inset-lg);
text-decoration: none;
}

View File

@@ -1,92 +1,177 @@
<template>
<Card class="log-card">
<div class="button-row">
<DropdownSelect :options="['logs/latest.log']" />
<DropdownSelect
name="Log date"
:model-value="logs[selectedLogIndex]"
:options="logs"
:display-name="(option) => option?.name"
:disabled="logs.length === 0"
@change="(value) => (selectedLogIndex = value.index)"
/>
<div class="button-group">
<Button>
<ClipboardCopyIcon />
Copy
<Button :disabled="!logs[selectedLogIndex]" @click="copyLog()">
<ClipboardCopyIcon v-if="!copied" />
<CheckIcon v-else />
{{ copied ? 'Copied' : 'Copy' }}
</Button>
<Button color="primary">
<Button disabled color="primary">
<SendIcon />
Share
</Button>
<Button color="danger">
<Button
:disabled="!logs[selectedLogIndex] || logs[selectedLogIndex].live === true"
color="danger"
@click="deleteLog()"
>
<TrashIcon />
Delete
</Button>
</div>
</div>
<div class="log-text">
<div v-for="(line, index) in fileContents.value.split('\n')" :key="index">{{ line }}</div>
<div ref="logContainer" class="log-text">
<!-- {{ logs[1] }}-->
<div v-for="line in logs[selectedLogIndex]?.stdout.split('\n')" :key="line" class="no-wrap">
{{ line }}
</div>
</div>
</Card>
</template>
<script setup>
import { Card, Button, TrashIcon, SendIcon, ClipboardCopyIcon, DropdownSelect } from 'omorphia'
</script>
<script>
export default {
data() {
return {
fileContents: {
value:
"'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' +
'[22:13:02] [Server thread/INFO]: venashial left the game\n' +
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' +
'[22:13:02] [Server thread/INFO]: Stopping server\n' +
'[22:13:02] [Server thread/INFO]: Saving players\n' +
'[22:13:02] [Server thread/INFO]: Saving worlds\n' +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n' +
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' +
'[22:13:07] [Render thread/INFO]: Stopping!\n' +
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...\n' +
"'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' +
'[22:13:02] [Server thread/INFO]: venashial left the game\n' +
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' +
'[22:13:02] [Server thread/INFO]: Stopping server\n' +
'[22:13:02] [Server thread/INFO]: Saving players\n' +
'[22:13:02] [Server thread/INFO]: Saving worlds\n' +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n' +
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' +
'[22:13:07] [Render thread/INFO]: Stopping!\n' +
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...\n' +
"'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' +
'[22:13:02] [Server thread/INFO]: venashial left the game\n' +
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' +
'[22:13:02] [Server thread/INFO]: Stopping server\n' +
'[22:13:02] [Server thread/INFO]: Saving players\n' +
'[22:13:02] [Server thread/INFO]: Saving worlds\n' +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n' +
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' +
'[22:13:07] [Render thread/INFO]: Stopping!\n' +
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...',
},
}
import {
Button,
Card,
CheckIcon,
ClipboardCopyIcon,
DropdownSelect,
SendIcon,
TrashIcon,
} from 'omorphia'
import { delete_logs_by_datetime, get_logs, get_stdout_by_datetime } from '@/helpers/logs.js'
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
import dayjs from 'dayjs'
import calendar from 'dayjs/plugin/calendar'
import { get_stdout_by_uuid, get_uuids_by_profile_path } from '@/helpers/process.js'
import { useRoute } from 'vue-router'
import { process_listener } from '@/helpers/events.js'
dayjs.extend(calendar)
const route = useRoute()
const props = defineProps({
instance: {
type: Object,
required: true,
},
})
async function getLiveLog() {
const uuids = await get_uuids_by_profile_path(route.params.id)
let returnValue
if (uuids.length === 0) {
returnValue = 'No live game detected. \nStart your game to proceed'
} else {
returnValue = await get_stdout_by_uuid(uuids[0])
}
return { name: 'Live Log', stdout: returnValue, live: true }
}
async function getLogs() {
return (await get_logs(props.instance.uuid, true)).reverse().map((log) => {
log.name = dayjs(
log.datetime_string.slice(0, 8) + 'T' + log.datetime_string.slice(9)
).calendar()
log.stdout = 'Loading...'
return log
})
}
async function setLogs() {
const [liveLog, allLogs] = await Promise.all([getLiveLog(), getLogs()])
logs.value = [liveLog, ...allLogs]
}
const logs = ref([])
await setLogs()
const selectedLogIndex = ref(0)
const copied = ref(false)
const copyLog = () => {
if (logs.value[selectedLogIndex.value]) {
navigator.clipboard.writeText(logs.value[selectedLogIndex.value].stdout)
copied.value = true
}
}
watch(selectedLogIndex, async (newIndex) => {
copied.value = false
userScrolled.value = false
if (newIndex !== 0) {
logs.value[newIndex].stdout = 'Loading...'
logs.value[newIndex].stdout = await get_stdout_by_datetime(
props.instance.uuid,
logs.value[newIndex].datetime_string
)
}
})
const deleteLog = async () => {
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
let deleteIndex = selectedLogIndex.value
selectedLogIndex.value = deleteIndex - 1
await delete_logs_by_datetime(props.instance.uuid, logs.value[deleteIndex].datetime_string)
await setLogs()
}
}
const logContainer = ref(null)
const interval = ref(null)
const userScrolled = ref(false)
const isAutoScrolling = ref(false)
function handleUserScroll() {
if (!isAutoScrolling.value) {
userScrolled.value = true
}
}
interval.value = setInterval(async () => {
if (logs.value.length > 0) {
logs.value[0] = await getLiveLog()
if (selectedLogIndex.value === 0 && !userScrolled.value) {
await nextTick()
isAutoScrolling.value = true
logContainer.value.scrollTop =
logContainer.value.scrollHeight - logContainer.value.offsetHeight
setTimeout(() => (isAutoScrolling.value = false), 50)
}
}
}, 250)
const unlistenProcesses = await process_listener(async (e) => {
if (e.event === 'finished') {
userScrolled.value = false
await setLogs()
}
})
onMounted(() => {
logContainer.value.addEventListener('scroll', handleUserScroll)
})
onBeforeUnmount(() => {
logContainer.value.removeEventListener('scroll', handleUserScroll)
})
onUnmounted(() => {
clearInterval(interval.value)
unlistenProcesses()
})
</script>
<style scoped lang="scss">
@@ -94,11 +179,14 @@ export default {
display: flex;
flex-direction: column;
gap: 1rem;
height: calc(100vh - 11rem);
}
.button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 0.5rem;
}
.button-group {
@@ -109,7 +197,7 @@ export default {
.log-text {
width: 100%;
aspect-ratio: 2/1;
height: 100%;
font-family: var(--mono-font);
background-color: var(--color-accent-contrast);
color: var(--color-contrast);
@@ -117,5 +205,6 @@ export default {
padding: 1.5rem;
overflow: auto;
white-space: normal;
color-scheme: dark;
}
</style>