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

@@ -138,84 +138,88 @@ pub async fn find_java17_jres() -> crate::Result<Vec<JavaVersion>> {
}
pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
let state = State::get().await?;
Box::pin(
async move {
let state = State::get().await?;
let loading_bar = init_loading(
LoadingBarType::JavaDownload {
version: java_version,
},
100.0,
"Downloading java version",
)
.await?;
#[derive(Deserialize)]
struct Package {
pub download_url: String,
pub name: PathBuf,
}
emit_loading(&loading_bar, 0.0, Some("Fetching java version")).await?;
let packages = fetch_json::<Vec<Package>>(
Method::GET,
&format!(
"https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jre&page_size=1",
std::env::consts::ARCH, java_version, std::env::consts::OS
),
None,
None,
&state.fetch_semaphore,
).await?;
emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
if let Some(download) = packages.first() {
let file = fetch_advanced(
Method::GET,
&download.download_url,
None,
None,
None,
Some((&loading_bar, 80.0)),
&state.fetch_semaphore,
)
.await?;
let path = state.directories.java_versions_dir();
if path.exists() {
tokio::fs::remove_dir_all(&path).await?;
}
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read java zip".to_string(),
))
})?;
emit_loading(&loading_bar, 0.0, Some("Extracting java")).await?;
archive.extract(&path).map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to extract java zip".to_string(),
))
})?;
emit_loading(&loading_bar, 100.0, Some("Done extracting java")).await?;
Ok(path
.join(
download
.name
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
let loading_bar = init_loading(
LoadingBarType::JavaDownload {
version: java_version,
},
100.0,
"Downloading java version",
)
.join(format!("zulu-{}.jre/Contents/Home/bin/java", java_version)))
} else {
Err(crate::ErrorKind::LauncherError(format!(
"No Java Version found for Java version {}, OS {}, and Architecture {}",
java_version, std::env::consts::OS, std::env::consts::ARCH,
)).into())
}
.await?;
#[derive(Deserialize)]
struct Package {
pub download_url: String,
pub name: PathBuf,
}
emit_loading(&loading_bar, 0.0, Some("Fetching java version")).await?;
let packages = fetch_json::<Vec<Package>>(
Method::GET,
&format!(
"https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jre&page_size=1",
std::env::consts::ARCH, java_version, std::env::consts::OS
),
None,
None,
&state.fetch_semaphore,
).await?;
emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
if let Some(download) = packages.first() {
let file = fetch_advanced(
Method::GET,
&download.download_url,
None,
None,
None,
Some((&loading_bar, 80.0)),
&state.fetch_semaphore,
)
.await?;
let path = state.directories.java_versions_dir();
if path.exists() {
tokio::fs::remove_dir_all(&path).await?;
}
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read java zip".to_string(),
))
})?;
emit_loading(&loading_bar, 0.0, Some("Extracting java")).await?;
archive.extract(&path).map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to extract java zip".to_string(),
))
})?;
emit_loading(&loading_bar, 100.0, Some("Done extracting java")).await?;
Ok(path
.join(
download
.name
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
)
.join(format!("zulu-{}.jre/Contents/Home/bin/java", java_version)))
} else {
Err(crate::ErrorKind::LauncherError(format!(
"No Java Version found for Java version {}, OS {}, and Architecture {}",
java_version, std::env::consts::OS, std::env::consts::ARCH,
)).into())
}
}
).await
}
// Get all JREs that exist on the system

View File

