Migrate configStore to ts

This commit is contained in:
arabcoders 2025-06-22 21:49:07 +03:00
parent bd695e7067
commit b37d614e27
9 changed files with 247 additions and 150 deletions

View file

@ -131,17 +131,20 @@ jobs:
VERSION="${GITHUB_REF##*/}"
SHA=$(git rev-parse HEAD)
DATE=$(date -u +"%Y%m%d")
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | sed 's/\//-/g')
echo "Current version: ${VERSION}, SHA: ${SHA}, Date: ${DATE}"
echo "Current version: ${VERSION}, Branch: ${BRANCH}, SHA: ${SHA}, Date: ${DATE}"
echo "APP_VERSION=${VERSION}" >> $"$GITHUB_ENV"
echo "APP_SHA=${SHA}" >> "$GITHUB_ENV"
echo "APP_DATE=${DATE}" >> "$GITHUB_ENV"
echo "APP_BRANCH=${BRANCH}" >> "$GITHUB_ENV"
sed -i \
-e "s/^APP_VERSION = \".*\"/APP_VERSION = \"${VERSION}\"/" \
-e "s/^APP_COMMIT_SHA = \".*\"/APP_COMMIT_SHA = \"${SHA}\"/" \
-e "s/^APP_BUILD_DATE = \".*\"/APP_BUILD_DATE = \"${DATE}\"/" \
-e "s/^APP_BRANCH = \".*\"/APP_BRANCH = \"${BRANCH}\"/" \
app/library/version.py
- name: Set up QEMU

View file

@ -12,7 +12,7 @@ import coloredlogs
from dotenv import load_dotenv
from .Utils import FileLogFormatter, arg_converter
from .version import APP_BUILD_DATE, APP_COMMIT_SHA, APP_VERSION
from .version import APP_BRANCH, APP_BUILD_DATE, APP_COMMIT_SHA, APP_VERSION
class Config:
@ -122,6 +122,9 @@ class Config:
app_build_date: str = APP_BUILD_DATE
"The build date of the application."
app_branch: str = APP_BRANCH
"The branch of the application."
__instance = None
"The instance of the class."
@ -201,6 +204,7 @@ class Config:
"app_version",
"app_commit_sha",
"app_build_date",
"app_branch",
)
"The variables that are immutable."
@ -257,6 +261,7 @@ class Config:
"app_version",
"app_commit_sha",
"app_build_date",
"app_branch",
)
"The variables that are relevant to the frontend."

View file

@ -11,3 +11,6 @@ APP_COMMIT_SHA = "unknown"
APP_BUILD_DATE = "unknown"
"Build date of the application"
APP_BRANCH = "unknown"
"Branch of the application"

18
ui/@types/changelogs.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
type changelogs = changeset[]
type changeset = {
tag: string
full_sha: string
date: string
commits: Commit[]
}
type Commit = {
sha: string
full_sha: string
message: string
author: string
date: string
}
export type {changelogs, changeset, Commit}

79
ui/@types/config.d.ts vendored Normal file
View file

@ -0,0 +1,79 @@
type AppConfig = {
/** Path where downloaded files will be saved */
download_path: string
/** Indicates if the app should keep an archive of downloaded files */
keep_archive: boolean
/** Indicates if files should be removed after download */
remove_files: boolean
/** Indicates if the UI should update the title with the current download status */
ui_update_title: boolean
/** Output template for yt-dlp, e.g. "%(title)s.%(ext)s" */
output_template: string
/** yt-dlp version, e.g. "2023.10.01" */
ytdlp_version: string
/** Maximum number of concurrent download workers */
max_workers: number
/** Indicates if the app is in basic mode, which may limit some features */
basic_mode: boolean
/** Default preset name, e.g. "default" */
default_preset: string
/** Instance title for the app, null if not set */
instance_title: string | null
/** Sentry DSN for error tracking, null if not configured */
sentry_dsn: string | null
/** Indicates if the console is enabled */
console_enabled: boolean
/** Indicates if the file browser is enabled */
browser_enabled: boolean
/** Command options for yt-dlp */
ytdlp_cli: string
/** Indicates if file logging is enabled */
file_logging: boolean
/** Indicates if the app is running in a native environment (e.g., Electron) */
is_native: boolean
/** App version in format "1.0.0" */
app_version: string
/** App version in semantic versioning format, e.g. "1.0.0" */
app_commit_sha: string
/** App build date in ISO format, e.g. "2023-10-01T12:00:00Z" */
app_build_date: string
/** App branch name, e.g. "main" or "develop" */
app_branch: string
}
type Preset = {
/** Unique identifier for the preset */
id?: string
/** Preset name, e.g. "default" */
name: string
/** Optional description for the preset */
description: string
/** Folder where files will be saved, e.g. "/downloads" */
folder: string
/** Output template for the preset, e.g. "%(title)s.%(ext)s" */
template: string
/** Cookies for the preset, e.g. "cookies.txt" */
cookies: string
/** Additional command line options for yt-dlp */
cli: string
/** Indicates if this is the default preset */
default: boolean
}
type ConfigState = {
/** Show or hide the download form */
showForm: RemovableRef<boolean>
/** Application configuration */
app: AppConfig
/** List of presets */
presets: Preset[]
/** List of folders where files can be saved */
folders: string[]
/** Indicates if downloads are currently paused */
paused: boolean
/** Indicates if the configuration has been loaded */
is_loaded: boolean
}
export type { AppConfig, Preset, ConfigState }

