minor design update

This commit is contained in:
arabcoders 2025-06-13 21:22:55 +03:00
parent fb51fc3b1f
commit a478e8e58d
13 changed files with 320 additions and 256 deletions

View file

@ -1,40 +0,0 @@
import logging
from .config import Config
from .DownloadQueue import DownloadQueue
from .encoder import Encoder
from .ItemDTO import Item
LOG = logging.getLogger("common")
class Common:
"""
This class is used to share common methods between the socket and the API gateways.
"""
def __init__(
self,
queue: DownloadQueue | None = None,
encoder: Encoder | None = None,
config: Config | None = None,
):
super().__init__()
self.queue: DownloadQueue = queue or DownloadQueue.get_instance()
self.encoder: Encoder = encoder or Encoder()
config: Config = config or Config.get_instance()
self.default_preset: str = config.default_preset
async def add(self, item: Item) -> dict[str, str]:
"""
Add an item to the download queue.
Args:
item (Item): The item to be added to the queue.
Returns:
dict[str, str]: The status of the download.
{ "status": "text" }
"""
return await self.queue.add(item=item)

8
ui/@types/logs.ts Normal file
View file

@ -0,0 +1,8 @@
type log_line = {
id: number,
line: string,
datetime?: string,
}
export type { log_line };

17
ui/@types/tasks.ts Normal file
View file

@ -0,0 +1,17 @@
type task_item = {
id: string,
name: string,
url: string,
preset?: string,
folder?: string,
template?: string,
cli?: string,
timer?: string,
in_progress?: boolean,
}
type exported_task = task_item & { _type: string, _version: string }
type error_response = { error: string }
export type { task_item, exported_task, error_response };

View file

