Compare commits

...

3 commits
master ... dev

Author SHA1 Message Date
arabcoders
60b8e7c4a6 fix: video player did not support multi subs
Some checks failed
build-docker-containers / Validate Build Configuration (push) Has been cancelled
build-docker-containers / DockerHub README sync (push) Has been cancelled
build-docker-containers / Run Tests & Prepare Frontend (push) Has been cancelled
build-docker-containers / Build Container (amd64) (push) Has been cancelled
build-docker-containers / Build Container (arm64) (push) Has been cancelled
build-docker-containers / Publish multi-arch manifest (push) Has been cancelled
2026-06-23 10:42:01 +03:00
arabcoders
e14387e399 refactor: fix card width in mobile 2026-06-22 03:49:50 +03:00
arabcoders
52d6672fb3 refactor: update design layout 2026-06-21 21:25:47 +03:00
43 changed files with 3168 additions and 3015 deletions

View file

@ -30,5 +30,8 @@
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
"files.associations": {
"*.css": "tailwindcss"
}
}

5
FAQ.md
View file

@ -3,8 +3,9 @@
Certain configuration values can be set via environment variables, using the `-e` parameter on the docker command line,
or the `environment:` section in `compose.yaml` file.
<details>
<summary>Click to expand</summary>
<summary>Click to expand all environment Variables</summary>
| Environment Variable | Description | Default |
| ------------------------------- | ------------------------------------------------------------------- | --------------------- |
@ -64,7 +65,6 @@ or the `environment:` section in `compose.yaml` file.
| YTP_THUMB_GENERATE | Enable ffmpeg thumbnail generation when no local thumbnail exists. | `true` |
| YTP_THUMB_SIDECAR | Save generated thumbnails next to media instead of temp cache. | `false` |
| YTP_DISABLE_EXEC | Strip some dangerous yt-dlp options. | `false` |
</details>
> [!NOTE]
> To raise the worker limit for a specific extractor, set an env variable using this format: `YTP_MAX_WORKERS_FOR_<EXTRACTOR_NAME>`
@ -77,6 +77,7 @@ or the `environment:` section in `compose.yaml` file.
> `YTP_AUTO_CLEAR_HISTORY_DAYS` `0` days means no automatic clearing of the download history. lowest value that will
> trigger the clearing is `1` day. This setting will **NOT** delete the downloaded files, it will only clear the
> history from the database.
</details>
# Browser extensions & bookmarklets

View file

@ -7,5 +7,69 @@ export default defineAppConfig({
success: 'emerald',
neutral: 'stone',
},
formField: {
slots: {
label: 'block font-bold text-default',
},
},
tooltip: {
slots: {
content: 'pointer-events-auto',
},
},
dropdownMenu: {
slots: {
content: 'ytp-floating-surface',
},
},
popover: {
slots: {
content: 'ytp-floating-surface',
},
},
select: {
slots: {
content: 'ytp-floating-surface',
},
variants: {
variant: {
outline: 'bg-elevated/40 hover:bg-elevated/55 disabled:bg-elevated/20',
},
},
},
selectMenu: {
slots: {
content: 'ytp-floating-surface',
},
variants: {
variant: {
outline: 'bg-elevated/40 hover:bg-elevated/55 disabled:bg-elevated/20',
},
},
},
button: {
compoundVariants: [
{
color: 'neutral',
variant: 'outline',
class:
'bg-transparent hover:bg-elevated/30 active:bg-elevated/40 disabled:bg-transparent aria-disabled:bg-transparent',
},
{
color: 'neutral',
variant: 'soft',
class:
'bg-elevated/30 hover:bg-elevated/45 active:bg-elevated/55 disabled:bg-elevated/20 aria-disabled:bg-elevated/20',
},
{
color: 'neutral',
variant: 'ghost',
class: 'hover:bg-elevated/30 active:bg-elevated/40 focus-visible:bg-elevated/30',
},
],
defaultVariants: {
size: 'sm',
},
},
},
});

View file