View file

@ -113,14 +113,14 @@
<div class="columns mt-3 is-mobile">
<div class="column is-8-mobile">
<div class="has-text-left" v-if="config.app?.version">
<div class="has-text-left" v-if="config.app?.app_version">
© {{ Year }} - <NuxtLink href="https://github.com/ArabCoders/ytptube" target="_blank">YTPTube</NuxtLink>
<span class="is-hidden-mobile"
v-tooltip="`Build Date: ${config.app?.app_build_date}, commit: ${config.app?.app_commit_sha}`">
&nbsp;({{ config?.app?.version || 'unknown' }})</span>
v-tooltip="`Build Date: ${config.app?.app_build_date}, Branch: ${config.app?.app_branch}, commit: ${config.app?.app_commit_sha}`">
&nbsp;({{ config?.app?.app_version || 'unknown' }})</span>
- <NuxtLink target="_blank" href="https://github.com/yt-dlp/yt-dlp">yt-dlp</NuxtLink>
<span class="is-hidden-mobile">&nbsp;({{ config?.app?.ytdlp_version || 'unknown' }})</span>
- <NuxtLink :to="`/changelog?version=${config?.app?.version || 'unknown'}`">CHANGELOG</NuxtLink>
- <NuxtLink to="/changelog">CHANGELOG</NuxtLink>
</div>
</div>
<div class="column is-4-mobile" v-if="config.app?.started">

View file