@@ -5,44 +5,64 @@ use tokio::fs::read_to_string;
#[derive(Serialize, Deserialize, Debug)]
pub struct Logs {
pub datetime_string: String,
pub stdout: String,
pub stderr: String,
pub stdout: Option<String>,
pub stderr: Option<String>,
}
impl Logs {
async fn build(
profile_uuid: uuid::Uuid,
datetime_string: String,
clear_contents: Option<bool>,
) -> crate::Result<Self> {
Ok(Self {
stdout: get_stdout_by_datetime(profile_uuid, &datetime_string)
.await?,
stderr: get_stderr_by_datetime(profile_uuid, &datetime_string)
.await?,
stdout: if clear_contents.unwrap_or(false) {
None
} else {
Some(
get_stdout_by_datetime(profile_uuid, &datetime_string)
.await?,
)
},
stderr: if clear_contents.unwrap_or(false) {
None
} else {
Some(
get_stderr_by_datetime(profile_uuid, &datetime_string)
.await?,
)
},
datetime_string,
})
}
}
#[tracing::instrument]
pub async fn get_logs(profile_uuid: uuid::Uuid) -> crate::Result<Vec<Logs>> {
pub async fn get_logs(
profile_uuid: uuid::Uuid,
clear_contents: Option<bool>,
) -> crate::Result<Vec<Logs>> {
let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
let mut logs = Vec::new();
for entry in std::fs::read_dir(logs_folder)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(datetime_string) = path.file_name() {
logs.push(
Logs::build(
profile_uuid,
datetime_string.to_string_lossy().to_string(),
)
.await,
);
if logs_folder.exists() {
for entry in std::fs::read_dir(logs_folder)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(datetime_string) = path.file_name() {
logs.push(
Logs::build(
profile_uuid,
datetime_string.to_string_lossy().to_string(),
clear_contents,
)
.await,
);
}
}
}
}
let mut logs = logs.into_iter().collect::<crate::Result<Vec<Logs>>>()?;
logs.sort_by_key(|x| x.datetime_string.clone());
Ok(logs)
@@ -54,8 +74,12 @@ pub async fn get_logs_by_datetime(
datetime_string: String,
) -> crate::Result<Logs> {
Ok(Logs {
stdout: get_stdout_by_datetime(profile_uuid, &datetime_string).await?,
stderr: get_stderr_by_datetime(profile_uuid, &datetime_string).await?,
stdout: Some(
get_stdout_by_datetime(profile_uuid, &datetime_string).await?,
),
stderr: Some(
get_stderr_by_datetime(profile_uuid, &datetime_string).await?,
),
datetime_string,
})
}

View File

@@ -187,24 +187,27 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
#[tracing::instrument]
async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
{
let state = State::get().await.map_err(|_| JREError::StateError)?;
Box::pin(async move {
let state = State::get().await.map_err(|_| JREError::StateError)?;
let mut jre_paths = HashSet::new();
let base_path = state.directories.java_versions_dir();
let mut jre_paths = HashSet::new();
let base_path = state.directories.java_versions_dir();
if base_path.is_dir() {
for entry in std::fs::read_dir(base_path)? {
let entry = entry?;
let file_path = entry.path().join("bin");
let contents = std::fs::read_to_string(file_path)?;
if base_path.is_dir() {
for entry in std::fs::read_dir(base_path)? {
let entry = entry?;
let file_path = entry.path().join("bin");
let contents = std::fs::read_to_string(file_path)?;
let entry = entry.path().join(contents);
println!("{:?}", entry);
jre_paths.insert(entry);
let entry = entry.path().join(contents);
println!("{:?}", entry);
jre_paths.insert(entry);
}
}
}
Ok(jre_paths)
Ok(jre_paths)
})
.await
}
// Gets all JREs from the PATH env variable

View File

@@ -15,7 +15,7 @@
"dependencies": {
"@tauri-apps/api": "^1.2.0",
"ofetch": "^1.0.1",
"omorphia": "^0.4.10",
"omorphia": "^0.4.13",
"pinia": "^2.0.33",
"vite-svg-loader": "^4.0.0",
"vue": "^3.2.45",

View File

@@ -14,8 +14,17 @@ pub struct Logs {
/// Get all Logs for a profile, sorted by datetime
#[tauri::command]
pub async fn logs_get_logs(profile_uuid: Uuid) -> Result<Vec<Logs>> {
Ok(logs::get_logs(profile_uuid).await?)
pub async fn logs_get_logs(
profile_uuid: Uuid,
clear_contents: Option<bool>,
) -> Result<Vec<Logs>> {
use std::time::Instant;
let now = Instant::now();
let val = logs::get_logs(profile_uuid, clear_contents).await?;
let elapsed = now.elapsed();
println!("Elapsed: {:.2?}", elapsed);
Ok(val)
}
/// Get a Log struct for a profile by profile id and datetime string

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>

View File

@@ -1149,10 +1149,10 @@ ofetch@^1.0.1:
node-fetch-native "^1.0.2"
ufo "^1.1.0"
omorphia@^0.4.10:
version "0.4.10"
resolved "https://registry.yarnpkg.com/omorphia/-/omorphia-0.4.10.tgz#93c0e6a08a233f27d76587286e42450af44bb55d"
integrity sha512-WgSFosOqoM0IRpzGNYyprfZSRyBLgqs6sTmKRuWo96ZpzrHRWAom2upIm/HAxAC+YBwFni5sgUeBemXYI7wmuw==
omorphia@^0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/omorphia/-/omorphia-0.4.13.tgz#6141886b9c332e4a28afe31a743f0c85d4a09efe"
integrity sha512-Yb76WoM4e42aAq3G/OPxQS6whCu+WIHVBhJxSzmUUycF1Pvf6tJZov+LefneSkk4xcQAjDZsgK8VOVD7q/siig==
dependencies:
dayjs "^1.11.7"
floating-vue "^2.0.0-beta.20"