@ -1,3 +1,5 @@
@layer theme, base, legacy, components, utilities;
@import 'tailwindcss';
@import '@nuxt/ui';
@ -30,6 +32,11 @@
html.simple-mode.bg-fanart #__nuxt {
background-image: none !important;
}
html.dark.simple-mode.bg-fanart body,
html.dark.simple-mode.bg-fanart #__nuxt {
background-color: var(--ui-bg) !important;
}
}
@layer components {
@ -51,9 +58,9 @@
.shell-surface {
background:
radial-gradient(circle at top left, rgb(99 102 241 / 0.1), transparent 28%),
radial-gradient(circle at top right, rgb(245 158 11 / 0.08), transparent 22%),
linear-gradient(180deg, rgb(255 255 255 / 0.84), rgb(248 250 252 / 0.9));
radial-gradient(circle at top left, rgb(99 102 241 / 0.14), transparent 28%),
radial-gradient(circle at top right, rgb(245 158 11 / 0.12), transparent 22%),
linear-gradient(180deg, rgb(255 255 255 / 0.6), rgb(248 250 252 / 0.72));
backdrop-filter: blur(16px);
}
@ -65,6 +72,115 @@
backdrop-filter: blur(16px);
}
.ytp-page-header {
@apply flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between;
}
.ytp-page-heading {
@apply flex min-w-0 items-center gap-3;
}
.ytp-page-kicker {
@apply flex flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned;
}
.ytp-page-icon {
@apply inline-flex size-11 shrink-0 items-center justify-center rounded-md border bg-elevated/70 text-primary;
border-color: var(--ytp-surface-border-solid);
}
.ytp-section-icon {
@apply inline-flex size-10 shrink-0 items-center justify-center rounded-md border bg-elevated/70 text-primary;
border-color: var(--ytp-surface-border-solid);
}
.ytp-detail-icon {
@apply inline-flex size-9 shrink-0 items-center justify-center rounded-md border bg-elevated/70 text-primary;
border-color: var(--ytp-surface-border-solid);
}
.ytp-card {
@apply min-w-0 max-w-full rounded-lg border bg-elevated/20;
border-color: var(--ytp-surface-border-soft);
}
.ytp-card-padded {
@apply min-w-0 max-w-full rounded-lg border bg-elevated/20 p-4;
border-color: var(--ytp-surface-border-soft);
}
.ytp-card-padded-lg {
@apply min-w-0 max-w-full rounded-lg border bg-elevated/20 p-4 sm:p-5;
border-color: var(--ytp-surface-border-soft);
}
.ytp-panel {
@apply min-w-0 max-w-full rounded-lg border;
border-color: var(--ytp-surface-border-soft);
}
.ytp-panel-padded {
@apply min-w-0 max-w-full rounded-lg border p-4;
border-color: var(--ytp-surface-border-soft);
}
.ytp-panel-padded-lg {
@apply min-w-0 max-w-full rounded-lg border p-4 sm:p-5;
border-color: var(--ytp-surface-border-soft);
}
.ytp-frame {
@apply min-w-0 max-w-full rounded-lg border;
border-color: var(--ytp-surface-border-soft);
}
.ytp-table-surface {
@apply min-w-0 max-w-full rounded-sm border bg-elevated/20;
border-color: var(--ytp-surface-border-solid);
}
.ytp-floating-surface {
@apply border bg-default/95 shadow-xl backdrop-blur-md;
border-color: var(--ytp-surface-border-solid);
box-shadow: 0 1rem 2rem rgb(15 23 42 / 0.1);
}
.ytp-floating-surface [data-slot='item'][data-highlighted]::before,
.ytp-floating-surface [data-slot='item'][data-state='open']::before,
.ytp-floating-surface [data-slot='item']:hover::before {
background-color: color-mix(in oklab, var(--ui-primary) 11%, transparent) !important;
}
.ytp-floating-surface [data-slot='item'][data-disabled]::before {
background-color: transparent !important;
}
.dark .ytp-floating-surface {
background: color-mix(in oklab, var(--ui-bg) 92%, var(--ui-bg-elevated) 8%);
box-shadow: 0 1rem 2rem rgb(0 0 0 / 0.3);
}
.dark .ytp-floating-surface [data-slot='item'][data-highlighted]::before,
.dark .ytp-floating-surface [data-slot='item'][data-state='open']::before,
.dark .ytp-floating-surface [data-slot='item']:hover::before {
background-color: color-mix(in oklab, var(--ui-primary) 16%, transparent) !important;
}
.ytp-border-top-soft {
@apply border-t;
border-color: var(--ytp-surface-border-soft);
}
.ytp-border-bottom-soft {
@apply border-b;
border-color: var(--ytp-surface-border-soft);
}
.ytp-border-left-soft {
@apply border-l;
border-color: var(--ytp-surface-border-soft);
}
.ytp-terminal {
display: block;
width: 100%;
@ -167,6 +283,15 @@
--ui-radius: 0.5rem;
--ui-container: 96rem;
--ui-header-height: 4.25rem;
--ytp-border-strong: var(--ui-border-accented);
--ytp-surface-border-soft: var(--ui-border-accented);
--ytp-surface-border-solid: var(--ui-border-accented);
}
.dark {
--ytp-border-strong: var(--ui-border);
--ytp-surface-border-soft: color-mix(in oklab, var(--ui-border) 70%, transparent);
--ytp-surface-border-solid: var(--ui-border);
}
html:not(.no-page-anim) .page-enter-active {

View file

@ -261,9 +261,7 @@
</div>
</div>
<div
class="grid gap-3 rounded-lg border border-default bg-default p-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]"
>
<div class="grid gap-3 ytp-card-padded md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
<UFormField :ui="fieldUi">
<template #label>
<div class="flex flex-wrap items-center gap-2">

View file

@ -1,7 +1,7 @@
<template>
<div v-if="'bool' === type && compact" class="w-full space-y-1.5">
<div
class="flex min-h-11 items-start justify-between gap-3 rounded-md border border-default bg-default/80 px-3 py-2"
class="flex min-h-11 items-start justify-between gap-3 rounded-md border border-default bg-elevated/20 px-3 py-2"
>
<div class="min-w-0 flex-1">
<div class="inline-flex min-w-0 items-center gap-2 text-sm font-semibold text-default">
@ -27,7 +27,7 @@
v-model="boolModel"
:disabled="disabled"
color="success"
:label="boolModel ? 'Yes' : 'No'"
label=""
size="lg"
class="shrink-0"
:ui="{ root: 'items-center gap-2', wrapper: 'ms-0 text-sm' }"
@ -70,7 +70,7 @@
:disabled="disabled"
size="lg"
class="w-full"
:ui="{ root: 'w-full', base: 'w-full bg-default/90' }"
:ui="{ root: 'w-full', base: 'w-full bg-elevated/60' }"
/>
<UTextarea

View file

@ -20,7 +20,7 @@
<div
v-if="open && suggestions.length"
ref="dropdownRef"
class="absolute inset-x-0 top-full z-20 mt-1 max-h-40 overflow-y-auto rounded-md border border-default bg-default shadow-lg"
class="absolute inset-x-0 top-full z-20 mt-1 max-h-40 overflow-y-auto rounded-md ytp-floating-surface"
role="listbox"
>
<button

View file

@ -12,7 +12,7 @@
variant="outline"
color="neutral"
class="w-full"
:ui="{ root: 'w-full', base: 'w-full bg-default/90', leadingIcon: iconClass }"
:ui="{ root: 'w-full', base: 'w-full bg-elevated/60', leadingIcon: iconClass }"
@focus="onFocus"
@blur="hideList"
@input="onInput"
@ -25,7 +25,7 @@
<div
v-if="showList && filteredOptions.length"
ref="dropdownRef"
class="absolute inset-x-0 top-full z-20 mt-1 max-h-40 overflow-y-auto rounded-md border border-default bg-default shadow-lg"
class="absolute inset-x-0 top-full z-20 mt-1 max-h-40 overflow-y-auto rounded-md ytp-floating-surface"
role="menu"
>
<button

View file

@ -25,10 +25,7 @@
</UButton>
</div>
<div
v-if="!limits && limitsLoading"
class="flex min-h-72 items-center justify-center rounded-md border border-default bg-default/90"
>
<div v-if="!limits && limitsLoading" class="ytp-card flex min-h-72 items-center justify-center">
<div class="flex flex-col items-center gap-3 text-center text-toned">
<UIcon name="i-lucide-loader-circle" class="size-10 animate-spin text-info" />
@ -66,7 +63,7 @@
title="Download queue is paused"
/>
<div class="rounded-md border border-default bg-default shadow-sm">
<div class="ytp-card shadow-sm">
<div class="grid gap-0 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]">
<section
class="space-y-4 border-b border-default px-4 py-4 sm:px-5 lg:border-r lg:border-b-0"
@ -197,13 +194,10 @@
description="Overrides and extractor usage appear here once activity is detected."
/>
<div
v-else
class="w-full min-w-0 max-w-full overflow-hidden rounded-md border border-default bg-default"
>
<div v-else class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface">
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-180 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>

View file

@ -119,9 +119,7 @@
@click="sourceOpen = !sourceOpen"
>
<div class="flex items-center gap-3">
<span
class="inline-flex size-9 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<span class="ytp-detail-icon">
<UIcon name="i-lucide-file-code" class="size-4" />
</span>
<p class="text-base font-semibold text-highlighted">Source</p>
@ -167,9 +165,7 @@
@click="fieldsOpen = !fieldsOpen"
>
<div class="flex items-center gap-3">
<span
class="inline-flex size-9 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<span class="ytp-detail-icon">
<UIcon name="i-lucide-tags" class="size-4" />
</span>
<p class="text-base font-semibold text-highlighted">Fields</p>
@ -225,14 +221,16 @@
>
<button
type="button"
class="flex w-full items-center justify-between gap-3 px-3 py-2 text-left"
class="grid w-full grid-cols-[minmax(7rem,0.45fr)_minmax(0,1fr)] items-center gap-3 px-3 py-2 text-left sm:grid-cols-[minmax(0,12rem)_minmax(0,1fr)]"
@click="toggleField(field.key)"
>
<span class="text-[11px] font-semibold uppercase tracking-wide text-toned">
<span
class="min-w-0 truncate text-[11px] font-semibold uppercase tracking-wide text-toned"
>
{{ field.label }}
</span>
<div class="flex items-center gap-2">
<span v-if="field.preview" class="max-w-md truncate text-xs text-toned">
<div class="flex min-w-0 items-center justify-end gap-2">
<span v-if="field.preview" class="min-w-0 truncate text-xs text-toned">
{{ field.preview }}
</span>
<UIcon
@ -306,9 +304,7 @@
@click="rawJsonOpen = !rawJsonOpen"
>
<div class="flex items-center gap-3">
<span
class="inline-flex size-9 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<span class="ytp-detail-icon">
<UIcon name="i-lucide-braces" class="size-4" />
</span>
<p class="text-base font-semibold text-highlighted">Raw Data</p>

View file

@ -1,8 +1,7 @@
<template>
<main class="space-y-4">
<form autocomplete="off" class="space-y-4" @submit.prevent="addDownload">
<UPageCard variant="outline" :ui="downloadCardUi">
<template #body>
<div class="ytp-card p-4 sm:p-6 space-y-4">
<div class="space-y-4">
<UFormField class="w-full" :ui="downloadFieldUi">
<template #label>
@ -26,7 +25,7 @@
v-model="form.url"
:disabled="addInProgress"
size="lg"
variant="outline"
:variant="show_description ? 'soft' : 'outline'"
color="neutral"
class="w-full"
:rows="3"
@ -47,7 +46,7 @@
placeholder="URLs to download"
:disabled="addInProgress"
size="lg"
variant="outline"
:variant="show_description ? 'soft' : 'outline'"
color="neutral"
class="w-full"
:ui="{
@ -97,7 +96,7 @@
<div class="flex w-full gap-2">
<UButton
color="neutral"
variant="outline"
:variant="show_description ? 'soft' : 'outline'"
icon="i-lucide-info"
square
class="shrink-0"
@ -158,7 +157,7 @@
<UButton
type="button"
color="neutral"
variant="outline"
:variant="showAdvanced ? 'soft' : 'outline'"
icon="i-lucide-settings-2"
size="lg"
class="w-full justify-center"
@ -185,11 +184,9 @@
</span>
</button>
</div>
</template>
</UPageCard>
</div>
<UPageCard v-if="showAdvanced" variant="outline" :ui="downloadCardUi">
<template #body>
<div v-if="showAdvanced" class="ytp-card p-4 sm:p-6 space-y-4">
<div class="space-y-4">
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-12">
<div class="xl:col-span-2">
@ -402,10 +399,7 @@
</UFormField>
</div>
<div
v-if="config.dl_fields.length > 0"
class="grid gap-4 md:grid-cols-2 xl:grid-cols-2"
>
<div v-if="config.dl_fields.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-2">
<DLInput
v-for="(fi, index) in sortedDLFields"
:id="fi?.id || `dlf-${index}`"
@ -421,9 +415,7 @@
</div>
</div>
<div
class="flex flex-wrap items-center justify-between gap-2 border-t border-default pt-4"
>
<div class="flex flex-wrap items-center justify-between gap-2 border-t border-default pt-4">
<UDropdownMenu class="sm:hidden" :items="mobileActionGroups" :modal="false">
<UButton
color="neutral"
@ -438,13 +430,11 @@
<div class="hidden flex-wrap items-center gap-2 sm:flex">
<UButton
type="button"
color="info"
color="neutral"
variant="outline"
icon="i-lucide-info"
:disabled="addInProgress || !hasValidUrl"
@click="
emitter('getInfo', splitUrls(form.url || '')[0] || '', form.preset, form.cli)
"
@click="emitter('getInfo', splitUrls(form.url || '')[0] || '', form.preset, form.cli)"
>
yt-dlp Information
</UButton>
@ -452,7 +442,7 @@
<UButton
v-if="config.app.console_enabled"
type="button"
color="warning"
color="neutral"
variant="outline"
icon="i-lucide-terminal"
:disabled="!hasValidUrl"
@ -464,7 +454,7 @@
<UButton
v-if="config.app.console_enabled"
type="button"
color="success"
color="neutral"
variant="outline"
icon="i-lucide-flask-conical"
:disabled="!hasValidUrl"
@ -476,7 +466,7 @@
<UButton
type="button"
color="error"
color="neutral"
variant="outline"
icon="i-lucide-rotate-ccw"
:disabled="!!form?.id"
@ -485,8 +475,7 @@
Reset local settings
</UButton>
</div>
</template>
</UPageCard>
</div>
</form>
<UModal
@ -707,13 +696,6 @@ const testResultsPreClasses = computed(() => [
testResultsClasses.value,
]);
const downloadCardUi = {
root: 'w-full',
container: 'w-full p-4 sm:p-6',
wrapper: 'w-full items-stretch',
body: 'w-full space-y-4',
};
const downloadFieldUi = {
label: 'font-semibold text-default',
container: 'space-y-2',
@ -793,7 +775,6 @@ const mobileActionGroups = computed(() => {
{
label: 'Reset local settings',
icon: 'i-lucide-rotate-ccw',
color: 'error',
disabled: Boolean(form.value?.id),
onSelect: () => void resetConfig(),
},

View file

@ -86,7 +86,7 @@
<UIcon name="i-lucide-check" class="size-3.5" />
</UButton>
<UButton
color="error"
color="neutral"
variant="ghost"
size="xs"
square
@ -121,7 +121,7 @@
</UButton>
<UButton
color="error"
color="neutral"
variant="ghost"
size="sm"
icon="i-lucide-trash"

View file

@ -0,0 +1,36 @@
<template>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<slot name="icon">
<UIcon :name="icon" class="size-5" />
</slot>
</span>
<div class="min-w-0" :class="contentClass">
<div class="ytp-page-kicker">
<slot name="kicker">
<span>{{ sectionLabel }}</span>
<span>/</span>
<span>{{ pageLabel }}</span>
</slot>
</div>
</div>
</div>
<div
v-if="$slots.actions"
class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end"
>
<slot name="actions" />
</div>
</div>
</template>
<script setup lang="ts">
import type { YtpPageHeaderProps } from '~/types';
withDefaults(defineProps<YtpPageHeaderProps>(), {
contentClass: 'space-y-1',
});
</script>

View file

@ -30,15 +30,15 @@
<template #body>
<div class="w-full space-y-6">
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi">
<template #header>
<div class="ytp-card w-full">
<div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2">
<UIcon name="i-lucide-layout-dashboard" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Page View</span>
</div>
</template>
</div>
<template #body>
<div class="p-4 sm:p-5 space-y-4">
<UFormField label="" class="w-full" :ui="settingsFieldUi">
<USelect
v-model="draftMode"
@ -73,18 +73,18 @@
:label="page_anims ? 'Animations On' : 'Animations Off'"
description="Enable page transition animations."
/>
</template>
</UPageCard>
</div>
</div>
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi">
<template #header>
<div class="ytp-card w-full">
<div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2">
<UIcon name="i-lucide-image" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Background</span>
</div>
</template>
</div>
<template #body>
<div class="p-4 sm:p-5 space-y-4">
<USwitch
v-model="bg_enable"
class="w-full"
@ -95,7 +95,7 @@
<UButton
v-if="bg_enable"
color="info"
color="neutral"
variant="outline"
icon="i-lucide-image-up"
class="w-full justify-center"
@ -121,18 +121,18 @@
class="w-full"
/>
</UFormField>
</template>
</UPageCard>
</div>
</div>
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi">
<template #header>
<div class="ytp-card w-full">
<div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2">
<UIcon name="i-lucide-monitor" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Downloads</span>
</div>
</template>
</div>
<template #body>
<div class="p-4 sm:p-5 space-y-4">
<UFormField v-if="!modeOn" label="URL Separator" class="w-full" :ui="settingsFieldUi">
<USelect
v-model="separator"
@ -179,18 +179,18 @@
:label="show_popover ? 'Popover On' : 'Popover Off'"
description="Show additional information over certain elements."
/>
</template>
</UPageCard>
</div>
</div>
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi">
<template #header>
<div class="ytp-card w-full">
<div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2">
<UIcon name="i-lucide-download" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Queue</span>
</div>
</template>
</div>
<template #body>
<div class="p-4 sm:p-5 space-y-4">
<USwitch
v-model="queue_auto_refresh"
class="w-full"
@ -219,18 +219,18 @@
How often to refresh the queue (5-60 seconds). Lower values increase server load.
</p>
</UFormField>
</template>
</UPageCard>
</div>
</div>
<UPageCard variant="subtle" class="w-full" :ui="settingsCardUi">
<template #header>
<div class="ytp-card w-full">
<div class="p-4 sm:p-5 ytp-border-bottom-soft">
<div class="flex items-center gap-2">
<UIcon name="i-lucide-bell" class="size-4 text-toned" />
<span class="text-sm font-semibold text-highlighted">Notifications</span>
</div>
</template>
</div>
<template #body>
<div class="p-4 sm:p-5 space-y-4">
<USwitch
v-model="allow_toasts"
class="w-full"
@ -288,8 +288,8 @@
:ui="settingsSwitchUi"
:label="toast_dismiss_on_click ? 'Dismiss on click' : 'Keep on click'"
/>
</template>
</UPageCard>
</div>
</div>
</div>
</template>
</USlideover>
@ -337,13 +337,6 @@ const queue_auto_refresh = useStorage<boolean>('queue_auto_refresh', true);
const queue_auto_refresh_delay = useStorage<number>('queue_auto_refresh_delay', 10000);
const isSecureContext = ref<boolean>(false);
const settingsCardUi = {
root: 'w-full',
container: 'w-full p-4 sm:p-5',
wrapper: 'w-full items-stretch',
body: 'w-full space-y-4',
};
const settingsFieldUi = {
root: 'w-full',
container: 'mt-2 w-full',
@ -415,6 +408,10 @@ const toastPositionItems: Array<{ label: string; value: toastPosition }> = [
{ label: 'bottom-right', value: 'bottom-right' },
];
const closeScrollLock = (): void => {
document.body.classList.remove('settings-panel-open');
};
const handleKeydown = (e: KeyboardEvent) => {
if ('Escape' === e.key && props.isOpen) {
e.preventDefault();
@ -434,7 +431,10 @@ onMounted(async () => {
document.addEventListener('keydown', handleKeydown);
});
onBeforeUnmount(() => document.removeEventListener('keydown', handleKeydown));
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleKeydown);
closeScrollLock();
});
const onNotificationTargetChange = async (): Promise<void> => {
if ('browser' === toast_target.value) {
@ -455,7 +455,7 @@ watch(
draftMode.value = mode.value;
document.body.classList.add('settings-panel-open');
} else {
document.body.classList.remove('settings-panel-open');
closeScrollLock();
}
},
);

View file

@ -1,16 +1,22 @@
<template>
<div class="rounded-lg border border-default/70 bg-elevated/40 p-3">
<div class="ytp-card bg-elevated/40 p-3">
<div class="flex items-center justify-between gap-3">
<div class="min-w-0 space-y-1">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-toned">{{ label }}</p>
<UTooltip v-if="tooltip" :text="tooltip" :content="{ side: 'left' }">
<div class="flex items-baseline gap-1.5">
<div :class="valueWrap ? 'space-y-1' : 'flex items-baseline gap-1.5'">
<span class="text-sm font-semibold text-highlighted">{{ value }}</span>
<span v-if="hint" class="truncate text-xs text-toned">{{ hint }}</span>
</div>
</UTooltip>
<div v-else class="flex items-baseline gap-1.5">
<span class="text-sm font-semibold text-highlighted">{{ value }}</span>
<div v-else :class="valueWrap ? 'space-y-1' : 'flex items-baseline gap-1.5'">
<span
:class="[
'text-sm font-semibold text-highlighted',
valueWrap ? 'block wrap-break-word' : '',
]"
>{{ value }}</span
>
<span v-if="hint" class="truncate text-xs text-toned">{{ hint }}</span>
</div>
</div>
@ -37,8 +43,9 @@ const props = withDefaults(
hint?: string;
tooltip?: string;
color?: Color;
valueWrap?: boolean;
}>(),
{ color: 'primary', hint: '', tooltip: '' },
{ color: 'primary', hint: '', tooltip: '', valueWrap: false },
);
const TILE_BG: Record<Color, string> = {

View file

@ -363,12 +363,10 @@
</UButton>
</div>
<div
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
>
<div class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface">
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-215 table-fixed w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>

View file

@ -116,7 +116,7 @@
<span>Result:</span>
</div>
<div class="overflow-hidden rounded-lg border border-default bg-default">
<div class="overflow-hidden ytp-frame">
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<pre
class="min-w-0 max-w-full overflow-x-auto p-4 text-xs leading-6 text-default"

View file

@ -34,7 +34,7 @@
<div
v-if="showList && filteredOptions.length"
class="absolute inset-x-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-default bg-default shadow-lg"
class="absolute inset-x-0 top-full z-20 mt-1 overflow-hidden rounded-md ytp-floating-surface"
role="menu"
>
<button

View file

@ -94,6 +94,7 @@
:label="nativeSubtitleTrack.name || 'Subtitles'"
default
:src="uri(nativeSubtitleTrack.url)"
@load="handleNativeTrackLoad"
/>
Your browser does not support the video tag.
</video>
@ -170,16 +171,30 @@
@click="forceSwitchToHls"
/>
</UTooltip>
<UButton
<USelect
v-if="hasSubtitles"
v-model="subtitleSelectValue"
:items="subtitleSelectItems"
value-key="value"
label-key="label"
color="neutral"
variant="soft"
size="sm"
trailing-icon=""
:content="subtitleSelectContent"
:portal="helpPortal"
:ui="subtitleSelectUi"
class="opacity-65 transition-opacity hover:opacity-100 focus-visible:opacity-100"
:icon="subtitleEnabled ? 'i-lucide-captions' : 'i-lucide-captions-off'"
:aria-label="subtitleEnabled ? 'Disable subtitles' : 'Enable subtitles'"
@click="subtitleEnabled = !subtitleEnabled"
:aria-label="subtitleButtonLabel"
>
<template #default>
<span class="sr-only">{{ subtitleButtonLabel }}</span>
<UIcon
:name="subtitleEnabled ? 'i-lucide-captions' : 'i-lucide-captions-off'"
class="size-4 shrink-0"
/>
</template>
</USelect>
<UButton
color="neutral"
variant="soft"
@ -429,9 +444,12 @@ const timeLabel = computed(() => {
return `${currentLabel} / ${durationLabel}`;
});
const {
subtitleTracks,
subtitleLoading,
subtitleLoadError,
subtitleEnabled,
selectedSubtitleTrack,
selectedSubtitleTrackId,
nativeSubtitleTrack,
usesAssSubtitleTrack,
hasSubtitles,
@ -445,6 +463,40 @@ const {
overlay: assOverlayElement,
});
const SUBTITLE_OFF_VALUE = '__off__';
const subtitleSelectContent = { align: 'end' as const } as const;
const subtitleSelectUi = {
base: 'size-8 min-h-8 min-w-8 justify-center rounded-md px-0',
value: 'flex items-center justify-center px-0',
trailing: 'hidden',
leading: 'hidden',
content: 'z-40 min-w-56 w-auto max-w-[min(24rem,calc(100vw-2rem))]',
} as const;
const subtitleSelectItems = computed(() => [
{ label: 'Off', value: SUBTITLE_OFF_VALUE },
...subtitleTracks.value.map((track) => ({ label: track.name, value: track.url })),
]);
const subtitleButtonLabel = computed(() => {
if (!subtitleEnabled.value || !selectedSubtitleTrack.value) {
return 'Subtitles off';
}
return selectedSubtitleTrack.value.name || 'Subtitles';
});
const subtitleSelectValue = computed<string>({
get: () =>
subtitleEnabled.value
? (selectedSubtitleTrackId.value ?? SUBTITLE_OFF_VALUE)
: SUBTITLE_OFF_VALUE,
set: (value: string) => {
if (SUBTITLE_OFF_VALUE === value) {
subtitleEnabled.value = false;
return;
}
selectedSubtitleTrackId.value = value;
subtitleEnabled.value = true;
},
});
useHead(() => (title.value ? { title: formatPageTitle(`Playing: ${title.value}`) } : {}));
watch(
@ -960,6 +1012,10 @@ function updateMediaSessionPosition(target: EventTarget | null) {
}
async function restoreDefaultTextTrack() {
if (true === destroyed.value) {
return;
}
const el = videoElement.value;
if (!el) {
return;
@ -971,36 +1027,6 @@ async function restoreDefaultTextTrack() {
return;
}
const firstTrack = tracksList[0] as TextTrack | undefined;
const needsReload = firstTrack && (!firstTrack.cues || firstTrack.cues.length === 0);
if (needsReload) {
const trackElements = el.querySelectorAll('track');
trackElements.forEach((trackEl, idx) => {
const parent = trackEl.parentNode;
const clone = trackEl.cloneNode(true) as HTMLTrackElement;
trackEl.remove();
setTimeout(() => {
if (parent) {
parent.appendChild(clone);
clone.addEventListener(
'load',
() => {
const trackObj = clone.track;
if (trackObj) {
trackObj.mode = idx === 0 && subtitleEnabled.value ? 'showing' : 'disabled';
}
},
{ once: true },
);
}
}, idx * 10);
});
return;
}
for (let i = 0; i < tracksList.length; i += 1) {
const track = tracksList[i] as TextTrack | undefined;
if (track) {
@ -1010,18 +1036,34 @@ async function restoreDefaultTextTrack() {
await new Promise((resolve) => setTimeout(resolve, 50));
for (let i = 0; i < tracksList.length; i += 1) {
const track = tracksList[i] as TextTrack | undefined;
if (el !== videoElement.value) {
return;
}
if (false === subtitleEnabled.value || null === nativeSubtitleTrack.value) {
return;
}
const activeTracksList = el.textTracks;
for (let i = 0; i < activeTracksList.length; i += 1) {
const track = activeTracksList[i] as TextTrack | undefined;
if (!track) {
continue;
}
track.mode = i === 0 && subtitleEnabled.value ? 'showing' : 'disabled';
track.mode = i === 0 ? 'showing' : 'disabled';
}
} catch (error) {
console.warn('Failed to restore subtitle track state', error);
}
}
function handleNativeTrackLoad() {
if (true === destroyed.value) {
return;
}
void restoreDefaultTextTrack();
}
function applyMediaSessionMetadata() {
if (false === 'mediaSession' in navigator) {
return;
@ -1242,6 +1284,10 @@ watch(subtitleEnabled, () => {
void nextTick(() => restoreDefaultTextTrack());
});
watch(selectedSubtitleTrack, () => {
void nextTick(() => restoreDefaultTextTrack());
});
onMounted(async () => {
disableOpacity();
isTouchDevice.value = window.matchMedia('(pointer: coarse)').matches;

View file

@ -123,12 +123,10 @@
<UBadge color="neutral" variant="soft" size="sm">{{ group.items.length }}</UBadge>
</div>
<div
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
>
<div class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface">
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-180 w-full table-auto text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-left [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -188,10 +186,7 @@
</section>
</template>
<div
v-else
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
>
<div v-else class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface">
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-215 w-full table-auto text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">

View file

@ -11,12 +11,7 @@
/>
</div>
<UPageCard
variant="outline"
:ui="pageCardUi"
class="relative w-full max-w-xl overflow-hidden bg-default/95"
>
<template #body>
<div class="ytp-panel p-0 relative w-full max-w-xl overflow-hidden bg-default/95">
<div class="space-y-6 px-5 py-6 sm:px-7 sm:py-8" role="status" aria-live="polite">
<div
class="inline-flex items-center gap-2 rounded-full border border-default bg-elevated/60 px-3 py-1.5 text-xs font-semibold tracking-[0.22em] text-toned uppercase"
@ -49,16 +44,8 @@
description="The native app is closing background services and should exit shortly."
/>
</div>
</template>
</UPageCard>
</div>
</div>
</template>
<script setup lang="ts">
const pageCardUi = {
root: 'w-full bg-transparent shadow-2xl shadow-primary/5',
container: 'w-full p-0',
wrapper: 'w-full items-stretch',
body: 'w-full p-0',
};
</script>
<script setup lang="ts"></script>

View file

@ -54,7 +54,10 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
const subtitleLoading = ref(false);
const subtitleLoadError = ref('');
const subtitleEnabled = ref(true);
const selectedTrack = computed(() => tracks.value[0] || null);
const selectedTrackId = ref<string | null>(null);
const selectedTrack = computed(
() => tracks.value.find((track) => track.url === selectedTrackId.value) || null,
);
const nativeSubtitleTrack = computed(() => {
const track = selectedTrack.value;
return subtitleEnabled.value && track?.renderer === 'native' ? track : null;
@ -82,6 +85,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
assRequestId += 1;
destroyAssRenderer();
tracks.value = [];
selectedTrackId.value = null;
subtitleLoadError.value = '';
if (!manifestUrl || !isVideo || !canPlay) {
@ -104,6 +108,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
tracks.value = (payload as SubtitleManifestResponse).subtitles || [];
if (tracks.value.length > 0) {
selectedTrackId.value = tracks.value[0]?.url || null;
subtitleEnabled.value = true;
}
} catch {
@ -212,6 +217,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) {
subtitleLoadError,
subtitleEnabled,
selectedSubtitleTrack: selectedTrack,
selectedSubtitleTrackId: selectedTrackId,
nativeSubtitleTrack,
usesAssSubtitleTrack: usesAssTrack,
hasSubtitles,

View file

@ -1,8 +1,7 @@
<template>
<NuxtLayout name="error-layout">
<div class="flex min-h-full flex-1 items-start justify-center py-4 sm:py-8">
<UPageCard variant="outline" :ui="pageCardUi" class="w-full">
<template #body>
<div class="ytp-card p-0 w-full">
<div class="space-y-6 px-4 py-5 sm:px-6 sm:py-6 lg:px-7">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0 space-y-2">
@ -17,8 +16,8 @@
</h1>
<p class="max-w-3xl text-sm leading-6 text-toned">
An unexpected error interrupted this view. You can go back home or retry the
current route.
An unexpected error interrupted this view. You can go back home or retry the current
route.
</p>
</div>
@ -65,8 +64,7 @@
</div>
</section>
</div>
</template>
</UPageCard>
</div>
</div>
</NuxtLayout>
</template>
@ -80,13 +78,6 @@ const props = defineProps<{
const showStacks = ref(false);
const pageCardUi = {
root: 'w-full bg-default',
container: 'w-full p-0',
wrapper: 'w-full items-stretch',
body: 'w-full p-0',
};
const handleRetry = async (): Promise<void> => {
await clearError({ redirect: useRoute().fullPath });
};

View file

@ -183,7 +183,7 @@
<UButton
v-if="true === config.app.is_native"
color="error"
color="neutral"
variant="ghost"
size="sm"
icon="i-lucide-power"

View file

@ -6,19 +6,14 @@
aria-hidden="true"
/>
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div class="flex min-w-0 items-start gap-3">
<span
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<UIcon :name="pageShell.icon" class="size-5" />
</span>
<div class="min-w-0 flex-1 space-y-3">
<nav
aria-label="Breadcrumb"
class="flex min-w-0 flex-wrap items-center gap-2 text-xs font-medium uppercase tracking-[0.2em] text-toned"
>
<nav aria-label="Breadcrumb" class="min-w-0 ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -50,7 +45,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
color="neutral"
:variant="show_filter ? 'soft' : 'outline'"
@ -123,7 +118,7 @@
<div
v-if="controlEnabled && hasItems"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
>
<div class="flex flex-wrap items-center gap-2">
<UButton
@ -169,11 +164,11 @@
<div
v-if="contentStyle === 'list' && hasItems"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-360 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -320,18 +315,12 @@
</div>
<div v-else-if="hasItems" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<UCard
<div
v-for="item in filteredItems"
:key="item.path"
class="flex h-full flex-col"
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
class="ytp-card flex h-full flex-col overflow-hidden"
>
<template #header>
<div class="p-4 pb-3 ytp-border-bottom-soft">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<div class="flex items-start gap-2">
@ -364,8 +353,9 @@
</label>
</div>
</div>
</template>
</div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="flex flex-wrap gap-2 text-sm *:min-w-32 *:flex-1">
<div
class="min-w-0 rounded-md border border-default bg-muted/20 px-3 py-2 text-center text-default"
@ -387,8 +377,9 @@
</UTooltip>
</div>
</div>
</div>
<template v-if="controlEnabled" #footer>
<div v-if="controlEnabled" class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
v-if="item.type === 'file'"
@ -437,8 +428,8 @@
Delete
</UButton>
</div>
</template>
</UCard>
</div>
</div>
</div>
<div v-if="localSearch && !hasItems && !isLoading" class="space-y-3">
@ -610,7 +601,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{
label: 'Delete Selected',
icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || isLoading.value,
onSelect: () => void handleDeleteSelected(),
},

View file

@ -1,17 +1,13 @@
<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"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
v-if="logs.length > 0"
color="neutral"
@ -64,89 +60,108 @@
/>
<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"
<div v-if="filteredLogs.length > 0" class="space-y-5">
<section v-for="log in filteredLogs" :key="log.tag" class="space-y-3">
<section class="space-y-3">
<button
type="button"
class="flex w-full flex-wrap items-center justify-between gap-3 text-left"
@click="toggleRelease(log.tag)"
>
<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>
<div class="flex min-w-0 items-center gap-3">
<span class="ytp-section-icon">
<UIcon
:name="isInstalled(log) ? 'i-lucide-badge-check' : 'i-lucide-git-branch'"
class="size-5"
/>
</span>
<div class="min-w-0 space-y-1">
<h2 class="truncate text-base font-semibold text-highlighted">
{{ log.tag }}
</h2>
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-toned">
<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 class="inline-flex cursor-help items-center gap-1.5">
<UIcon name="i-lucide-calendar-days" class="size-3.5 text-muted" />
<span>{{ moment(log.date).fromNow() }}</span>
</span>
</UTooltip>
</div>
</div>
</div>
<div class="space-y-3 border-t border-default pt-4">
<div class="flex shrink-0 items-center gap-2">
<UBadge
v-if="isInstalled(log)"
color="success"
variant="soft"
size="sm"
icon="i-lucide-check"
>
Installed
</UBadge>
<UBadge color="neutral" variant="outline" size="sm" icon="i-lucide-list">
{{ log.commits?.length || 0 }} commits
</UBadge>
<UIcon
name="i-lucide-chevron-right"
:class="[
'size-4 text-toned transition-transform',
isReleaseOpen(log.tag) ? 'rotate-90' : '',
]"
/>
</div>
</button>
<div v-if="isReleaseOpen(log.tag)" class="space-y-2">
<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"
class="rounded-md border border-default bg-elevated/20 px-3 py-3 transition-colors hover:bg-elevated/35"
>
<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"
class="block min-w-0 text-sm font-medium leading-5 text-highlighted transition hover:text-primary"
>
<span class="font-semibold text-highlighted">
{{ ucFirst(commit.message).replace(/\.$/, '') }}.
</span>
{{ formatCommitMessage(commit.message) }}
</NuxtLink>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-toned">
<div class="mt-2 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"
class="inline-flex items-center gap-1 rounded-sm border border-default px-2 py-1"
>
<UIcon name="i-lucide-user" class="size-3.5" />
{{ commit.author }}
<UIcon name="i-lucide-user" class="size-3.5 text-muted" />
<span>{{ commit.author }}</span>
</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"
<time
class="inline-flex cursor-help items-center gap-1 rounded-sm border border-default px-2 py-1"
>
<UIcon name="i-lucide-clock-3" class="size-3.5" />
{{ moment(commit.date).fromNow() }}
</span>
<UIcon name="i-lucide-clock-3" class="size-3.5 text-muted" />
<span>{{ moment(commit.date).fromNow() }}</span>
</time>
</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"
<NuxtLink
:to="`${REPO}/commit/${commit.full_sha}`"
target="_blank"
class="inline-flex items-center gap-1 rounded-sm border border-default px-2 py-1 font-mono transition hover:border-primary hover:text-primary"
>
<UIcon name="i-lucide-git-commit-horizontal" class="size-3.5" />
{{ commit.sha }}
</span>
<UIcon name="i-lucide-git-commit-horizontal" class="size-3.5 text-muted" />
<span>{{ commit.sha }}</span>
</NuxtLink>
</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"
class="inline-flex items-center gap-1 rounded-sm border border-default px-2 py-1 font-medium"
>
<UIcon name="i-lucide-check" class="size-3.5 text-success" />
<span>Installed</span>
@ -154,9 +169,8 @@
</div>
</article>
</div>
</div>
</template>
</UPageCard>
</section>
</section>
</div>
<div v-else class="space-y-3">
@ -206,13 +220,7 @@ 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',
};
const openReleases = useStorage<string[]>('changelog_open_releases', []);
watch(toggleFilter, () => {
if (!toggleFilter.value) {
@ -250,6 +258,27 @@ const filteredLogs = computed<changelogs>(() => {
.filter((log): log is changeset => log !== null);
});
watch(filteredLogs, (items) => {
if (openReleases.value.length > 0 || items.length < 1) {
return;
}
openReleases.value = items.slice(0, 3).map((log) => log.tag);
});
const isReleaseOpen = (tag: string): boolean => openReleases.value.includes(tag);
const toggleRelease = (tag: string): void => {
if (isReleaseOpen(tag)) {
openReleases.value = openReleases.value.filter((entry) => entry !== tag);
return;
}
openReleases.value = [...openReleases.value, tag];
};
const formatCommitMessage = (message: string): string => `${ucFirst(message).replace(/\.$/, '')}.`;
const loadContent = async (): Promise<void> => {
if (app_version.value === '' || logs.value.length > 0) {
return;

View file

@ -1,17 +1,13 @@
<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"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
v-if="items.length > 0"
color="neutral"
@ -84,7 +80,7 @@
<div
v-if="!isLoading && filteredItems.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
>
<div class="flex flex-wrap items-center gap-2">
<UButton
@ -129,11 +125,11 @@
<div
v-if="contentStyle === 'list' && filteredItems.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-235 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -238,16 +234,8 @@
<div v-else-if="filteredItems.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div v-for="cond in filteredItems" :key="cond.id" class="min-w-0 w-full max-w-full">
<UCard
class="flex h-full min-w-0 w-full max-w-full flex-col"
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
<div class="p-4 pb-3 ytp-border-bottom-soft">
<div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<div class="flex items-start gap-2">
@ -283,8 +271,9 @@
</label>
</div>
</div>
</template>
</div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<button
@ -319,7 +308,9 @@
<UIcon name="i-lucide-filter" class="mt-0.5 size-4 shrink-0 text-toned" />
<div class="min-w-0 flex-1">
<div class="text-xs font-medium text-toned">Filter</div>
<span :class="['block', expandClass(cond.id, 'filter')]">{{ cond.filter }}</span>
<span :class="['block', expandClass(cond.id, 'filter')]">{{
cond.filter
}}</span>
</div>
</button>
@ -375,8 +366,9 @@
</div>
</div>
</div>
</div>
<template #footer>
<div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
color="neutral"
@ -398,8 +390,8 @@
Delete
</UButton>
</div>
</template>
</UCard>
</div>
</div>
</div>
</div>
@ -566,7 +558,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{
label: 'Remove Selected',
icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(),
},

View file

@ -1,17 +1,13 @@
<template>
<main class="flex min-h-0 w-full min-w-0 max-w-full flex-1 flex-col gap-6">
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
<div class="flex min-w-0 items-start gap-3">
<span
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<div class="ytp-page-header">
<div class="ytp-page-heading items-start">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -45,19 +41,15 @@
</div>
</div>
<UPageCard variant="naked" :ui="pageCardUi" class="flex min-h-0 flex-1">
<template #body>
<div class="ytp-card flex min-h-0 flex-1 p-0 sm:p-0 flex-col">
<div class="flex min-h-0 flex-1 flex-col gap-4">
<div
class="flex min-h-72 min-w-0 flex-1 overflow-hidden rounded-sm border border-default bg-neutral-950/95 shadow-sm"
>
<div
ref="terminal_window"
class="terminal-host h-full min-h-0 w-full overflow-hidden"
/>
<div ref="terminal_window" class="terminal-host h-full min-h-0 w-full overflow-hidden" />
</div>
<div class="rounded-xl border border-default bg-default shadow-sm">
<div class="ytp-card shadow-sm">
<div
class="flex flex-col gap-3 border-b border-default bg-muted/10 px-4 py-3 lg:flex-row lg:items-start lg:justify-between"
>
@ -172,9 +164,7 @@
:ui="historyCardUi"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<div
class="flex items-center gap-2 text-sm font-semibold text-highlighted"
>
<div class="flex items-center gap-2 text-sm font-semibold text-highlighted">
<UIcon name="i-lucide-history" class="size-4 text-toned" />
<span>Recent runs</span>
</div>
@ -201,7 +191,7 @@
<div
v-else
class="max-h-96 w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="max-h-96 w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-auto overscroll-contain">
<table class="w-full text-sm">
@ -261,9 +251,7 @@
.format('YYYY-M-DD H:mm Z')
"
>
<span
class="inline-flex items-center gap-1.5 cursor-help"
>
<span class="inline-flex items-center gap-1.5 cursor-help">
<UIcon name="i-lucide-clock-3" class="size-3.5" />
<time
:datetime="
@ -370,8 +358,7 @@
</div>
</div>
</div>
</template>
</UPageCard>
</div>
</main>
</template>
@ -436,13 +423,6 @@ const commandInput = ref<InstanceType<typeof InputAutocomplete> | null>(null);
const commandTextarea = ref<InstanceType<typeof TextareaAutocomplete> | null>(null);
const storedCommand = useStorage<string>('console_command', '');
const pageCardUi = {
root: 'flex min-h-0 flex-1 w-full bg-transparent',
container: 'flex min-h-0 flex-1 w-full p-0 sm:p-0',
wrapper: 'flex min-h-0 flex-1 w-full items-stretch',
body: 'flex min-h-0 flex-1 w-full flex-col',
};
const historyCardUi = {
body: 'space-y-3 p-4',
};

View file

@ -1,17 +1,13 @@
<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-start gap-3">
<span
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<div class="ytp-page-header">
<div class="ytp-page-heading items-start">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
color="neutral"
variant="outline"
@ -47,10 +43,7 @@
</div>
</div>
<div
v-if="!report && isLoading"
class="flex min-h-72 items-center justify-center rounded-md border border-default bg-default/90"
>
<div v-if="!report && isLoading" class="ytp-card flex min-h-72 items-center justify-center">
<div class="flex flex-col items-center gap-3 text-center text-toned">
<UIcon name="i-lucide-loader-circle" class="size-10 animate-spin text-info" />
<div class="space-y-1">
@ -86,28 +79,15 @@
</div>
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<article
<StatCard
v-for="item in summaryCards"
:key="item.label"
class="rounded-md border border-default bg-default px-3 py-3 shadow-sm"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="text-xs font-medium uppercase tracking-wide text-toned">
{{ item.label }}
</p>
<p class="mt-1 text-xs text-toned">{{ item.description }}</p>
</div>
<span
class="inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-default bg-default"
>
<UIcon :name="item.icon" class="size-4 text-toned" />
</span>
</div>
<p :class="['mt-4 text-2xl font-semibold', item.valueClass]">{{ item.value }}</p>
</article>
:label="item.label"
:value="item.value"
:hint="item.description"
:icon="item.icon"
:color="item.color"
/>
</div>
</section>
@ -118,28 +98,16 @@
</div>
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
<article
<StatCard
v-for="row in runtimeRows"
:key="row.label"
class="rounded-md border border-default bg-default px-3 py-3 shadow-sm"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="text-xs font-medium uppercase tracking-wide text-toned">
{{ row.label }}
</p>
<p class="mt-1 text-xs text-toned">{{ row.description }}</p>
</div>
<span
class="inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-default bg-default"
>
<UIcon :name="row.icon" class="size-4 text-toned" />
</span>
</div>
<p class="mt-4 wrap-break-word text-sm font-semibold text-default">{{ row.value }}</p>
</article>
:label="row.label"
:value="row.value"
:hint="row.description"
:icon="row.icon"
color="neutral"
value-wrap
/>
</div>
</section>
@ -155,11 +123,7 @@
</div>
<div class="grid gap-4 lg:grid-cols-2 2xl:grid-cols-3">
<article
v-for="item in section.items"
:key="item.id"
class="rounded-md border border-default bg-default px-4 py-4 shadow-sm"
>
<article v-for="item in section.items" :key="item.id" class="ytp-card-padded shadow-sm">
<div class="min-w-0 space-y-3">
<div class="space-y-2">
<div class="flex flex-wrap items-center gap-2">
@ -198,6 +162,7 @@
<script setup lang="ts">
import moment from 'moment';
import StatCard from '~/components/StatCard.vue';
import type { DiagnosticCheck, DiagnosticStatus } from '~/types/diagnostics';
import { requirePageShell } from '~/utils/topLevelNavigation';
import { copyText } from '~/utils';
@ -207,7 +172,7 @@ type SummaryCard = {
description: string;
value: number;
icon: string;
valueClass: string;
color: 'success' | 'error' | 'warning' | 'neutral';
};
type DetailRow = {
@ -295,28 +260,28 @@ const summaryCards = computed<Array<SummaryCard>>(() => {
description: 'Checks that passed.',
value: current.summary.pass,
icon: 'i-lucide-badge-check',
valueClass: current.summary.pass > 0 ? 'text-success' : 'text-default',
color: current.summary.pass > 0 ? 'success' : 'neutral',
},
{
label: 'Required fails',
description: 'Items that block core use.',
value: current.summary.required_failed,
icon: 'i-lucide-octagon-alert',
valueClass: current.summary.required_failed > 0 ? 'text-error' : 'text-default',
color: current.summary.required_failed > 0 ? 'error' : 'neutral',
},
{
label: 'Warnings',
description: 'Optional or incomplete items.',
value: current.summary.warn,
icon: 'i-lucide-triangle-alert',
valueClass: current.summary.warn > 0 ? 'text-warning' : 'text-default',
color: current.summary.warn > 0 ? 'warning' : 'neutral',
},
{
label: 'Skipped',
description: 'Not configured or not needed.',
value: current.summary.skip,
icon: 'i-lucide-minus',
valueClass: current.summary.skip > 0 ? 'text-toned' : 'text-default',
color: 'neutral',
},
];
});

View file

@ -1,17 +1,13 @@
<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"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
v-if="items.length > 0"
color="neutral"
@ -84,7 +80,7 @@
<div
v-if="!isLoading && filteredItems.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
>
<div class="flex flex-wrap items-center gap-2">
<UButton
@ -129,11 +125,11 @@
<div
v-if="contentStyle === 'list' && filteredItems.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-235 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -237,16 +233,8 @@
<div v-else-if="filteredItems.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div v-for="field in filteredItems" :key="field.id" class="min-w-0 w-full max-w-full">
<UCard
class="flex h-full min-w-0 w-full max-w-full flex-col"
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
<div class="p-4 pb-3 ytp-border-bottom-soft">
<div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<div class="flex items-start gap-2">
@ -282,8 +270,9 @@
</label>
</div>
</div>
</template>
</div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<span
@ -310,7 +299,9 @@
<UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" />
<div class="min-w-0 flex-1">
<div class="text-xs font-medium text-toned">Associated option</div>
<span :class="['block', expandClass(field.id, 'field')]">{{ field.field }}</span>
<span :class="['block', expandClass(field.id, 'field')]">{{
field.field
}}</span>
</div>
</button>
@ -333,8 +324,9 @@
</button>
</div>
</div>
</div>
<template #footer>
<div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
color="neutral"
@ -356,8 +348,8 @@
Delete
</UButton>
</div>
</template>
</UCard>
</div>
</div>
</div>
</div>
@ -491,7 +483,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{
label: 'Remove Selected',
icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(),
},

View file

@ -1,17 +1,13 @@
<template>
<main class="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"
>
<main class="w-full min-w-0 max-w-full space-y-6">
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -20,42 +16,24 @@
<p class="max-w-3xl text-sm text-toned">{{ pageShell.description }}</p>
</div>
</div>
<div class="flex flex-wrap justify-end gap-2 xl:justify-end">
<UButton
v-for="entry in docsEntries"
:key="entry.id"
:to="entry.route"
:color="entry.file === docEntry.file ? 'primary' : 'neutral'"
:variant="entry.file === docEntry.file ? 'solid' : 'outline'"
size="sm"
:icon="entry.icon"
>
{{ entry.navLabel }}
</UButton>
</div>
</div>
<UPageCard variant="outline" :ui="pageCardUi">
<template #body>
<div class="px-4 py-5 sm:px-6 sm:py-6 lg:px-7">
<div class="ytp-card p-0 overflow-hidden">
<div class="min-w-0 max-w-full px-4 py-5 sm:px-6 sm:py-6 lg:px-7">
<Markdown :file="`/api/docs/${docEntry.file}`" />
</div>
</template>
</UPageCard>
</div>
</main>
</template>
<script setup lang="ts">
import Markdown from '~/components/Markdown.vue';
import { DOCS_ENTRIES, getDocsEntryBySlug } from '~/composables/useDocs';
import { getDocsEntryBySlug } from '~/composables/useDocs';
import { formatPageTitle } from '~/utils';
import { requirePageShell } from '~/utils/topLevelNavigation';
const route = useRoute();
const docsEntries = DOCS_ENTRIES;
const docEntry = computed(() => {
const entry = getDocsEntryBySlug(route.params.slug as string | string[] | undefined);
@ -74,11 +52,4 @@ const pageShell = computed(() => requirePageShell(docEntry.value.id));
useHead(() => ({
title: formatPageTitle(docEntry.value.title),
}));
const pageCardUi = {
root: 'w-full bg-default',
container: 'w-full p-0',
wrapper: 'w-full items-stretch',
body: 'w-full p-0',
};
</script>

View file

@ -6,18 +6,14 @@
aria-hidden="true"
/>
<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"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -27,7 +23,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
color="neutral"
variant="outline"
@ -102,7 +98,7 @@
<div class="w-full min-w-0 max-w-full space-y-4">
<div
v-if="hasItems"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
>
<div class="flex flex-wrap items-center gap-2">
<UButton
@ -143,11 +139,11 @@
<div
v-if="'list' === contentStyle && hasItems"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-210 table-fixed w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -395,16 +391,8 @@
:min-height="showThumbnails ? 410 : 210"
class="min-h-0 min-w-0 w-full max-w-full"
>
<UCard
class="flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden"
:ui="{
body: 'flex flex-1 flex-col gap-4 p-4',
footer: 'border-t border-default px-4 py-4',
header: 'p-4 pb-3',
root: 'bg-default border border-default',
}"
>
<template #header>
<div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
<div class="ytp-border-bottom-soft p-4 pb-3">
<div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<UTooltip :text="item.title">
@ -474,8 +462,9 @@
</label>
</div>
</div>
</template>
</div>
<div class="flex flex-1 flex-col gap-4 p-4">
<div
v-if="showThumbnails"
class="-mx-4 -mt-4 overflow-hidden border-b border-default bg-muted/20"
@ -626,7 +615,7 @@
</p>
</div>
<template #footer>
<div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
v-if="showRetryAction(item)"
@ -685,8 +674,9 @@
</UButton>
</UDropdownMenu>
</div>
</template>
</UCard>
</div>
</div>
</div>
</LateLoader>
</div>

View file

@ -1,17 +1,13 @@
<template>
<div class="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"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
color="neutral"
:variant="toggleFilter ? 'soft' : 'outline'"
@ -56,7 +52,7 @@
<UButton
color="neutral"
variant="outline"
:variant="config.showForm ? 'soft' : 'outline'"
size="sm"
icon="i-lucide-plus"
@click="config.showForm = !config.showForm"
@ -109,7 +105,7 @@
<div class="w-full min-w-0 max-w-full space-y-4">
<div v-if="!socket.isConnected" class="flex justify-end">
<UButton
color="info"
color="neutral"
variant="outline"
size="sm"
icon="i-lucide-refresh-cw"
@ -122,7 +118,7 @@
<div
v-if="hasItems"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
>
<div class="flex flex-wrap items-center gap-2">
<UButton
@ -175,11 +171,11 @@
<div
v-if="'list' === contentStyle && hasItems"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-210 table-fixed w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -409,16 +405,8 @@
:min-height="showThumbnails ? 475 : 265"
class="min-h-0 min-w-0 w-full max-w-full"
>
<UCard
class="flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden"
:ui="{
body: 'flex flex-1 flex-col gap-4 p-4',
footer: 'border-t border-default px-4 py-4',
header: 'p-4 pb-3',
root: 'bg-default border border-default',
}"
>
<template #header>
<div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
<div class="p-4 pb-3 ytp-border-bottom-soft">
<div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<UTooltip :text="item.title">
@ -494,8 +482,9 @@
</label>
</div>
</div>
</template>
</div>
<div class="flex flex-1 flex-col gap-4 p-4">
<div
v-if="showThumbnails"
class="-mx-4 -mt-4 overflow-hidden border-b border-default bg-muted/20"
@ -572,7 +561,10 @@
@click="toggleExpand(item._id, 'preset')"
>
<span class="inline-flex w-full items-center justify-center gap-2">
<UIcon name="i-lucide-sliders-horizontal" class="size-4 shrink-0 text-toned" />
<UIcon
name="i-lucide-sliders-horizontal"
class="size-4 shrink-0 text-toned"
/>
<span :class="['min-w-0 text-center', expandClass(item._id, 'preset')]">
{{ item.preset }}
</span>
@ -610,8 +602,9 @@
</span>
</button>
</div>
</div>
<template #footer>
<div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
color="neutral"
@ -657,8 +650,8 @@
</UButton>
</UDropdownMenu>
</div>
</template>
</UCard>
</div>
</div>
</LateLoader>
</div>

View file

@ -17,11 +17,9 @@
<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-start gap-3">
<span
class="inline-flex size-11 shrink-0 items-center justify-center rounded-md border border-default bg-elevated/70 text-primary"
>
<div class="ytp-page-header">
<div class="ytp-page-heading items-start">
<span class="ytp-page-icon">
<UIcon
name="i-lucide-file-text"
:class="[
@ -34,9 +32,7 @@
</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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -46,7 +42,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
v-if="!autoScroll"
color="neutral"
@ -123,9 +119,8 @@
</div>
</div>
<UPageCard variant="naked" :ui="pageCardUi">
<template #body>
<div class="overflow-hidden rounded-sm border border-default bg-default shadow-sm">
<div class="ytp-card min-w-0 max-w-full p-4 sm:p-5 overflow-hidden">
<div class="ytp-frame overflow-hidden shadow-sm">
<div
ref="logContainer"
class="w-full min-w-0 max-w-full min-h-[55vh] max-h-[60vh] overflow-y-auto overflow-x-hidden bg-transparent font-mono text-sm text-default overscroll-x-contain"
@ -249,8 +244,7 @@
</div>
</div>
</div>
</template>
</UPageCard>
</div>
<LogDetailModal v-model="detailsOpen" :log="selectedLog" />
</main>
@ -316,13 +310,6 @@ const reachedEnd = ref(false);
const detailsOpen = ref(false);
const expandedRows = ref<Set<string>>(new Set());
const pageCardUi = {
root: 'w-full min-w-0 max-w-full bg-transparent',
container: 'w-full min-w-0 max-w-full p-4 sm:p-5',
wrapper: 'w-full min-w-0 items-stretch',
body: 'w-full min-w-0 max-w-full overflow-hidden',
};
const query = ref<string>(
(() => {
const filter = route.query.filter ?? '';

View file

@ -1,17 +1,13 @@
<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"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
v-if="notifications.length > 0"
color="neutral"
@ -97,7 +93,7 @@
<div
v-if="!isLoading && filteredTargets.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
>
<div class="flex flex-wrap items-center gap-2">
<UButton
@ -142,11 +138,11 @@
<div
v-if="contentStyle === 'list' && filteredTargets.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-235 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -275,16 +271,8 @@
<div v-else-if="filteredTargets.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div v-for="item in filteredTargets" :key="item.id" class="min-w-0 w-full max-w-full">
<UCard
class="flex h-full min-w-0 w-full max-w-full flex-col"
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
<div class="p-4 pb-3 ytp-border-bottom-soft">
<div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<div class="flex items-start gap-2">
@ -323,8 +311,9 @@
</label>
</div>
</div>
</template>
</div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<button
@ -434,8 +423,9 @@
</button>
</div>
</div>
</div>
<template #footer>
<div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
color="neutral"
@ -457,8 +447,8 @@
Delete
</UButton>
</div>
</template>
</UCard>
</div>
</div>
</div>
</div>
@ -657,7 +647,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{
label: 'Remove Selected',
icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(),
},

View file

@ -1,17 +1,13 @@
<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"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
v-if="presets.length > 0"
color="neutral"
@ -83,7 +79,7 @@
<div
v-if="!isLoading && filteredPresets.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
>
<div class="flex flex-wrap items-center gap-2">
<UButton
@ -128,11 +124,11 @@
<div
v-if="contentStyle === 'list' && filteredPresets.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-200 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -236,16 +232,8 @@
<div v-else-if="filteredPresets.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div v-for="item in filteredPresets" :key="item.id" class="min-w-0 w-full max-w-full">
<UCard
class="flex h-full min-w-0 w-full max-w-full flex-col"
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
<div class="p-4 pb-3 ytp-border-bottom-soft">
<div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<div class="flex items-start gap-2">
@ -283,8 +271,9 @@
</label>
</div>
</div>
</template>
</div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<span
@ -367,8 +356,9 @@
</div>
</button>
</div>
</div>
<template #footer>
<div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
color="neutral"
@ -390,8 +380,8 @@
Delete
</UButton>
</div>
</template>
</UCard>
</div>
</div>
</div>
</div>
@ -540,7 +530,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{
label: 'Remove Selected',
icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(),
},

View file

@ -1,6 +1,7 @@
<template>
<div>
<AppRoot mode="simple" @ready="init" v-slot="{ openSettings }">
<div class="shell-stage flex min-h-screen flex-col bg-default/95 backdrop-blur-sm">
<div class="shell-stage shell-surface flex min-h-screen flex-col">
<ConnectionBanner />
<div
@ -14,8 +15,7 @@
<div class="transition-transform duration-300">
<div class="mx-auto w-full transition-all duration-300" :class="formContainerClass">
<UPageCard variant="outline" :ui="formCardUi">
<template #body>
<div class="ytp-card p-4 sm:p-5">
<form autocomplete="off" class="space-y-4" @submit.prevent="addDownload">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0 space-y-1">
@ -30,7 +30,7 @@
<div class="flex shrink-0 items-center gap-1 sm:gap-2">
<UTooltip v-if="!socketStore.isConnected" text="Reload queue">
<UButton
color="info"
color="neutral"
variant="ghost"
size="sm"
icon="i-lucide-refresh-cw"
@ -64,8 +64,8 @@
<UTooltip :text="showExtras ? 'Hide extra options' : 'Show extra options'">
<UButton
type="button"
color="info"
variant="outline"
color="neutral"
:variant="showExtras ? 'soft' : 'outline'"
size="lg"
:icon="showExtras ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
class="shrink-0 justify-center w-12"
@ -99,7 +99,7 @@
</UButton>
</div>
<div v-if="showExtras" class="space-y-3 border-t border-default pt-4">
<div v-if="showExtras" class="space-y-3 ytp-border-top-soft pt-4">
<UFormField label="Preset" :ui="fieldUi" class="w-full">
<template #label>
<span class="inline-flex items-center gap-2 font-semibold">
@ -155,8 +155,7 @@
</div>
</div>
</form>
</template>
</UPageCard>
</div>
</div>
</div>
@ -203,13 +202,12 @@
tag="div"
class="grid grid-cols-1 gap-3 lg:grid-cols-2"
>
<UCard
<div
v-for="item in queueItems"
:key="`queue-${item._id}`"
class="w-full min-w-0 max-w-full overflow-hidden"
:ui="queueCardUi"
class="ytp-card w-full min-w-0 max-w-full overflow-hidden"
>
<template #header>
<div class="p-4 pb-0 ytp-border-bottom-soft">
<div
v-if="downloadingStatuses.has(item.status) || item.status === null"
class="queue-progress rounded-md border border-default bg-muted/20"
@ -235,8 +233,9 @@
<span>{{ updateProgress(item) }}</span>
</div>
</div>
</template>
</div>
<div class="p-4">
<div class="flex min-w-0 flex-col gap-4 sm:flex-row">
<figure
class="relative w-full shrink-0 overflow-hidden rounded-lg border border-default bg-muted/20 sm:w-52"
@ -299,15 +298,33 @@
<div class="flex flex-wrap items-center gap-2 text-xs">
<UBadge
:color="downloadingStatuses.has(item.status) ? 'info' : 'warning'"
:color="
downloadingStatuses.has(item.status) ? 'info' : 'warning'
"
variant="soft"
size="sm"
class="gap-1"
>
{{ downloadingStatuses.has(item.status) ? 'Active' : 'Queued' }}
<UIcon
:name="getQueueIcon(item)"
:class="['size-3.5', getStatusIconAnimation(item)]"
/>
<span>{{
downloadingStatuses.has(item.status) ? 'Active' : 'Queued'
}}</span>
</UBadge>
<UBadge :color="getStatusColor(item)" variant="soft" size="sm">
{{ getStatusLabel(item) }}
<UBadge
:color="getStatusColor(item)"
variant="soft"
size="sm"
class="gap-1"
>
<UIcon
:name="getStatusIcon(item)"
:class="['size-3.5', getStatusIconAnimation(item)]"
/>
<span>{{ getStatusLabel(item) }}</span>
</UBadge>
<span
@ -332,10 +349,12 @@
</template>
</p>
<div class="mt-auto flex flex-wrap items-center justify-end gap-2 pt-1">
<div
class="mt-auto flex flex-wrap items-center justify-end gap-2 pt-1"
>
<UButton
v-if="!item.status && item.auto_start === false"
color="success"
color="neutral"
variant="soft"
size="xs"
icon="i-lucide-play-circle"
@ -346,7 +365,7 @@
<UButton
v-if="!item.status && item.auto_start === true"
color="warning"
color="neutral"
variant="soft"
size="xs"
icon="i-lucide-pause"
@ -356,7 +375,7 @@
</UButton>
<UButton
color="error"
color="neutral"
variant="outline"
size="xs"
icon="i-lucide-x"
@ -367,7 +386,8 @@
</div>
</div>
</div>
</UCard>
</div>
</div>
</TransitionGroup>
<UAlert
@ -443,12 +463,12 @@
v-if="historyEntries.length > 0"
class="grid grid-cols-1 gap-3 lg:grid-cols-2"
>
<UCard
<div
v-for="item in historyEntries"
:key="`history-${historyPagination.page}-${item._id}`"
class="w-full min-w-0 max-w-full overflow-hidden"
:ui="queueCardUi"
class="ytp-card w-full min-w-0 max-w-full overflow-hidden"
>
<div class="p-4">
<div class="flex min-w-0 flex-col gap-4 sm:flex-row">
<figure
class="relative w-full shrink-0 overflow-hidden rounded-lg border border-default bg-muted/20 sm:w-52"
@ -512,8 +532,17 @@
<div class="flex flex-wrap items-center gap-2 text-xs">
<UBadge color="neutral" variant="soft" size="sm">History</UBadge>
<UBadge :color="getStatusColor(item)" variant="soft" size="sm">
{{ getStatusLabel(item) }}
<UBadge
:color="getStatusColor(item)"
variant="soft"
size="sm"
class="gap-1"
>
<UIcon
:name="getStatusIcon(item)"
:class="['size-3.5', getStatusIconAnimation(item)]"
/>
<span>{{ getStatusLabel(item) }}</span>
</UBadge>
<span
@ -538,7 +567,9 @@
</template>
</p>
<div class="mt-auto flex flex-wrap items-center justify-end gap-2 pt-1">
<div
class="mt-auto flex flex-wrap items-center justify-end gap-2 pt-1"
>
<UButton
v-if="getDownloadLink(item)"
color="primary"
@ -554,7 +585,7 @@
<UButton
v-if="!item.filename"
color="info"
color="neutral"
variant="soft"
size="xs"
icon="i-lucide-rotate-cw"
@ -564,7 +595,7 @@
</UButton>
<UButton
color="error"
color="neutral"
variant="outline"
size="xs"
icon="i-lucide-trash"
@ -575,7 +606,8 @@
</div>
</div>
</div>
</UCard>
</div>
</div>
</div>
<UAlert
@ -663,6 +695,7 @@
</div>
</div>
</AppRoot>
</div>
</template>
<script setup lang="ts">
@ -756,19 +789,6 @@ const fieldUi = {
container: 'space-y-2',
};
const formCardUi = {
root: 'w-full border border-default bg-default',
container: 'w-full p-4 sm:p-5',
wrapper: 'w-full items-stretch',
body: 'w-full',
};
const queueCardUi = {
root: 'w-full border border-default bg-default',
header: 'p-4 pb-0',
body: 'p-4',
};
const urlInputUi = {
root: 'w-full',
base: 'bg-elevated/60 ring-default focus-visible:ring-primary',
@ -1090,6 +1110,57 @@ const getStatusLabel = (item: StoreItem): string => {
return statusOverrides[item.status] ?? ucFirst(item.status.replace(/_/g, ' '));
};
const getQueueIcon = (item: StoreItem): string => {
if (downloadingStatuses.has(item.status)) {
return item.is_live ? 'i-lucide-globe' : 'i-lucide-download';
}
return 'i-lucide-clock-3';
};
const getStatusIcon = (item: StoreItem): string => {
if (!item.auto_start) {
return 'i-lucide-clock-3';
}
if (item.status === null && paused.value === true) {
return 'i-lucide-circle-pause';
}
if (item.status === null) {
return 'i-lucide-circle-question-mark';
}
if (isDownloadSkipped(item)) {
return 'i-lucide-circle-slash';
}
if (item.status === 'downloading' && item.is_live) {
return 'i-lucide-globe';
}
const map: Record<string, string> = {
downloading: 'i-lucide-download',
postprocessing: 'i-lucide-settings-2',
preparing: 'i-lucide-loader-circle',
finished: 'i-lucide-circle-check',
error: 'i-lucide-triangle-alert',
cancelled: 'i-lucide-circle-x',
not_live: 'i-lucide-clock-3',
skip: 'i-lucide-circle-slash',
};
return map[item.status] ?? 'i-lucide-circle-question-mark';
};
const getStatusIconAnimation = (item: StoreItem): string => {
const icon = getStatusIcon(item);
return ['i-lucide-globe', 'i-lucide-settings-2', 'i-lucide-loader-circle'].includes(icon)
? 'animate-spin'
: '';
};
const getStatusColor = (item: StoreItem): 'neutral' | 'info' | 'success' | 'error' | 'warning' => {
if (item.status === null) {
return 'neutral';

View file

@ -1,17 +1,13 @@
<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"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
v-if="definitions.length > 0"
color="neutral"
@ -101,7 +97,7 @@
<div
v-if="!isLoading && filteredDefinitions.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
>
<div class="flex flex-wrap items-center gap-2">
<UButton
@ -134,11 +130,11 @@
<div
v-if="contentStyle === 'list' && filteredDefinitions.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-255 w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -266,16 +262,8 @@
:key="definition.id"
class="min-w-0 w-full max-w-full"
>
<UCard
class="flex h-full min-w-0 w-full max-w-full flex-col"
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
<div class="p-4 pb-3 ytp-border-bottom-soft">
<div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<div class="flex items-start gap-2">
@ -313,8 +301,9 @@
</label>
</div>
</div>
</template>
</div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default">
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
<button
@ -352,8 +341,9 @@
</div>
</button>
</div>
</div>
<template #footer>
<div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
color="neutral"
@ -375,8 +365,8 @@
Delete
</UButton>
</div>
</template>
</UCard>
</div>
</div>
</div>
</div>
@ -572,7 +562,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{
label: 'Remove Selected',
icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(),
},

View file

@ -1,17 +1,13 @@
<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"
>
<div class="ytp-page-header">
<div class="ytp-page-heading">
<span class="ytp-page-icon">
<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"
>
<div class="ytp-page-kicker">
<span>{{ pageShell.sectionLabel }}</span>
<span>/</span>
<span>{{ pageShell.pageLabel }}</span>
@ -21,7 +17,7 @@
</div>
</div>
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2 xl:justify-end">
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end">
<UButton
v-if="tasks.length > 0"
color="neutral"
@ -83,7 +79,7 @@
<div
v-if="!isLoading && filteredTasks.length > 0"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
class="flex flex-wrap items-center justify-between gap-3 ytp-card px-3 py-3"
>
<div class="flex flex-wrap items-center gap-2">
<UButton
@ -128,11 +124,11 @@
<div
v-if="contentStyle === 'list' && filteredTasks.length > 0"
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
class="w-full min-w-0 max-w-full overflow-hidden ytp-table-surface"
>
<div class="w-full max-w-full overflow-x-auto overscroll-x-contain">
<table class="min-w-210 table-fixed w-full text-sm">
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
<thead class="bg-elevated/60 text-xs uppercase tracking-wide text-toned">
<tr
class="text-center [&>th]:border-r [&>th]:border-default/60 [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold [&>th:last-child]:border-r-0"
>
@ -339,16 +335,8 @@
class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"
>
<div v-for="item in filteredTasks" :key="item.id" class="min-w-0 w-full max-w-full">
<UCard
class="flex h-full min-w-0 w-full max-w-full flex-col"
:ui="{
root: 'bg-default border border-default',
header: 'p-4 pb-3',
body: 'flex flex-1 flex-col gap-4 p-4 pt-0',
footer: 'border-t border-default px-4 py-4',
}"
>
<template #header>
<div class="ytp-card flex h-full min-w-0 w-full max-w-full flex-col overflow-hidden">
<div class="p-4 pb-3 ytp-border-bottom-soft">
<div class="flex min-w-0 items-start justify-between gap-3">
<div class="min-w-0 flex-1 space-y-2">
<div class="flex items-start gap-2">
@ -414,8 +402,9 @@
</label>
</div>
</div>
</template>
</div>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<div class="space-y-2 text-sm text-default">
<div class="grid grid-cols-2 gap-2 text-xs text-toned sm:flex sm:flex-wrap">
<button
@ -473,7 +462,11 @@
<div
v-if="
item.timer || item.folder || item.template || item.cli || willTaskBeProcessed(item)
item.timer ||
item.folder ||
item.template ||
item.cli ||
willTaskBeProcessed(item)
"
class="feature-meta-grid"
>
@ -565,8 +558,9 @@
</button>
</div>
</div>
</div>
<template #footer>
<div class="ytp-border-top-soft px-4 py-4">
<div class="flex flex-wrap gap-2 *:min-w-32 *:flex-1">
<UButton
color="neutral"
@ -600,8 +594,8 @@
</UButton>
</UDropdownMenu>
</div>
</template>
</UCard>
</div>
</div>
</div>
</div>
@ -853,7 +847,6 @@ const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
{
label: 'Remove Selected',
icon: 'i-lucide-trash',
color: 'error',
disabled: !hasSelected.value || massDelete.value,
onSelect: () => void deleteSelected(),
},

View file

@ -35,10 +35,18 @@ type TerminalSessionsResponse = {
items: Array<TerminalSessionItem>;
};
type YtpPageHeaderProps = {
icon?: string;
sectionLabel?: string;
pageLabel?: string;
contentClass?: string;
};
export {
ImportedItem,
TerminalSessionItem,
TerminalSessionStatus,
TerminalSessionsResponse,
YtpPageHeaderProps,
version_check,
};

View file

@ -214,8 +214,7 @@ const NavItems: Array<NavDefinition> = [
group: 'docs',
label: 'Changelog',
pageLabel: 'Changelog',
description:
'Latest project changes, loaded remotely when available and falling back to the bundled changelog file.',
description: 'Latest project changes',
icon: 'i-lucide-git-commit-horizontal',
to: '/changelog',
matchPath: '/changelog',

View file

@ -11,9 +11,9 @@
"typecheck": "nuxt typecheck",
"format": "oxfmt --write app/",
"format:check": "oxfmt --check app/",
"lint": "eslint .",
"lint:ci": "eslint . --quiet",
"lint:fix": "eslint . --fix",
"lint": "eslint . --no-warn-ignored",
"lint:ci": "eslint . --quiet --no-warn-ignored",
"lint:fix": "eslint . --fix --no-warn-ignored",
"test": "bun test",
"test:watch": "bun test --watch",
"test:ci": "bun test --ci --dots",

224
uv.lock
View file

@ -147,14 +147,14 @@ wheels = [
[[package]]
name = "anyio"
version = "4.13.0"
version = "4.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
{ url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" },
]
[[package]]
@ -253,11 +253,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2026.5.20"
version = "2026.6.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
]
[[package]]
@ -437,7 +437,7 @@ wheels = [
[[package]]
name = "dateparser"
version = "1.4.0"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
@ -445,9 +445,9 @@ dependencies = [
{ name = "regex" },
{ name = "tzlocal" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/2d/a0ccdb78788064fa0dc901b8524e50615c42be1d78b78d646d0b28d09180/dateparser-1.4.0.tar.gz", hash = "sha256:97a21840d5ecdf7630c584f673338a5afac5dfe84f647baf4d7e8df98f9354a4", size = 321512, upload-time = "2026-03-26T09:56:10.292Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/f4/561c49bca97af561d34eed27e3e831135eb5cb88e754c1150be41820f5c6/dateparser-1.4.1.tar.gz", hash = "sha256:f265df13c0380e2e07543ba74b67c0681aaa1096981ffcd35227e1aa0cb81c7c", size = 314734, upload-time = "2026-06-15T08:45:47.659Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/0b/3c3bb7cbe757279e693a0be6049048012f794d01f81099609ecd53b899f0/dateparser-1.4.0-py3-none-any.whl", hash = "sha256:7902b8e85d603494bf70a5a0b1decdddb2270b9c6e6b2bc8a57b93476c0df378", size = 300379, upload-time = "2026-03-26T09:56:08.409Z" },
{ url = "https://files.pythonhosted.org/packages/8b/7c/2e5dcf53909deddd0bf38cbe277ad9806be038276b1c6c436561b4d9b2e2/dateparser-1.4.1-py3-none-any.whl", hash = "sha256:f25d4e051a84be27a35bd297e3e1dc59ff78373701b89be352ba80372d22d0d0", size = 300503, upload-time = "2026-06-15T08:45:45.951Z" },
]
[[package]]
@ -551,59 +551,59 @@ wheels = [
[[package]]
name = "greenlet"
version = "3.5.1"
version = "3.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" }
sdist = { url = "https://files.pythonhosted.org/packages/dd/8b/befc3cb36965f397d87e86fb3b00e3ec0dc67c1ecb0986d7f54ee528f018/greenlet-3.5.2.tar.gz", hash = "sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c", size = 199243, upload-time = "2026-06-17T20:19:01.317Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" },
{ url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" },
{ url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" },
{ url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" },
{ url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" },
{ url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" },
{ url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" },
{ url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" },
{ url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" },
{ url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" },
{ url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767, upload-time = "2026-05-20T14:09:12.15Z" },
{ url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" },
{ url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786, upload-time = "2026-05-20T14:01:42.064Z" },
{ url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" },
{ url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" },
{ url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" },
{ url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" },
{ url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" },
{ url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" },
{ url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" },
{ url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607, upload-time = "2026-05-20T14:09:13.949Z" },
{ url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287, upload-time = "2026-05-20T14:01:43.143Z" },
{ url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" },
{ url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" },
{ url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" },
{ url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" },
{ url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" },
{ url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" },
{ url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962, upload-time = "2026-05-20T14:09:15.532Z" },
{ url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" },
{ url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047, upload-time = "2026-05-20T14:01:44.39Z" },
{ url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" },
{ url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" },
{ url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" },
{ url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" },
{ url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" },
{ url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998, upload-time = "2026-05-20T14:09:16.912Z" },
{ url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" },
{ url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470, upload-time = "2026-05-20T14:01:45.611Z" },
{ url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" },
{ url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" },
{ url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" },
{ url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" },
{ url = "https://files.pythonhosted.org/packages/d0/3c/bb37b9d40d65b0741a8b040ca5c307034d0a9822994dff5f825c88dd7a6b/greenlet-3.5.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34", size = 287178, upload-time = "2026-06-17T17:35:25.132Z" },
{ url = "https://files.pythonhosted.org/packages/f0/a6/0c5902393f492f8ceb19d0b5cf139284e3a11b333a049739643b1036b6f8/greenlet-3.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a", size = 606900, upload-time = "2026-06-17T18:07:21.692Z" },
{ url = "https://files.pythonhosted.org/packages/d8/7c/42899c31d4b87148ae4e3f87f63e13398824be6241f4dde42ded95768a34/greenlet-3.5.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7", size = 619265, upload-time = "2026-06-17T18:29:44.837Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7e/28f991affb413b232b1e7d768db24c37b3f4d5daecc3f19b455d40bd2dea/greenlet-3.5.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9dc23f0e5ad76415457212a4b947d22ebe4dc80baf02adf7dd5647a90f38bb4e", size = 625044, upload-time = "2026-06-17T18:39:29.046Z" },
{ url = "https://files.pythonhosted.org/packages/d3/52/4ff8c98d3cfe62b4515f8584ae14510a58f35c549cc5292b78d9b7a40b70/greenlet-3.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473", size = 616187, upload-time = "2026-06-17T17:39:29.473Z" },
{ url = "https://files.pythonhosted.org/packages/29/05/0cc9ec660e7acff85f93b0a048b6654371c822c884add44c02a465cf70e0/greenlet-3.5.2-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:423167363c510a75b649f5cd58d873c29498ea03598b9e4b1c3b73e0f899f3d5", size = 427322, upload-time = "2026-06-17T18:41:20.892Z" },
{ url = "https://files.pythonhosted.org/packages/c9/a6/269c8bf9aefc13361ce1088f0e392b154cb21005de7862e42b5d782b81fd/greenlet-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5", size = 1573778, upload-time = "2026-06-17T18:22:13.497Z" },
{ url = "https://files.pythonhosted.org/packages/1f/9b/391d015cbc6323e81b14c02cf825fdca7e0049c9bb489bf4ac72883118ba/greenlet-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5", size = 1638092, upload-time = "2026-06-17T17:40:08.163Z" },
{ url = "https://files.pythonhosted.org/packages/49/53/5b4df711f4356c62e85d9f819d87966d526d1cfb32bae49a8f7d6fc36ea4/greenlet-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c", size = 239352, upload-time = "2026-06-17T17:38:51.593Z" },
{ url = "https://files.pythonhosted.org/packages/bb/b6/18efc3a329ec035c3f344b8f2b60356451950ddf9b7b64ff00023778a1dd/greenlet-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4", size = 237635, upload-time = "2026-06-17T17:35:36.632Z" },
{ url = "https://files.pythonhosted.org/packages/c7/89/aaafc8e14de4ac882e02ccb963225329b0e8578aba4365e71eb678e45722/greenlet-3.5.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", size = 287676, upload-time = "2026-06-17T17:33:31.514Z" },
{ url = "https://files.pythonhosted.org/packages/b8/fc/2308249206c12ac70de7b9a00970f84f07d10b3cd60e05d2fbcaa84124e8/greenlet-3.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", size = 653552, upload-time = "2026-06-17T18:07:23.493Z" },
{ url = "https://files.pythonhosted.org/packages/7c/24/47730d1f8f1336b9b089237521ed7a26eee997065dcb4cab81cdca333abc/greenlet-3.5.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", size = 665756, upload-time = "2026-06-17T18:29:46.616Z" },
{ url = "https://files.pythonhosted.org/packages/23/5c/2664d290cbd1fef9eb3f69b5d3bc5aa91b6fa907519298ca6af93a90c6cb/greenlet-3.5.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d", size = 669989, upload-time = "2026-06-17T18:39:30.79Z" },
{ url = "https://files.pythonhosted.org/packages/99/69/d6c99db15dc0b5e892ac3cc7b942c8b21f4a9cc3bd9ea0bc3b0f339ffbd4/greenlet-3.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", size = 663228, upload-time = "2026-06-17T17:39:31.073Z" },
{ url = "https://files.pythonhosted.org/packages/42/d4/fcb53fa9847d7fbd4723fbed9469c3869b9e3544c4e001d9d5aa2f66162d/greenlet-3.5.2-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef", size = 472888, upload-time = "2026-06-17T18:41:22.511Z" },
{ url = "https://files.pythonhosted.org/packages/4f/88/9e603f448e2bc107c883e95817b980fb9b45ba6aea0299b2e9978124bea2/greenlet-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", size = 1620723, upload-time = "2026-06-17T18:22:14.817Z" },
{ url = "https://files.pythonhosted.org/packages/11/91/26da17e3777858c16fdb8d020a4c68f3a03cb92f238de8f5351d5d5186e9/greenlet-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", size = 1684227, upload-time = "2026-06-17T17:40:09.536Z" },
{ url = "https://files.pythonhosted.org/packages/2d/44/b3a11f7aa34cb38f1b7f3df8bcd9fcd09bac9d342c2a2c9b8686c804bcd2/greenlet-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", size = 240257, upload-time = "2026-06-17T17:35:23.359Z" },
{ url = "https://files.pythonhosted.org/packages/de/e3/3b62145fe917311732041a258adb218248add00542e3131c48bd047fbed5/greenlet-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e", size = 239038, upload-time = "2026-06-17T17:37:56.792Z" },
{ url = "https://files.pythonhosted.org/packages/47/ac/d3bad483e9f6cd1848604fdffa32cac25846dd6dfcec0e6f81c790185518/greenlet-3.5.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", size = 295668, upload-time = "2026-06-17T17:36:02.293Z" },
{ url = "https://files.pythonhosted.org/packages/00/e9/3a7e557b895fd0469b00cd0b2bd498ba950e8bfdf6d7adeecf2c5e4130a6/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", size = 652820, upload-time = "2026-06-17T18:07:24.95Z" },
{ url = "https://files.pythonhosted.org/packages/78/67/6225d5c5e4afc04be0fd161eec82e4b72017e8a100d222f25d7b42b0140d/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", size = 658697, upload-time = "2026-06-17T18:29:48.365Z" },
{ url = "https://files.pythonhosted.org/packages/35/ad/9b3058f999b81750a9c6d9ec424f509462d232b58002086fe2ba63b66407/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c", size = 658945, upload-time = "2026-06-17T18:39:32.509Z" },
{ url = "https://files.pythonhosted.org/packages/fa/99/6324b8ef916dcaddccb340b304c992ca3f947614ce0f2685d438187300b8/greenlet-3.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", size = 656436, upload-time = "2026-06-17T17:39:32.509Z" },
{ url = "https://files.pythonhosted.org/packages/92/75/1b6ecd8c027b69ab1b6798a84094df79aab5e69ac7e249c78b9d361dd1fa/greenlet-3.5.2-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e", size = 490529, upload-time = "2026-06-17T18:41:23.954Z" },
{ url = "https://files.pythonhosted.org/packages/a9/ee/f5bf9daac27c5e1b011965f64b5630a32b415daf7381b312943629e12c2a/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", size = 1617193, upload-time = "2026-06-17T18:22:16.252Z" },
{ url = "https://files.pythonhosted.org/packages/8a/21/b05d5b12715bda92ce27c118d64971d21e9b8f3563ed959a7d271e2d4223/greenlet-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", size = 1677512, upload-time = "2026-06-17T17:40:10.771Z" },
{ url = "https://files.pythonhosted.org/packages/b8/97/1b8f1314b868041b327dc1051603e8142b826480cb0ecb8a7b7632aee9c4/greenlet-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", size = 243145, upload-time = "2026-06-17T17:34:37.502Z" },
{ url = "https://files.pythonhosted.org/packages/36/07/1b5311775e04c718a118c504d7a3a312430e2a1bd1347226aff4774e4549/greenlet-3.5.2-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", size = 288315, upload-time = "2026-06-17T17:34:34.04Z" },
{ url = "https://files.pythonhosted.org/packages/ed/cc/6abcd2a486b58b9f77b7a93b690d59cb2c11a5906ed2ad4c63c7b9c1113d/greenlet-3.5.2-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", size = 659130, upload-time = "2026-06-17T18:07:26.354Z" },
{ url = "https://files.pythonhosted.org/packages/f2/12/f4aaad6d3d383233f700ab322568a4f29f2c701a4861d85f4811d99689b2/greenlet-3.5.2-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", size = 669724, upload-time = "2026-06-17T18:29:50.13Z" },
{ url = "https://files.pythonhosted.org/packages/53/e0/4ce3a046b51e53934eae93d7f9c13975a97285741e9e1fcadf8751314c37/greenlet-3.5.2-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f", size = 673494, upload-time = "2026-06-17T18:39:34.196Z" },
{ url = "https://files.pythonhosted.org/packages/91/2a/a089811fc31c6bf8742f40a4e73470d6d401cef18e4314eb20dc399b377c/greenlet-3.5.2-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", size = 668089, upload-time = "2026-06-17T17:39:33.808Z" },
{ url = "https://files.pythonhosted.org/packages/52/e0/9c18721e63445dce02ee67e4c81c0f281626604ff55ae6f7b7f4354d7129/greenlet-3.5.2-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3", size = 479721, upload-time = "2026-06-17T18:41:25.726Z" },
{ url = "https://files.pythonhosted.org/packages/0f/1c/2f47c7d5fcfa98a62b705bf9a0505d86f4563c0d81cab1f7159ff1e743b7/greenlet-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", size = 1625684, upload-time = "2026-06-17T18:22:17.664Z" },
{ url = "https://files.pythonhosted.org/packages/b9/bf/661dd24624f70b7b32972d7693d0344ecde10278f647d7b828baf739899c/greenlet-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", size = 1688043, upload-time = "2026-06-17T17:40:12.403Z" },
{ url = "https://files.pythonhosted.org/packages/60/49/d9bde1d15a21296b3b521fe083eb8aabd54ac05d15de9832918f3d639543/greenlet-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", size = 240531, upload-time = "2026-06-17T17:35:47.448Z" },
{ url = "https://files.pythonhosted.org/packages/7f/4d/86d7768bd53e9907de0333df215c2018cd01a593b3715cbd79aa82dd94b7/greenlet-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9", size = 239579, upload-time = "2026-06-17T17:39:39.954Z" },
{ url = "https://files.pythonhosted.org/packages/92/15/907be5e8900901039bae752fa9a31c03a3c1e064833f35a4e49449184581/greenlet-3.5.2-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", size = 296697, upload-time = "2026-06-17T17:37:15.887Z" },
{ url = "https://files.pythonhosted.org/packages/95/5c/08c57be575c3d6a3c023bbf22144a1c7dc6ed4d134527bb36ded4dbf04a8/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", size = 656710, upload-time = "2026-06-17T18:07:28.046Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d0/749f917bdc9fc90fceea4aa65fbf6556e617a50714d1496bdc8ad190bb36/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c", size = 662629, upload-time = "2026-06-17T18:29:51.728Z" },
{ url = "https://files.pythonhosted.org/packages/55/87/10776cd88df54d0f563e9e21e98363f2d6af94bedc553b1da0972fa87f80/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014", size = 663191, upload-time = "2026-06-17T18:39:35.639Z" },
{ url = "https://files.pythonhosted.org/packages/5a/a5/68cefae3a07f6d0093a490cf28ab604f14578f3e60205a2a2b2d5cd70af2/greenlet-3.5.2-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", size = 660147, upload-time = "2026-06-17T17:39:35.068Z" },
{ url = "https://files.pythonhosted.org/packages/02/aa/26ddf92826a99d87bfb8fdb8f3a262a6f16495a5d8e579737baa92fb4543/greenlet-3.5.2-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b", size = 498199, upload-time = "2026-06-17T18:41:27.464Z" },
{ url = "https://files.pythonhosted.org/packages/d2/6b/b9156d8397e4750220f54c7c5c34650f1e740a8d2f66eab9cfd1b7b53b69/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", size = 1621675, upload-time = "2026-06-17T18:22:18.873Z" },
{ url = "https://files.pythonhosted.org/packages/b0/e3/d3250f4fa01c211a93d04e34fded63187e648dbec17b9b1a14d388040593/greenlet-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", size = 1680577, upload-time = "2026-06-17T17:40:14.055Z" },
{ url = "https://files.pythonhosted.org/packages/55/ba/eaee8bda4419770d7096b5a009ebff0ab20a2a28cdd83c4b591bfdf36fa9/greenlet-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", size = 243482, upload-time = "2026-06-17T17:37:34.741Z" },
{ url = "https://files.pythonhosted.org/packages/37/45/f794a81c91e9942c61f9110bd1f9a38a0ea565eab57f8b08cd53d3131e48/greenlet-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f", size = 242062, upload-time = "2026-06-17T17:35:39.814Z" },
]
[[package]]
@ -1398,7 +1398,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.1.0"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@ -1407,9 +1407,9 @@ dependencies = [
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" },
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
@ -1727,32 +1727,32 @@ wheels = [
[[package]]
name = "ruff"
version = "0.15.17"
version = "0.15.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" }
sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" },
{ url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" },
{ url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" },
{ url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" },
{ url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" },
{ url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" },
{ url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" },
{ url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" },
{ url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" },
{ url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" },
{ url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" },
{ url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" },
{ url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" },
{ url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" },
{ url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
{ url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" },
{ url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" },
{ url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" },
{ url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" },
{ url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" },
{ url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" },
{ url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" },
{ url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" },
{ url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" },
{ url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" },
{ url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" },
{ url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" },
{ url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" },
{ url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" },
{ url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" },
{ url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" },
{ url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" },
]
[[package]]
name = "selenium"
version = "4.44.0"
version = "4.45.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@ -1762,9 +1762,9 @@ dependencies = [
{ name = "urllib3", extra = ["socks"] },
{ name = "websocket-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2d/4a/6d0a4f4a07e2a91511a51398203ee82bf6ce644a448aaa35c59b44aa9531/selenium-4.44.0.tar.gz", hash = "sha256:b03a831fcfcab9d912b4682f60718c48a04560d6c62f7496c16b7498c9a4427e", size = 993133, upload-time = "2026-05-12T22:48:19.246Z" }
sdist = { url = "https://files.pythonhosted.org/packages/86/48/486aa67320f27452e9f551b8608f1a59ce7091c8fe7ebc9f4eba274775d4/selenium-4.45.0.tar.gz", hash = "sha256:563f0c4102f112df1cda30d46ce6d177b2e4a7a3d4b0756902d5dc84d3a8a365", size = 1005503, upload-time = "2026-06-16T04:43:57.915Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/bc/885047e975e996cb317db31c4551caa915aafc6befea990f082c7233adc2/selenium-4.44.0-py3-none-any.whl", hash = "sha256:d01ea3e5ecad8149460a765f7cf5177194c21dcc0173093fc05427c289b1bf24", size = 9654291, upload-time = "2026-05-12T22:48:16.836Z" },
{ url = "https://files.pythonhosted.org/packages/e4/8a/6ff6beb9c7c6cc642f628df9328a8b6637f86602eff8d28e70b5d4e8bca7/selenium-4.45.0-py3-none-any.whl", hash = "sha256:1fd9d0dc08192b2f8100e264ed720f83b05d2dd3a7feff673df04e0c7580df4b", size = 9536616, upload-time = "2026-06-16T04:43:55.968Z" },
]
[[package]]
@ -1814,36 +1814,36 @@ wheels = [
[[package]]
name = "sqlalchemy"
version = "2.0.50"
version = "2.0.51"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" }
sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" },
{ url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" },
{ url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" },
{ url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" },
{ url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" },
{ url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" },
{ url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" },
{ url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" },
{ url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" },
{ url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" },
{ url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" },
{ url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" },
{ url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" },
{ url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" },
{ url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" },
{ url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" },
{ url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" },
{ url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" },
{ url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" },
{ url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" },
{ url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" },
{ url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" },
{ url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" },
{ url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" },
{ url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" },
{ url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" },
{ url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" },
{ url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" },
{ url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" },
{ url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" },
{ url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" },
{ url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" },
{ url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" },
{ url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" },
{ url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" },
{ url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" },
{ url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" },
{ url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" },
{ url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" },
{ url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" },
{ url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
]
[[package]]
@ -1909,14 +1909,14 @@ wheels = [
[[package]]
name = "tzlocal"
version = "5.3.1"
version = "5.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
sdist = { url = "https://files.pythonhosted.org/packages/48/55/15e2340963d2bfedcc6042da3911438fd336f8ae96b65bdbe3a29766da0c/tzlocal-5.4.3.tar.gz", hash = "sha256:3a8c9bc18cf47e1dcde252ea0e6a72a6cde320a400b6ac6db1f1f8cccd553c00", size = 30873, upload-time = "2026-06-17T04:17:41.764Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
{ url = "https://files.pythonhosted.org/packages/42/28/fc144409c71569e928585f8f3c629d80d1ca3ef40175e9222f01588f98c9/tzlocal-5.4.3-py3-none-any.whl", hash = "sha256:24ce97bb58e2a973f7640ec2553ab4e6f6d5a0d0d1aa9dc43bca21d89e1feb82", size = 18039, upload-time = "2026-06-17T04:17:40.027Z" },
]
[[package]]