@ -7,6 +7,7 @@
width: 80vw;
}
</style>
<template>
<div class="content">
<h1 class="has-text-white">Not downloaded yet.</h1>
@ -14,10 +15,8 @@
</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
const props = defineProps({
<script setup lang="ts">
defineProps({
url: {
type: String,
required: true,
@ -26,12 +25,13 @@ const props = defineProps({
const emitter = defineEmits(['closeModel'])
const eventFunc = e => {
if (e.key === 'Escape') {
emitter('closeModel')
const handle_event = (e: KeyboardEvent) => {
if (e.key !== 'Escape') {
return
}
emitter('closeModel')
}
onMounted(async () => window.addEventListener('keydown', eventFunc))
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
onMounted(() => document.addEventListener('keydown', handle_event))
onBeforeUnmount(() => document.removeEventListener('keydown', handle_event))
</script>

View file

@ -1,47 +1,50 @@
<template>
<div>
<div class="modal is-active" v-if="false === externalModel">
<div class="modal-background" @click="closeModal"></div>
<div class="modal-background" @click="emitter('closeModel')"></div>
<div class="modal-content" style="width:60vw;">
<div style="font-size:30vh; width: 99%" class="has-text-centered" v-if="isLoading">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
<div v-else>
<div class="p-0 m-0" style="position: relative">
<div class="content" style="white-space: pre;">
<code class="p-4 is-block" style="overflow: scroll;" v-text="data" />
<button class="button is-small m-4" @click="() => copyText(JSON.stringify(data, null, 4))"
<div class="content p-0 m-0" style="position: relative">
<pre><code class="p-4 is-block" v-text="data" />
<button class="button m-4" @click="() => copyText(JSON.stringify(data, null, 4))"
style="position: absolute; top:0; right:0;">
<span class="icon"><i class="fas fa-copy"></i></span>
</button>
</div>
</pre>
</div>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="closeModal"></button>
<button class="modal-close is-large" aria-label="close" @click="emitter('closeModel')"></button>
</div>
<div style="width:70vw; height: 80vh;" v-else>
<div class="p-0 m-0" style="position: relative">
<div class="content" style="white-space: pre;">
<code class="p-4 is-block" v-text="data" />
<button class="button is-small m-4" @click="() => copyText(JSON.stringify(data, null, 4))"
style="position: absolute; top:0; right:0;">
<span class="icon"><i class="fas fa-copy"></i></span>
</button>
</div>
<div class="content p-0 m-0" style="position: relative">
<pre><code class="p-4 is-block" v-text="data" /></pre>
<button class="button m-4" @click="() => copyText(JSON.stringify(data, null, 4))"
style="position: absolute; top:0; right:0;">
<span class="icon"><i class="fas fa-copy"></i></span>
</button>
</div>
</div>
</div>
</template>
<script setup>
<style scoped>
code {
color: var(--bulma-code) !important
}
</style>
<script setup lang="ts">
import { request } from '~/utils/index'
const emitter = defineEmits(['closeModel'])
const isLoading = ref(false)
const data = ref({})
const toast = useNotification()
const emitter = defineEmits(['closeModel'])
const isLoading = ref<Boolean>(false)
const data = ref<any>({})
const props = defineProps({
link: {
type: String,
@ -60,16 +63,15 @@ const props = defineProps({
},
})
const closeModal = () => emitter('closeModel')
const eventFunc = e => {
if (e.key === 'Escape') {
emitter('closeModel')
const handle_event = (e: KeyboardEvent) => {
if (e.key !== 'Escape') {
return
}
emitter('closeModel')
}
onMounted(async () => {
window.addEventListener('keydown', eventFunc)
document.addEventListener('keydown', handle_event)
const url = props.useUrl ? props.link : '/api/yt-dlp/url/info?url=' + encodePath(props.link)
@ -84,7 +86,7 @@ onMounted(async () => {
data.value = body
}
} catch (e) {
} catch (e: any) {
console.error(e)
toast.error(`Error: ${e.message}`)
} finally {
@ -92,5 +94,5 @@ onMounted(async () => {
}
})
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
onBeforeUnmount(() => document.removeEventListener('keydown', handle_event))
</script>

View file

@ -1,30 +1,31 @@
<style>
.is-image {
<style scoped>
img {
max-width: 100%;
max-height: 100%;
margin: auto;
display: block;
}
</style>
<template>
<div>
<div style="font-size:30vh; width: 99%" class="has-text-centered" v-if="isLoading">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
<div v-else>
<img :src="image" class="is-image">
<img :src="image">
</div>
</div>
</template>
<script setup>
<script setup lang="ts">
import { request } from '~/utils/index'
const emitter = defineEmits(['closeModel'])
const isLoading = ref(false)
const data = ref({})
const toast = useNotification()
const image = ref('')
const emitter = defineEmits(['closeModel'])
const isLoading = ref<boolean>(false)
const image = ref<string>('')
const props = defineProps({
link: {
@ -33,15 +34,15 @@ const props = defineProps({
},
})
const eventFunc = e => {
if ('Escape' === e.key) {
emitter('closeModel')
const handle_event = (e: KeyboardEvent) => {
if (e.key !== 'Escape') {
return
}
emitter('closeModel')
}
onMounted(async () => {
window.addEventListener('keydown', eventFunc)
document.addEventListener('keydown', handle_event)
const url = props.link.startsWith('/') ? props.link : '/api/thumbnail?url=' + encodePath(props.link)
@ -54,7 +55,7 @@ onMounted(async () => {
}
image.value = URL.createObjectURL(await imgRequest.blob())
} catch (e) {
} catch (e: any) {
console.error(e)
toast.error(`Error: ${e.message}`)
} finally {
@ -62,5 +63,5 @@ onMounted(async () => {
}
})
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
onBeforeUnmount(() => document.removeEventListener('keydown', handle_event))
</script>

View file

@ -2,19 +2,19 @@
<div>
<div class="modal is-active">
<div class="model-title" v-if="title" />
<div class="modal-background" @click="closeModal"></div>
<div class="modal-background" @click="emitter('close')"></div>
<div class="modal-content" style="width:70vw;">
<slot />
</div>
<button class="modal-close is-large" aria-label="close" @click="closeModal"></button>
<button class="modal-close is-large" aria-label="close" @click="emitter('close')"></button>
</div>
</div>
</template>
<script setup>
<script setup lang="ts">
const emitter = defineEmits(['close'])
const props = defineProps({
defineProps({
title: {
type: String,
default: '',
@ -22,14 +22,13 @@ const props = defineProps({
},
})
const closeModal = () => emitter('close')
const eventFunc = e => {
if (e.key === 'Escape') {
emitter('close')
const handle_event = (e: KeyboardEvent) => {
if (e.key !== 'Escape') {
return
}
emitter('close')
}
onMounted(() => window.addEventListener('keydown', eventFunc))
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
onMounted(() => document.addEventListener('keydown', handle_event))
onBeforeUnmount(() => document.removeEventListener('keydown', handle_event))
</script>

View file

@ -10,8 +10,8 @@
</style>
<template>
<div v-if="infoLoaded">
<video class="player" ref="video" :poster="uri(thumbnail)" :title="title" playsinline controls crossorigin="anonymous"
preload="auto" autoplay>
<video class="player" ref="video" :poster="uri(thumbnail)" :title="title" playsinline controls
crossorigin="anonymous" preload="auto" autoplay>
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
:type="source.type" />
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
@ -25,11 +25,35 @@
</div>
</template>
<script setup>
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { onMounted, onUpdated, ref, onUnmounted } from 'vue'
import Hls from 'hls.js'
import { makeDownload } from '~/utils/index'
type video_track_element = {
file: string,
kind: string,
label: string,
lang: string,
}
type video_source_element = {
src: string,
type: string,
onerror: (e: Event) => void,
}
type file_info = {
title: string,
mimetype: string,
sidecar?: {
image?: Array<{ file: string }>,
text?: Array<{ file: string }>,
subtitle?: Array<{ name: string, lang: string, file: string }>,
}
error?: string,
}
const config = useConfigStore()
const toast = useNotification()
@ -42,9 +66,9 @@ const props = defineProps({
const emitter = defineEmits(['closeModel'])
const video = ref(null)
const tracks = ref([])
const sources = ref([])
const video = useTemplateRef<HTMLMediaElement>('video')
const tracks = ref<Array<video_track_element>>([])
const sources = ref<Array<video_source_element>>([])
const thumbnail = ref('/images/placeholder.png')
const artist = ref('')
@ -54,19 +78,39 @@ const isApple = /(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent)
const volume = useStorage('player_volume', 1)
const notFirefox = !navigator.userAgent.toLowerCase().includes('firefox')
const infoLoaded = ref(false)
const destroyed = ref(false)
let hls = null
let hls: Hls | null = null
const eventFunc = e => {
if ('Escape' === e.key) {
emitter('closeModel')
const handle_event = (e: KeyboardEvent) => {
if (e.key !== 'Escape') {
return
}
emitter('closeModel')
}
const volume_change_handler = () => {
if (!video.value) {
return
}
volume.value = video.value.volume
if (false === ("mediaSession" in navigator)) {
return
}
navigator.mediaSession.setPositionState({
duration: video.value.duration,
playbackRate: video.value.playbackRate,
position: video.value.currentTime,
})
}
onMounted(async () => {
const req = await request(makeDownload(config, props.item, 'api/file/info'))
const response = await req.json()
const response: file_info = await req.json()
if (!req.ok) {
toast.error(`Failed to fetch video info. ${response?.error}`)
@ -91,13 +135,13 @@ onMounted(async () => {
sources.value.push({
src: makeDownload(config, props.item, allowedCodec ? 'api/download' : 'm3u8'),
type: allowedCodec ? response.mimetype : 'application/x-mpegURL',
onerror: e => src_error(e),
onerror: (e: Event) => src_error(),
})
} else {
sources.value.push({
src: makeDownload(config, props.item, 'api/download'),
type: response.mimetype,
onerror: e => src_error(e),
onerror: e => src_error(),
})
}
@ -122,7 +166,7 @@ onMounted(async () => {
isAudio.value = true
}
response?.sidecar?.subtitle?.forEach((cap, id) => {
response.sidecar?.subtitle?.forEach((cap, _) => {
tracks.value.push({
kind: "captions",
label: cap.name,
@ -139,30 +183,35 @@ onMounted(async () => {
await nextTick()
prepareVideoPlayer()
window.addEventListener('keydown', eventFunc)
document.addEventListener('keydown', handle_event)
})
onUpdated(() => prepareVideoPlayer())
onUnmounted(() => {
onBeforeUnmount(() => {
if (hls) {
hls.destroy()
}
window.removeEventListener('keydown', eventFunc)
document.removeEventListener('keydown', handle_event)
if (title.value) {
window.document.title = 'YTPTube'
}
if (video.value) {
try {
video.value.pause()
video.value.querySelectorAll("source").forEach(source => source.removeAttribute("src"))
video.value.load()
}
catch (e) {
console.error(e)
}
if (!video.value) {
return
}
destroyed.value = true
try {
video.value.pause()
video.value.querySelectorAll("source").forEach(source => source.removeAttribute("src"))
video.value.removeEventListener('volumechange', volume_change_handler)
video.value.load()
}
catch (e) {
console.error(e)
}
})
@ -175,9 +224,7 @@ const prepareVideoPlayer = () => {
return
}
let mediaMetadata = {
title: title.value,
}
let mediaMetadata: MediaMetadataInit = { title: title.value }
if (thumbnail.value) {
mediaMetadata.artwork = [{ src: thumbnail.value, sizes: '1920x1080', type: 'image/jpeg' }]
@ -192,22 +239,31 @@ const prepareVideoPlayer = () => {
window.document.title = `YTPTube - Playing: ${title.value}`
}
video.value.volume = volume.value
if (!video.value) {
return
}
video.value.addEventListener('volumechange', () => {
volume.value = video.value.volume
})
video.value.volume = volume.value
video.value.addEventListener('volumechange', volume_change_handler)
}
const src_error = () => {
const src_error = async () => {
if (hls) {
return
}
await nextTick()
if (destroyed.value) {
return
}
console.warn('Direct play failed, trying HLS.')
attach_hls(makeDownload(config, props.item, 'm3u8'))
}
const attach_hls = link => {
const attach_hls = (link: string) => {
if (!video.value) {
return
}
hls = new Hls({
debug: false,
enableWorker: true,

View file

@ -60,17 +60,17 @@
</a>
<div class="navbar-dropdown">
<NuxtLink class="navbar-item" to="/console" @click.native="e => changeRoute(e)"
v-if="config.app.console_enabled">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>Terminal</span>
</NuxtLink>
<NuxtLink class="navbar-item" to="/logs" @click.native="e => changeRoute(e)"
v-if="config.app.file_logging">
<span class="icon"><i class="fa-solid fa-file-lines" /></span>
<span>Logs</span>
</NuxtLink>
<NuxtLink class="navbar-item" to="/console" @click.native="e => changeRoute(e)"
v-if="config.app.console_enabled">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>Console</span>
</NuxtLink>
</div>
</div>

View file

@ -340,7 +340,7 @@ const reloadContent = async (dir = '/', fromMounted = false) => {
}
}
const popstateHandler = e => {
const event_handler = e => {
if (!e.state) {
return
@ -358,12 +358,10 @@ onMounted(async () => {
await reloadContent(path.value, true)
}
window.addEventListener('popstate', popstateHandler)
document.addEventListener('popstate', event_handler)
})
onUnmounted(() => {
window.removeEventListener('popstate', popstateHandler)
})
onBeforeUnmount(() => document.removeEventListener('popstate', event_handler))
const makeBreadCrumb = path => {
const baseLink = '/'

View file

@ -10,11 +10,11 @@
<h1 class="title is-4">
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>Terminal</span>
<span>Console</span>
</span>
</h1>
<div class="subtitle is-6 is-unselectable">
You can use this terminal window to execute non-interactive commands. The interface is jailed to the
You can use this console window to execute non-interactive commands. The interface is jailed to the
<code>yt-dlp</code>
</div>
</div>
@ -25,8 +25,9 @@
<span class="icon"><i class="fa-solid fa-terminal" /></span> Console Output
</p>
<p class="card-header-icon">
<span v-tooltip.top="'Clear console window'" class="icon" @click="clearOutput">
<i class="fa-solid fa-broom" /></span>
<span v-tooltip.top="'Clear console window'" class="icon" @click="clearOutput()">
<i class="fa-solid fa-broom" />
</span>
</p>
</header>
<section class="card-content p-0 m-0">
@ -34,12 +35,6 @@
</section>
<section class="card-content p-1 m-1">
<div class="field is-grouped">
<div class="control">
<span class="icon-text input is-unselectable">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>yt-dlp</span>
</span>
</div>
<div class="control is-expanded">
<input type="text" class="input" v-model="command" placeholder="--help" autocomplete="off"
ref="command_input" @keydown.enter="runCommand" :disabled="isLoading" id="command">
@ -60,22 +55,24 @@
</div>
</template>
<script setup>
<script setup lang="ts">
import "@xterm/xterm/css/xterm.css"
import { Terminal } from "@xterm/xterm"
import { FitAddon } from "@xterm/addon-fit"
import { useStorage } from '@vueuse/core'
const terminal = ref()
const terminalFit = ref()
const command = ref('')
const terminal_window = ref()
const command_input = ref()
const isLoading = ref(false)
const config = useConfigStore()
const socket = useSocketStore()
const bg_enable = useStorage('random_bg', true)
const bg_opacity = useStorage('random_bg_opacity', 0.85)
const bg_enable = useStorage<boolean>('random_bg', true)
const bg_opacity = useStorage<number>('random_bg_opacity', 0.85)
const terminal = ref<Terminal>()
const terminalFit = ref<FitAddon>()
const command = ref<string>('')
const terminal_window = useTemplateRef<HTMLDivElement>('terminal_window')
const command_input = useTemplateRef<HTMLInputElement>('command_input')
const isLoading = ref<boolean>(false)
watch(() => isLoading.value, async value => {
if (value) {
@ -100,11 +97,11 @@ watch(() => config.app.console_enabled, async () => {
await navigateTo('/')
})
const reSizeTerminal = () => {
const handle_event = () => {
if (!terminal.value) {
return
}
terminalFit.value.fit()
terminalFit.value?.fit()
}
const runCommand = async () => {
@ -128,13 +125,14 @@ const runCommand = async () => {
cursorStyle: 'underline',
cols: 108,
rows: 10,
buffer: 1000,
disableStdin: true,
scrollback: 1000,
})
terminalFit.value = new FitAddon()
terminal.value.loadAddon(terminalFit.value)
terminal.value.open(terminal_window.value)
if (terminal_window.value) {
terminal.value.open(terminal_window.value)
}
terminalFit.value.fit();
}
@ -149,7 +147,7 @@ const runCommand = async () => {
terminal.value.writeln(`$ yt-dlp ${command.value}`)
}
const clearOutput = async (withCommand = false) => {
const clearOutput = async (withCommand: boolean = false) => {
if (terminal.value) {
terminal.value.clear()
}
@ -168,7 +166,7 @@ const focusInput = () => {
command_input.value.focus()
}
const writer = s => {
const writer = (s: string) => {
if (!terminal.value) {
return
}
@ -188,23 +186,23 @@ watch(() => config.app.basic_mode, async () => {
})
onMounted(async () => {
window.addEventListener('resize', reSizeTerminal);
document.addEventListener('resize', handle_event);
focusInput()
socket.off('cli_close', loader)
socket.off('cli_output', writer)
socket.on('cli_close', loader)
socket.on('cli_output', writer)
if (bg_enable.value) {
document.querySelector('body').setAttribute("style", `opacity: 1.0`)
document.querySelector('body')?.setAttribute("style", `opacity: 1.0`)
}
})
onUnmounted(() => {
onBeforeUnmount(() => {
socket.off('cli_close', loader)
socket.off('cli_output', writer)
window.removeEventListener('resize', reSizeTerminal)
document.removeEventListener('resize', handle_event)
if (bg_enable.value) {
document.querySelector('body').setAttribute("style", `opacity: ${bg_opacity.value}`)
document.querySelector('body')?.setAttribute("style", `opacity: ${bg_opacity.value}`)
}
});
</script>

View file

@ -89,19 +89,19 @@ code {
</span>
<span v-for="log in filteredItems" :key="log.id" class="is-block">
<template v-if="log?.datetime">[<span class="has-tooltip" :title="log.datetime">{{ moment(log.datetime).format('HH:mm:ss') }}</span>]</template>
{{ log.line }}
</span>
<span class="is-block" v-if="filteredItems.length < 1">
<span class="is-block m-0 notification is-warning is-dark has-text-centered" v-if="query">
<span class="notification-title is-danger">
<span class="icon"><i class="fas fa-filter"/></span>
No logs match this query: <u>{{ query }}</u>
</span>
</span>
<span v-else>
<span class="has-text-danger">No logs available</span></span>
</span>
</code>
{{ log.line }}
</span>
<span class="is-block" v-if="filteredItems.length < 1">
<span class="is-block m-0 notification is-warning is-dark has-text-centered" v-if="query">
<span class="notification-title is-danger">
<span class="icon"><i class="fas fa-filter" /></span>
No logs match this query: <u>{{ query }}</u>
</span>
</span>
<span v-else>
<span class="has-text-danger">No logs available</span></span>
</span>
</code>
<div ref="bottomMarker"></div>
</div>
</div>
@ -109,30 +109,45 @@ code {
</div>
</template>
<script setup>
<script setup lang="ts">
import moment from 'moment'
import { request } from '~/utils/index'
import { ref, onMounted, nextTick } from 'vue'
import { useStorage } from '@vueuse/core'
import type { log_line } from '~/@types/logs'
let scrollTimeout = null
let scrollTimeout: NodeJS.Timeout | null = null
const toast = useNotification()
const socket = useSocketStore()
const config = useConfigStore()
const route = useRoute()
const logs = ref([])
const offset = ref(0)
const loading = ref(false)
const logContainer = ref(null)
const bottomMarker = ref(null)
const autoScroll = ref(true)
const textWrap = ref(true)
const reachedEnd = ref(false)
const bg_enable = useStorage('random_bg', true)
const bg_opacity = useStorage('random_bg_opacity', 0.85)
const logs = ref<Array<log_line>>([])
const offset = ref<number>(0)
const loading = ref<boolean>(false)
const logContainer = useTemplateRef<HTMLDivElement>('logContainer')
const bottomMarker = useTemplateRef<HTMLDivElement>('bottomMarker')
const autoScroll = ref<boolean>(true)
const textWrap = ref<boolean>(true)
const reachedEnd = ref<boolean>(false)
const bg_enable = useStorage<boolean>('random_bg', true)
const bg_opacity = useStorage<number>('random_bg_opacity', 0.85)
const query = ref<string>((() => {
const filter = route.query.filter ?? ''
if (!filter) {
return ''
}
if (typeof filter === 'string') {
return filter.trim()
}
if (Array.isArray(filter) && filter.length > 0) {
return filter[0]?.trim() ?? ''
}
return ''
})())
const query = ref(useRoute().query.filter ?? '')
const toggleFilter = ref(false)
watch(toggleFilter, () => {
if (!toggleFilter.value) {
@ -161,9 +176,11 @@ const filteredItems = computed(() => {
const context = contextMatch ? parseInt(contextMatch[1], 10) : 0
const searchTerm = raw.replace(/context:\d+/, '').trim()
if (!searchTerm) return logs.value
if (!searchTerm) {
return logs.value
}
const result = []
const result: Array<log_line> = []
const matchedIndexes = new Set()
logs.value.forEach((log, i) => {
@ -174,7 +191,7 @@ const filteredItems = computed(() => {
}
})
Array.from(matchedIndexes).sort((a, b) => a - b).forEach(index => {
Array.from(matchedIndexes).sort((a: any, b: any) => a - b).forEach((index: any) => {
result.push(logs.value[index])
})
@ -251,11 +268,11 @@ const handleScroll = () => {
}
}
const scrollToBottom = (fast=false) => {
const scrollToBottom = (fast = false) => {
autoScroll.value = true
nextTick(() => {
if (bottomMarker.value) {
bottomMarker.value.scrollIntoView({ behavior: fast ? 'auto': 'smooth' })
bottomMarker.value.scrollIntoView({ behavior: fast ? 'auto' : 'smooth' })
}
})
}
@ -263,7 +280,8 @@ const scrollToBottom = (fast=false) => {
onMounted(async () => {
await fetchLogs()
socket.emit('subscribe', 'log_lines')
socket.on('log_lines', data => {
socket.on('log_lines', (data: any) => {
logs.value.push(data)
nextTick(() => {
@ -271,17 +289,19 @@ onMounted(async () => {
bottomMarker.value.scrollIntoView({ behavior: 'smooth' })
}
})
})
if (bg_enable.value) {
document.querySelector('body').setAttribute("style", `opacity: 1.0`)
document.querySelector('body')?.setAttribute("style", `opacity: 1.0`)
}
})
onUnmounted(() => {
onBeforeUnmount(() => {
socket.emit('unsubscribe', 'log_lines')
socket.off('log_lines')
if (bg_enable.value) {
document.querySelector('body').setAttribute("style", `opacity: ${bg_opacity.value}`)
document.querySelector('body')?.setAttribute("style", `opacity: ${bg_opacity.value}`)
}
})

View file

@ -36,7 +36,7 @@ div.is-centered {
</p>
<p class="control">
<button class="button is-info" @click="reloadContent" :class="{ 'is-loading': isLoading }"
<button class="button is-info" @click="reloadContent()" :class="{ 'is-loading': isLoading }"
:disabled="!socket.isConnected || isLoading" v-if="tasks && tasks.length > 0">
<span class="icon"><i class="fas fa-refresh" /></span>
</button>
@ -97,9 +97,8 @@ div.is-centered {
<span v-if="item.timer" class="has-tooltip" v-tooltip="item.timer">
{{ tryParse(item.timer) }}
</span>
<span v-else class="has-text-danger">
<span class="icon"><i class="fa-solid fa-exclamation-triangle" /></span>
<span>No timer is set</span>
<span v-else class="has-text-warning">
No timer is set
</span>
</td>
<td class="is-vcentered is-items-center">
@ -155,9 +154,16 @@ div.is-centered {
<div class="card-content is-flex-grow-1">
<div class="content">
<p class="is-text-overflow">
<span class="icon"><i class="fa-solid fa-clock" /></span>
<span v-if="item.timer">{{ tryParse(item.timer) }} - {{ item.timer }}</span>
<span v-else>No timer is set</span>
<span class="icon">
<i class="fa-solid"
:class="{ 'fa-clock': item.timer, 'has-text-danger fa-exclamation-triangle': !item.timer }" />
</span>
<span v-if="item.timer">
<NuxtLink target="_blank" :to="`https://crontab.guru/#${item.timer.replace(/ /g, '_')}`">
{{ item.timer }} - {{ tryParse(item.timer) }}
</NuxtLink>
</span>
<span class="has-text-warning" v-else>No timer is set</span>
</p>
<p class="is-text-overflow" v-if="item.folder">
<span class="icon"><i class="fa-solid fa-folder" /></span>
@ -214,25 +220,26 @@ div.is-centered {
</main>
</template>
<script setup>
<script setup lang="ts">
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import { CronExpressionParser } from 'cron-parser'
import { request } from '~/utils/index'
import type { task_item, exported_task, error_response } from '~/@types/tasks'
const box = useConfirm()
const toast = useNotification()
const config = useConfigStore()
const socket = useSocketStore()
const box = useConfirm()
const tasks = ref([])
const task = ref({})
const taskRef = ref('')
const toggleForm = ref(false)
const isLoading = ref(true)
const initialLoad = ref(true)
const addInProgress = ref(false)
const display_style = useStorage("tasks_display_style", "cards")
const tasks = ref<Array<task_item>>([])
const task = ref<task_item | Object>({})
const taskRef = ref<string>('')
const toggleForm = ref<boolean>(false)
const isLoading = ref<boolean>(true)
const initialLoad = ref<boolean>(true)
const addInProgress = ref<boolean>(false)
const display_style = useStorage<string>("tasks_display_style", "cards")
const remove_keys = ['in_progress']
watch(() => config.app.basic_mode, async () => {
@ -250,7 +257,7 @@ watch(() => socket.isConnected, async () => {
}
})
const reloadContent = async (fromMounted = false) => {
const reloadContent = async (fromMounted: boolean = false) => {
try {
isLoading.value = true
const response = await request('/api/tasks')
@ -276,18 +283,16 @@ const reloadContent = async (fromMounted = false) => {
}
}
const resetForm = (closeForm = false) => {
const resetForm = (closeForm: boolean = false) => {
task.value = {}
taskRef.value = null
taskRef.value = ''
addInProgress.value = false
if (closeForm) {
toggleForm.value = false
}
}
const updateTasks = async items => {
let data = {}
const updateTasks = async (items: Array<task_item>) => {
try {
addInProgress.value = true
@ -299,24 +304,24 @@ const updateTasks = async items => {
body: JSON.stringify(items.map(item => cleanObject(toRaw(item), remove_keys))),
})
data = await response.json()
const data: Array<task_item> | error_response = await response.json()
if (200 !== response.status) {
toast.error(`Failed to update task. ${data.error}`);
if ("error" in data) {
toast.error(`Failed to update tasks. ${data.error}`);
return false
}
tasks.value = data
resetForm(true)
return true
} catch (e) {
toast.error(`Failed to update task. ${data?.error ?? e.message}`);
} catch (e: any) {
toast.error(`Failed to update tasks. ${e.message}`);
} finally {
addInProgress.value = false
}
}
const deleteItem = async item => {
const deleteItem = async (item: task_item) => {
if (true !== box.confirm(`Delete '${item.name}' task?`, true)) {
return
}
@ -338,7 +343,7 @@ const deleteItem = async item => {
toast.success('Task deleted.')
}
const updateItem = async ({ reference, task }) => {
const updateItem = async ({ reference, task }: { reference?: string, task: task_item }) => {
if (reference) {
// -- find the task index.
const index = tasks.value.findIndex((t) => t?.id === reference)
@ -354,17 +359,17 @@ const updateItem = async ({ reference, task }) => {
return
}
toast.success('Task updated')
toast.success('Task updated.')
resetForm(true)
}
const editItem = item => {
const editItem = (item: task_item) => {
task.value = item
taskRef.value = item.id
toggleForm.value = true
}
const calcPath = path => {
const calcPath = (path: string) => {
const loc = config.app.download_path || '/downloads'
if (path) {
@ -382,7 +387,7 @@ onMounted(async () => {
await reloadContent(true)
});
const tryParse = expression => {
const tryParse = (expression: string) => {
try {
return moment(CronExpressionParser.parse(expression).next().toISOString()).fromNow()
} catch (e) {
@ -390,7 +395,7 @@ const tryParse = expression => {
}
}
const runNow = async item => {
const runNow = async (item: task_item) => {
if (true !== box.confirm(`Run '${item.name}' now? it will also run at the scheduled time.`)) {
return
}
@ -400,7 +405,7 @@ const runNow = async item => {
let data = {
url: item.url,
preset: item.preset,
}
} as task_item
if (item.folder) {
data.folder = item.folder
@ -422,9 +427,9 @@ const runNow = async item => {
}, 500)
}
onUnmounted(() => socket.off('status', statusHandler))
onBeforeUnmount(() => socket.off('status', statusHandler))
const statusHandler = async stream => {
const statusHandler = async (stream: string) => {
const { status, msg } = JSON.parse(stream)
if ('error' === status) {
@ -433,7 +438,7 @@ const statusHandler = async stream => {
}
}
const exportItem = async item => {
const exportItem = async (item: task_item) => {
const info = JSON.parse(JSON.stringify(item))
let data = {
@ -442,7 +447,7 @@ const exportItem = async item => {
preset: info.preset,
timer: info.timer,
folder: info.folder,
}
} as exported_task
if (info.template) {
data.template = info.template
@ -452,8 +457,8 @@ const exportItem = async item => {
data.cli = info.cli
}
data['_type'] = 'task'
data['_version'] = '2.0'
data._type = 'task'
data._version = '2.0'
return copyText(base64UrlEncode(JSON.stringify(data)));
}