@ -1,3 +1,18 @@
<style scoped>
.logs-container {
padding: 1rem;
min-width: 100%;
max-height: 73vh;
overflow-y: auto;
overflow-x: auto;
}
hr {
background-color: unset;
border-bottom: 1px solid var(--bulma-grey-light) !important
}
</style>
<template>
<main>
<div class="mt-1 columns is-multiline">
@ -51,8 +66,9 @@
</main>
</template>
<script setup>
<script setup lang="ts">
import moment from 'moment'
import type { changelogs, changeset } from '~/@types/changelogs'
const toast = useNotification()
const config = useConfigStore()
@ -63,63 +79,38 @@ const PROJECT = 'ytptube'
const REPO = `https://github.com/arabcoders/${PROJECT}`
const REPO_URL = `https://arabcoders.github.io/${PROJECT}/CHANGELOG.json?version={version}`
const logs = ref([])
const logs = ref<changelogs>([])
const api_version = ref('')
const isLoading = ref(false)
const hashLength = ref(7)
const branch = computed(() => {
const branch = String(api_version.value).split('-')[0] ?? 'master'
return ['master', 'dev'].includes(branch) ? branch : 'master'
})
watch(() => config.app.version, async value => {
if (!value) {
return
}
api_version.value = value
hashLength.value = value.split('-').pop().length
await nextTick()
await loadContent()
}, { immediate: true })
const api_branch = ref('')
const api_sha = ref('')
const isLoading = ref(true)
const loadContent = async () => {
if ('' === api_version.value) {
if ('' === api_version.value || logs.value.length > 0) {
return
}
isLoading.value = true
try {
try{
logs.value = await (await request(uri('/CHANGELOG.json'), { method: 'GET' })).json()
}catch(e){
console.error(e)
const changes = await fetch(REPO_URL.replace('{branch}', branch.value).replace('{version}', api_version.value))
try {
const changes = await fetch(REPO_URL.replace('{branch}', api_branch.value).replace('{version}', api_version.value))
logs.value = await changes.json()
} catch (e) {
console.error(e)
logs.value = await (await request(uri('/CHANGELOG.json'), { method: 'GET' })).json()
}
await nextTick()
logs.value = logs.value.slice(0, 10).map(log => {
log.commits = log.commits.map(commit => {
commit.full_sha = commit.sha.padEnd(hashLength.value, '0')
return commit
})
log.full_sha = log.tag.padEnd(hashLength.value, '0')
return log
})
} catch (e) {
logs.value = logs.value.slice(0, 10)
} catch (e: any) {
console.error(e)
toast.error('error', 'Error', `Failed to fetch changelog. ${e.message}`)
toast.error(`Failed to fetch changelog. ${e.message}`)
} finally {
isLoading.value = false
}
}
const isInstalled = log => {
const installed = String(api_version.value).split('-').pop()
const isInstalled = (log: changeset) => {
const installed = String(api_sha.value)
if (log.full_sha.startsWith(installed)) {
return true
@ -134,20 +125,11 @@ const isInstalled = log => {
return false
}
onMounted(() => loadContent())
onMounted(async () => {
await awaiter(config.isLoaded)
api_branch.value = config.app.app_branch
api_version.value = config.app.app_version
api_sha.value = config.app.app_commit_sha
loadContent()
})
</script>
<style scoped>
.logs-container {
padding: 1rem;
min-width: 100%;
max-height: 73vh;
overflow-y: auto;
overflow-x: auto;
}
hr {
background-color: unset;
border-bottom: 1px solid var(--bulma-grey-light) !important
}
</style>

View file

@ -1,86 +0,0 @@
import { useStorage } from '@vueuse/core'
const CONFIG_KEYS = {
showForm: useStorage('showForm', false),
app: {
download_path: '/downloads',
keep_archive: false,
remove_files: false,
ui_update_title: true,
output_template: '',
ytdlp_version: '',
max_workers: 1,
version: '',
basic_mode: true,
default_preset: 'default',
instance_title: null,
sentry_dsn: null,
console_enabled: false,
browser_enabled: false,
ytdlp_cli: '',
file_logging: false,
is_native: false,
app_version: '',
app_commit_sha: '',
app_build_date: '',
},
presets: [
{
'name': 'default',
'format': 'default',
'folder': '',
'template': '',
'cookies': '',
'cli': '',
'default': true
}
],
folders: [],
tasks: [],
paused: false,
};
export const useConfigStore = defineStore('config', () => {
const state = reactive(CONFIG_KEYS);
const actions = {
add(key, value) {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.');
state[parentKey][subKey] = value;
return;
}
state[key] = value;
},
get(key, defaultValue = null) {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.');
return state[parentKey][subKey] || defaultValue;
}
return state[key] || defaultValue;
},
update(key, value) {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.');
state[parentKey][subKey] = value;
return;
}
state[key] = value;
},
getAll() {
return state;
},
setAll(data) {
Object.keys(data).forEach((key) => {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.');
state[parentKey][subKey] = data[key];
return;
}
state[key] = data[key];
});
},
}
return { ...toRefs(state), ...actions };
});

93
ui/stores/ConfigStore.ts Normal file
View file

@ -0,0 +1,93 @@
import { useStorage } from '@vueuse/core'
import type { ConfigState } from '~/@types/config';
export const useConfigStore = defineStore('config', () => {
const state = reactive<ConfigState>({
showForm: useStorage('showForm', false),
app: {
download_path: '/downloads',
keep_archive: false,
remove_files: false,
ui_update_title: true,
output_template: '',
ytdlp_version: '',
max_workers: 1,
basic_mode: true,
default_preset: 'default',
instance_title: null,
sentry_dsn: null,
console_enabled: false,
browser_enabled: false,
ytdlp_cli: '',
file_logging: false,
is_native: false,
app_version: '',
app_commit_sha: '',
app_build_date: '',
app_branch: '',
},
presets: [
{
'name': 'default',
'description': 'Default preset for downloads',
'folder': '',
'template': '',
'cookies': '',
'cli': '',
'default': true
}
],
folders: [],
paused: false,
is_loaded: false,
});
const add = (key: string, value: any) => {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string]
state[parentKey][subKey] = value;
return;
}
(state as any)[key] = value
}
const get = (key: string, defaultValue: any = null): any => {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string]
const parent = state[parentKey] as any
return parent?.[subKey] ?? defaultValue
}
return (state as any)[key] ?? defaultValue
}
const update = add
const getAll = (): ConfigState => state
const setAll = (data: Record<string, any>) => {
Object.keys(data).forEach((key) => {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.') as [keyof ConfigState, string]
const parent = state[parentKey] as any
parent[subKey] = data[key]
return
}
(state as any)[key] = data[key]
})
state.is_loaded = true
}
const isLoaded = () => state.is_loaded
return {
...toRefs(state), add, get, update, getAll, setAll, isLoaded,
} as { [K in keyof ConfigState]: Ref<ConfigState[K]> } & {
add: typeof add
get: typeof get
update: typeof update
getAll: typeof getAll
setAll: typeof setAll
isLoaded: typeof isLoaded
}
});