chore: Port huge amount of code to OliveTin 3k

This commit is contained in:
jamesread
2025-08-20 00:05:40 +01:00
parent 17c716c599
commit 6b342cbedb
59 changed files with 3973 additions and 3290 deletions

View File

@@ -1,27 +1,43 @@
<template>
<div :id="`actionButton-${actionId}`" role="none" class="action-button">
<button :id="`actionButtonInner-${actionId}`" :title="title" :disabled="!canExec || isDisabled"
:class="buttonClasses" @click="handleClick">
<span class="icon" v-html="unicodeIcon"></span>
<span class="title" aria-live="polite">{{ displayTitle }}</span>
</button>
<div :id="`actionButton-${actionId}`" role="none" class="action-button">
<button :id="`actionButtonInner-${actionId}`" :title="title" :disabled="!canExec || isDisabled"
:class="buttonClasses" @click="handleClick">
<ArgumentForm v-if="showArgumentForm" :action-data="actionData" @submit="handleArgumentSubmit"
@cancel="handleArgumentCancel" @close="handleArgumentClose" />
</div>
<div class="navigate-on-start-container">
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
<HugeiconsIcon :icon="ComputerTerminal01Icon" />
</div>
<div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start">
<HugeiconsIcon :icon="TypeCursorIcon" />
</div>
<div v-if="navigateOnStart == ''" class="navigate-on-start" title="Run in the background">
<HugeiconsIcon :icon="WorkoutRunIcon" />
</div>
</div>
<span class="icon" v-html="unicodeIcon"></span>
<span class="title" aria-live="polite">{{ displayTitle }}
</span>
</button>
</div>
</template>
<script setup>
import ArgumentForm from './ArgumentForm.vue'
import ArgumentForm from './views/ArgumentForm.vue'
import { buttonResults } from './stores/buttonResults'
import { useRouter } from 'vue-router'
import { HugeiconsIcon } from '@hugeicons/vue'
import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon } from '@hugeicons/core-free-icons'
import { ref, computed, watch, onMounted, inject } from 'vue'
import { ref, watch, onMounted, inject } from 'vue'
const executionDialog = inject('executionDialog');
const router = useRouter()
const navigateOnStart = ref('')
const props = defineProps({
actionData: {
type: Object,
required: true
type: Object,
required: true
}
})
@@ -46,16 +62,15 @@ const updateIterationTimestamp = ref(0)
function getUnicodeIcon(icon) {
if (icon === '') {
return '&#x1f4a9;'
return '&#x1f4a9;'
} else {
return unescape(icon)
return unescape(icon)
}
}
function constructFromJson(json) {
updateIterationTimestamp.value = 0
// Class attributes
updateFromJson(json)
actionId.value = json.id
@@ -63,6 +78,12 @@ function constructFromJson(json) {
canExec.value = json.canExec
popupOnStart.value = json.popupOnStart
if (popupOnStart.value.includes('execution-dialog')) {
navigateOnStart.value = 'pop'
} else if (props.actionData.arguments.length > 0) {
navigateOnStart.value = 'arg'
}
isDisabled.value = !json.canExec
displayTitle.value = title.value
unicodeIcon.value = getUnicodeIcon(json.icon)
@@ -77,29 +98,17 @@ function updateFromJson(json) {
async function handleClick() {
if (props.actionData.arguments && props.actionData.arguments.length > 0) {
updateUrlWithAction()
showArgumentForm.value = true
router.push(`/actionBinding/${props.actionData.bindingId}/argumentForm`)
} else {
await startAction()
await startAction()
}
}
function updateUrlWithAction() {
// Get the current URL and create a new URL object
const url = new URL(window.location.href)
// Set the action parameter
url.searchParams.set('action', title.value)
// Update the URL without reloading the page
window.history.replaceState({}, '', url.toString())
}
function getUniqueId() {
if (window.isSecureContext) {
return window.crypto.randomUUID()
return window.crypto.randomUUID()
} else {
return Date.now().toString()
return Date.now().toString()
}
}
@@ -107,70 +116,59 @@ async function startAction(actionArgs) {
buttonClasses.value = [] // Removes old animation classes
if (actionArgs === undefined) {
actionArgs = []
actionArgs = []
}
// UUIDs are create client side, so that we can setup a "execution-button"
// to track the execution before we send the request to the server.
const startActionArgs = {
actionId: actionId.value,
arguments: actionArgs,
uniqueTrackingId: getUniqueId()
bindingId: props.actionData.bindingId,
arguments: actionArgs,
uniqueTrackingId: getUniqueId()
}
onActionStarted(startActionArgs.uniqueTrackingId)
console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId)
watch(
() => buttonResults[startActionArgs.uniqueTrackingId],
(newResult, oldResult) => {
onLogEntryChanged(newResult)
}
)
try {
await window.client.startAction(startActionArgs)
await window.client.startAction(startActionArgs)
} catch (err) {
console.error('Failed to start action:', err)
console.error('Failed to start action:', err)
}
}
function onActionStarted(execTrackingId) {
console.log('onActionStarted', execTrackingId)
console.log('executionDialog', executionDialog)
function onLogEntryChanged(logEntry) {
if (logEntry.executionFinished) {
onExecutionFinished(logEntry)
} else {
onExecutionStarted(logEntry)
}
}
function onExecutionStarted(logEntry) {
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
if (executionDialog.value) {
executionDialog.value.reset();
if (popupOnStart.value === 'execution-dialog-stdout-only') {
executionDialog.value.hideEverythingApartFromOutput();
}
}
executionDialog.value.executionTrackingId = execTrackingId;
executionDialog.value.show()
router.push(`/logs/${logEntry.executionTrackingId}`)
}
isDisabled.value = true
}
function handleArgumentSubmit(args) {
startAction(args)
showArgumentForm.value = false
}
function handleArgumentCancel() {
showArgumentForm.value = false
}
function handleArgumentClose() {
showArgumentForm.value = false
}
// ExecutionFeedbackButton methods
function onExecutionFinished(logEntry) {
if (logEntry.timedOut) {
renderExecutionResult('action-timeout', 'Timed out')
renderExecutionResult('action-timeout', 'Timed out')
} else if (logEntry.blocked) {
renderExecutionResult('action-blocked', 'Blocked!')
renderExecutionResult('action-blocked', 'Blocked!')
} else if (logEntry.exitCode !== 0) {
renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
} else {
const ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
renderExecutionResult('action-success', 'Success!')
const ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
renderExecutionResult('action-success', 'Success!')
}
}
@@ -181,9 +179,9 @@ function renderExecutionResult(resultCssClass, temporaryStatusMessage) {
function updateDom(resultCssClass, newTitle) {
if (resultCssClass == null) {
buttonClasses.value = []
buttonClasses.value = []
} else {
buttonClasses.value = [resultCssClass]
buttonClasses.value = [resultCssClass]
}
displayTitle.value = newTitle
@@ -193,7 +191,7 @@ function onExecStatusChanged() {
isDisabled.value = false
setTimeout(() => {
updateDom(null, title.value)
updateDom(null, title.value)
}, 2000)
}
@@ -204,84 +202,93 @@ onMounted(() => {
watch(
() => props.actionData,
(newData) => {
updateFromJson(newData)
updateFromJson(newData)
},
{ deep: true }
)
defineExpose({
onExecutionFinished
})
</script>
<style scoped>
.action-button {
display: flex;
flex-direction: column;
flex-grow: 1;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.action-button button {
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
gap: 0.5em;
padding: 0.5em 1em;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 0 .6em #aaa;
font-size: .85em;
border-radius: .7em;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
padding: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 0 .6em #aaa;
font-size: .85em;
border-radius: .7em;
}
.action-button button:hover:not(:disabled) {
background: #f5f5f5;
border-color: #999;
background: #f5f5f5;
border-color: #999;
}
.action-button button:disabled {
opacity: 0.6;
cursor: not-allowed;
opacity: 0.6;
cursor: not-allowed;
}
.action-button button .icon {
font-size: 3em;
font-size: 3em;
flex-grow: 1;
align-content: center;
}
.action-button button .title {
font-weight: 500;
font-weight: 500;
padding: 0.2em;
}
/* Animation classes */
.action-button button.action-timeout {
background: #fff3cd;
border-color: #ffeaa7;
color: #856404;
background: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.action-button button.action-blocked {
background: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
background: #f8d7da !important;
border-color: #f5c6cb;
color: #721c24;
}
.action-button button.action-nonzero-exit {
background: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
background: #f8d7da !important;
border-color: #f5c6cb;
color: #721c24;
}
.action-button button.action-success {
background: #d4edda;
border-color: #c3e6cb;
color: #155724;
background: #d4edda !important;
border-color: #c3e6cb;
color: #155724;
}
.action-button-footer {
margin-top: 0.5em;
margin-top: 0.5em;
}
</style>
.navigate-on-start-container {
position: relative;
margin-left: auto;
height: 0;
right: 0;
top: 0;
}
</style>