ytptube/ui/app/pages/changelog.vue
2026-05-17 20:21:58 +03:00

299 lines
9.9 KiB
Vue

<template>
<main class="w-full min-w-0 max-w-full space-y-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div class="flex min-w-0 items-center gap-3">
<span
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<UIcon :name="pageShell.icon" class="size-5" />
</span>
<div class="min-w-0 space-y-2">
<div
class="flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
</div>
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<UButton
v-if="logs.length > 0"
color="neutral"
:variant="toggleFilter ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-filter"
@click="toggleFilter = !toggleFilter"
>
<span>Filter</span>
</UButton>
<USwitch
v-model="latestOnly"
color="primary"
size="sm"
:label="latestOnly ? 'Latest Only' : 'All Loaded'"
:ui="{ root: 'items-center gap-2', wrapper: 'ms-0 text-xs text-toned' }"
/>
<UInput
v-if="toggleFilter && logs.length > 0"
id="filter"
v-model.lazy="query"
type="search"
placeholder="Filter changelog entries"
icon="i-lucide-filter"
size="sm"
class="order-last w-full sm:order-first sm:w-80"
/>
</div>
</div>
<UAlert
v-if="isLoading"
color="info"
variant="soft"
icon="i-lucide-loader-circle"
title="Loading"
description="Loading data. Please wait..."
/>
<template v-else>
<div v-if="filteredLogs.length > 0" class="space-y-4">
<UPageCard v-for="log in filteredLogs" :key="log.tag" variant="outline" :ui="pageCardUi">
<template #body>
<div class="space-y-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0 space-y-2">
<div class="flex flex-wrap items-center gap-2">
<div
class="inline-flex items-center gap-2 text-base font-semibold text-highlighted"
>
<UIcon name="i-lucide-git-branch" class="size-4 text-toned" />
<span>{{ log.tag }}</span>
</div>
<UBadge v-if="isInstalled(log)" color="success" variant="soft" size="sm">
Installed
</UBadge>
</div>
</div>
<div class="flex flex-wrap items-center justify-end gap-2 text-xs text-toned">
<span
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
>
<UIcon name="i-lucide-list" class="size-3.5" />
<span>{{ log.commits?.length || 0 }} commits</span>
</span>
<UTooltip v-if="log.date" :text="`Release Date: ${log.date}`">
<span
class="inline-flex cursor-help items-center gap-1 rounded-md border border-default px-2 py-1"
>
<UIcon name="i-lucide-calendar-days" class="size-3.5" />
<span>{{ moment(log.date).fromNow() }}</span>
</span>
</UTooltip>
</div>
</div>
<div class="space-y-3 border-t border-default pt-4">
<article
v-for="commit in log.commits"
:key="commit.sha"
class="flex flex-col gap-2 rounded-md border border-default bg-muted/20 px-3 py-3"
>
<div class="min-w-0">
<NuxtLink
:to="`${REPO}/commit/${commit.full_sha}`"
target="_blank"
class="block min-w-0 text-sm text-default hover:underline"
>
<span class="font-semibold text-highlighted">
{{ ucFirst(commit.message).replace(/\.$/, '') }}.
</span>
</NuxtLink>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-toned">
<span
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
>
<UIcon name="i-lucide-user" class="size-3.5" />
{{ commit.author }}
</span>
<UTooltip :text="`Date: ${commit.date}`">
<span
class="inline-flex cursor-help items-center gap-1 rounded-md border border-default px-2 py-1"
>
<UIcon name="i-lucide-clock-3" class="size-3.5" />
{{ moment(commit.date).fromNow() }}
</span>
</UTooltip>
<UTooltip :text="`SHA: ${commit.full_sha}`">
<span
class="inline-flex cursor-help items-center gap-1 rounded-md border border-default px-2 py-1 font-medium"
>
<UIcon name="i-lucide-git-commit-horizontal" class="size-3.5" />
{{ commit.sha }}
</span>
</UTooltip>
<span
v-if="commit.full_sha === app_sha"
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 font-medium"
>
<UIcon name="i-lucide-check" class="size-3.5 text-success" />
<span>Installed</span>
</span>
</div>
</article>
</div>
</div>
</template>
</UPageCard>
</div>
<div v-else class="space-y-3">
<UAlert
v-if="query"
color="warning"
variant="soft"
icon="i-lucide-search"
title="No Results"
:description="`No changelog entries found for the query: ${query}.`"
/>
<UAlert
v-else
color="warning"
variant="soft"
icon="i-lucide-circle-alert"
title="No changelog entries"
description="No changelog data is available right now."
/>
</div>
</template>
</main>
</template>
<script setup lang="ts">
import moment from 'moment';
import { useStorage } from '@vueuse/core';
import type { changelogs, changeset } from '~/types/changelogs';
import { request, ucFirst, uri } from '~/utils';
import { requirePageShell } from '~/utils/topLevelNavigation';
const toast = useNotification();
const config = useYtpConfig();
const pageShell = requirePageShell('changelog');
const PROJECT = 'ytptube';
const REPO = `https://github.com/arabcoders/${PROJECT}`;
const REPO_URL = `https://arabcoders.github.io/${PROJECT}/CHANGELOG.json?version={version}`;
const DEFAULT_LIMIT = 10;
const logs = ref<changelogs>([]);
const app_version = ref('');
const app_branch = ref('');
const app_sha = ref('');
const isLoading = ref(true);
const query = ref('');
const toggleFilter = ref(false);
const latestOnly = useStorage<boolean>('changelog_latest_only', true);
const pageCardUi = {
root: 'w-full bg-default',
container: 'w-full p-4 sm:p-5',
wrapper: 'w-full items-stretch',
body: 'w-full',
};
watch(toggleFilter, () => {
if (!toggleFilter.value) {
query.value = '';
}
});
const visibleLogs = computed<changelogs>(() =>
latestOnly.value ? logs.value.slice(0, DEFAULT_LIMIT) : logs.value,
);
const filteredLogs = computed<changelogs>(() => {
const q = query.value?.toLowerCase();
if (!q) {
return visibleLogs.value;
}
return visibleLogs.value
.map((log) => {
const tagMatches = log.tag.toLowerCase().includes(q);
const filteredCommits =
log.commits?.filter(
(commit) =>
commit.message.toLowerCase().includes(q) ||
commit.author.toLowerCase().includes(q) ||
commit.full_sha.toLowerCase().includes(q),
) ?? [];
if (tagMatches || filteredCommits.length > 0) {
return { ...log, commits: tagMatches ? log.commits : filteredCommits };
}
return null;
})
.filter((log): log is changeset => log !== null);
});
const loadContent = async (): Promise<void> => {
if (app_version.value === '' || logs.value.length > 0) {
return;
}
try {
try {
const changes = await fetch(
REPO_URL.replace('{branch}', app_branch.value).replace('{version}', app_version.value),
);
logs.value = await changes.json();
} catch (error) {
console.error(error);
logs.value = await (await request(uri('/CHANGELOG.json'), { method: 'GET' })).json();
}
} catch (error: any) {
console.error(error);
toast.error(`Failed to fetch changelog. ${error.message}`);
} finally {
isLoading.value = false;
}
};
const isInstalled = (log: changeset): boolean => {
const installed = String(app_sha.value);
if (log.full_sha.startsWith(installed)) {
return true;
}
for (const commit of log?.commits ?? []) {
if (commit.full_sha.startsWith(installed)) {
return true;
}
}
return false;
};
onMounted(async () => {
await awaiter(config.isLoaded);
app_branch.value = config.app.app_branch;
app_version.value = config.app.app_version;
app_sha.value = config.app.app_commit_sha;
await loadContent();
});
</script>