improve the simple view flow and change settings render to work on both

This commit is contained in:
arabcoders 2025-09-25 17:32:28 +03:00
parent 4d16618cfb
commit 1fd7477a9c
15 changed files with 159 additions and 88 deletions

View file

@ -358,6 +358,10 @@ div.is-centered {
justify-content: center;
}
.is-overflow-visible {
overflow: visible !important
}
.modal-content-max {
width: calc(100% - 20px);
max-width: 1344px;

View file

@ -268,6 +268,7 @@ import { useStorage } from '@vueuse/core'
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
import type { AutoCompleteOptions } from '~/types/autocomplete';
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
import {useConfirm} from '~/composables/useConfirm'
const emitter = defineEmits<{
(e: 'cancel'): void
@ -410,7 +411,7 @@ const importItem = async (): Promise<void> => {
return
}
if ((form.filter || form.cli || Object.keys(form.extras).length > 0) && !(await box.confirm('Overwrite the current form fields?', true))) {
if ((form.filter || form.cli || Object.keys(form.extras).length > 0) && !(await box.confirm('Overwrite the current form fields?'))) {
return
}

View file

@ -450,6 +450,7 @@
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import type { StoreItem } from '~/types/store'
import {useConfirm} from '~/composables/useConfirm'
const emitter = defineEmits<{
(e: 'getInfo', url: string, preset: string, cli: string): void
@ -588,7 +589,7 @@ const deleteSelectedItems = async () => {
if (true === config.app.remove_files) {
msg += ' This will remove any associated files if they exists.'
}
if (false === (await box.confirm(msg, config.app.remove_files))) {
if (false === (await box.confirm(msg))) {
return
}
for (const key in selectedElms.value) {
@ -750,7 +751,7 @@ const removeItem = async (item: StoreItem) => {
if (item.status === 'finished' && config.app.remove_files) {
msg += ' This will remove any associated files if they exists.'
}
if (false === (await box.confirm(msg, Boolean(item.filename && config.app.remove_files)))) {
if (false === (await box.confirm(msg))) {
return false
}

View file

@ -308,6 +308,7 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import type { notification, notificationImport } from '~/types/notification'
import {useConfirm} from '~/composables/useConfirm'
const emitter = defineEmits(['cancel', 'submit'])
const toast = useNotification()
@ -412,7 +413,7 @@ const importItem = async () => {
}
if (form.name || form.request?.url) {
if (false === (await box.confirm('Overwrite the current form fields?', true))) {
if (false === (await box.confirm('Overwrite the current form fields?'))) {
return
}
}

View file

@ -313,6 +313,7 @@
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import type { StoreItem } from '~/types/store'
import {useConfirm} from '~/composables/useConfirm'
const emitter = defineEmits<{
(e: 'getInfo', url: string, preset: string, cli: string): void

View file

@ -12,6 +12,26 @@
<div class="columns is-multiline">
<div class="column is-6">
<div class="field">
<label class="label is-unselectable">Page View</label>
<div class="control">
<label for="ui_advanced" class="radio">
<input id="ui_advanced" type="radio" v-model="simpleMode" :value="false">
<span class="icon"><i class="fa-solid fa-computer" /></span>
Regular View
</label>
<label for="ui_simple" class="radio">
<input id="ui_simple" type="radio" v-model="simpleMode" :value="true">
<span class="icon"><i class="fa-solid fa-mobile-screen-button" /></span>
Simple View (Experimental)
</label>
</div>
<p class="help is-bold has-text-danger">
<span class="icon"> <i class="fa-solid fa-info-circle" /></span>
The simple view is experimental and will change without notice and maybe even deleted in the future.
</p>
</div>
<div class="field">
<label class="label is-unselectable">Color scheme</label>
<div class="control">
@ -35,7 +55,7 @@
<div class="field">
<label class="label is-unselectable">
Backgrounds
Show Background
<template v-if="bg_enable">
<NuxtLink @click="$emit('reload_bg')" class="is-bold">Reload</NuxtLink>
<span class="icon" v-if="isLoading"><i class="fa fa-spin fa-spinner" /></span>
@ -44,7 +64,7 @@
<div class="control">
<input id="random_bg" type="checkbox" class="switch is-success" v-model="bg_enable">
<label for="random_bg" class="is-unselectable">
&nbsp;{{ bg_enable ? 'Enabled' : 'Disabled' }}
&nbsp;{{ bg_enable ? 'Yes' : 'No' }}
</label>
</div>
</div>
@ -59,17 +79,7 @@
</div>
</div>
<div class="field">
<label class="label is-unselectable" for="show_thumbnail">Show Videos Thumbnail when possible</label>
<div class="control">
<input id="show_thumbnail" type="checkbox" class="switch is-success" v-model="show_thumbnail">
<label for="show_thumbnail" class="is-unselectable">
&nbsp;{{ show_thumbnail ? 'Enabled' : 'Disabled' }}
</label>
</div>
</div>
<div class="field">
<div class="field" v-if="!simpleMode">
<label class="label is-unselectable" for="show_thumbnail">URLs Separator</label>
<div class="control">
<div class="select is-fullwidth">
@ -85,27 +95,28 @@
</div>
<div class="column is-6">
<div class="field">
<label class="label is-unselectable" for="reduce_confirm">Reduce confirm box usage</label>
<label class="label is-unselectable" for="show_thumbnail">Show Thumbnails</label>
<div class="control">
<input id="reduce_confirm" type="checkbox" class="switch is-success" v-model="reduce_confirm">
<label for="reduce_confirm" class="is-unselectable">
&nbsp;{{ reduce_confirm ? 'Enabled' : 'Disabled' }}
<input id="show_thumbnail" type="checkbox" class="switch is-success" v-model="show_thumbnail">
<label for="show_thumbnail" class="is-unselectable">
&nbsp;{{ show_thumbnail ? 'Yes' : 'No' }}
</label>
</div>
<p class="help is-bold"> Show videos thumbnail if available.</p>
</div>
<div class="field">
<label class="label" for="allow_toasts">Show notifications toasts</label>
<label class="label" for="allow_toasts">Show notifications</label>
<div class="control">
<input id="allow_toasts" type="checkbox" class="switch is-success" v-model="allow_toasts">
<label for="allow_toasts" class="is-unselectable">
&nbsp;{{ allow_toasts ? 'Enabled' : 'Disabled' }}
&nbsp;{{ allow_toasts ? 'Yes' : 'No' }}
</label>
</div>
</div>
<div class="field" v-if="allow_toasts">
<label class="label">Toasts position</label>
<label class="label">Notification position</label>
<div class="control">
<div class="select is-fullwidth">
<select v-model="toast_position">
@ -121,12 +132,12 @@
</div>
<div class="field" v-if="allow_toasts">
<label class="label" for="dismiss_on_click">Dismiss toasts on click</label>
<label class="label" for="dismiss_on_click">Dismiss notification on click</label>
<div class="control">
<input id="dismiss_on_click" type="checkbox" class="switch is-success"
v-model="toast_dismiss_on_click">
<label for="dismiss_on_click" class="is-unselectable">
&nbsp;{{ toast_dismiss_on_click ? 'Enabled' : 'Disabled' }}
&nbsp;{{ toast_dismiss_on_click ? 'Yes' : 'No' }}
</label>
</div>
</div>
@ -142,6 +153,7 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import { POSITION } from 'vue-toastification'
import { useConfigStore } from '~/stores/ConfigStore'
defineProps<{ isLoading: boolean }>()
defineEmits<{ (e: 'reload_bg'): void }>()
@ -150,9 +162,9 @@ const bg_enable = useStorage<boolean>('random_bg', true)
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95)
const selectedTheme = useStorage<'auto' | 'light' | 'dark'>('theme', 'auto')
const allow_toasts = useStorage<boolean>('allow_toasts', true)
const reduce_confirm = useStorage<boolean>('reduce_confirm', false)
const toast_position = useStorage<POSITION>('toast_position', POSITION.TOP_RIGHT)
const toast_dismiss_on_click = useStorage<boolean>('toast_dismiss_on_click', true)
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',')
const simpleMode = useStorage<boolean>('simple_mode', useConfigStore().app.simple_mode || false)
</script>

View file

@ -3,8 +3,19 @@
<div class="form-container" :class="{ 'is-centered': shouldCenterForm }">
<section class="download-form box">
<form class="download-form__body" autocomplete="off" @submit.prevent="addDownload">
<label class="label" for="download-url">What you would like to download?</label>
<label class="label" for="download-url">
What you would like to download?
<span class="is-pulled-right">
<span class="icon has-text-primary is-pointer" @click="$emit('show_settings')" v-tooltip="'Settings'"><i
class="fas fa-cogs" /></span>
</span>
</label>
<div class="field has-addons">
<div class="control">
<button type="button" class="button is-info" @click="() => showPresets = !showPresets">
<span class="icon"><i class="fas" :class="showPresets ? 'fa-chevron-up' : 'fa-chevron-down'" /></span>
</button>
</div>
<div class="control is-expanded">
<input id="download-url" v-model="formUrl" :disabled="!socketStore.isConnected || addInProgress"
class="input" placeholder="https://..." type="url" required>
@ -12,11 +23,37 @@
<div class="control">
<button type="submit" class="button is-primary" :class="{ 'is-loading': addInProgress }"
:disabled="!socketStore.isConnected || addInProgress || !formUrl.trim()">
<span class="icon"><i class="fa-solid fa-plus" /></span>
<span class="icon"><i class="fas fa-plus" /></span>
<span>Add</span>
</button>
</div>
</div>
<div class="field has-addons" v-if="showPresets">
<div class="control">
<label class="button is-static">
<span class="icon"><i class="fas fa-sliders" /></span>
<span>Preset</span>
</label>
</div>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select id="preset" class="is-fullwidth" :disabled="!socketStore.isConnected || addInProgress"
v-model="formPreset.preset">
<optgroup label="Custom presets" v-if="presets.filter(p => !p?.default).length > 0">
<option v-for="cPreset in filter_presets(false)" :key="cPreset.name" :value="cPreset.name">
{{ cPreset.name }}
</option>
</optgroup>
<optgroup label="Default presets">
<option v-for="dPreset in filter_presets(true)" :key="dPreset.name" :value="dPreset.name">
{{ dPreset.name }}
</option>
</optgroup>
</select>
</div>
</div>
</div>
</form>
</section>
</div>
@ -26,12 +63,12 @@
<TransitionGroup name="queue-card" tag="div" class="columns is-multiline queue-card-columns">
<div v-for="entry in displayItems" :key="entry.item._id" class="column is-12-mobile is-6-tablet">
<article class="queue-card card" :class="{ 'is-history': 'history' === entry.source }">
<div v-if="'queue' === entry.source && shouldShowProgress(entry.item)"
class="progress-bar is-unselectable mb-3">
<div class="progress-percentage">{{ updateProgress(entry.item) }}</div>
<div class="progress" :style="{ width: getProgressWidth(entry.item) }"></div>
</div>
<div class="card-content">
<div v-if="'queue' === entry.source && shouldShowProgress(entry.item)"
class="progress-bar is-unselectable mb-3">
<div class="progress-percentage">{{ updateProgress(entry.item) }}</div>
<div class="progress" :style="{ width: getProgressWidth(entry.item) }"></div>
</div>
<article class="media">
<figure class="media-left">
<figure class="image is-16by9 queue-thumb" :class="{ 'is-clickable': isEmbedable(entry.item.url) }"
@ -40,7 +77,7 @@
<span
v-if="entry.item.filename && entry.item.status === 'finished' || isEmbedable(entry.item.url)"
class="queue-thumb__overlay">
<span class="icon has-text-white"><i class="fa-solid fa-play" /></span>
<span class="icon has-text-white"><i class="fas fa-play" /></span>
</span>
</figure>
</figure>
@ -48,7 +85,7 @@
<p class="title is-6 mb-0 queue-title">
<NuxtLink target="_blank" :href="entry.item.url">{{ entry.item.title }}</NuxtLink>
</p>
<div class="field is-grouped">
<div class="field is-grouped is-unselectable">
<div class="control">
<span class="tag is-size-7 has-text-weight-semibold is-uppercase"
:class="getSourceTagClass(entry)">
@ -56,32 +93,32 @@
</span>
</div>
<div class="control">
<span class="tag is-size-7 has-text-weight-semibold" :class="getStatusClass(entry.item)">{{
getStatusLabel(entry.item) }}</span>
<span class="tag is-size-7 has-text-weight-semibold" :class="getStatusClass(entry.item)">
{{ getStatusLabel(entry.item) }}
</span>
</div>
<div class="control">
<span class="tag" :date-datetime="entry.item.datetime" v-rtime="entry.item.datetime" />
</div>
</div>
<p v-if="getDescription(entry.item)" class="content is-size-7 has-text-grey queue-description">
{{ getDescription(entry.item) }}
<p class="content is-size-7 has-text-grey queue-description">
{{ getDescription(entry.item) || 'No description available.' }}
</p>
</div>
</article>
<div v-if="'queue' === entry.source" class="buttons are-small is-right is-flex-wrap-wrap mt-3">
<button v-if="canStart(entry.item)" class="button is-success is-light" type="button"
@click="startQueueItem(entry.item)">
<span class="icon"><i class="fa-solid fa-circle-play" /></span>
<span class="icon"><i class="fas fa-circle-play" /></span>
<span>Start</span>
</button>
<button v-if="canPause(entry.item)" class="button is-warning is-light" type="button"
@click="pauseQueueItem(entry.item)">
<span class="icon"><i class="fa-solid fa-pause" /></span>
<span class="icon"><i class="fas fa-pause" /></span>
<span>Pause</span>
</button>
<button class="button is-warning" type="button" @click="cancelDownload(entry.item)">
<span class="icon"><i class="fa-solid fa-xmark" /></span>
<span class="icon"><i class="fas fa-xmark" /></span>
<span>Cancel</span>
</button>
</div>
@ -89,16 +126,16 @@
<div v-else class="buttons are-small is-right is-flex-wrap-wrap mt-3">
<a v-if="getDownloadLink(entry.item)" class="button is-link" :href="getDownloadLink(entry.item)"
:download="getDownloadName(entry.item)">
<span class="icon"><i class="fa-solid fa-download" /></span>
<span class="icon"><i class="fas fa-download" /></span>
<span>Download</span>
</a>
<button v-if="entry.item.status != 'finished' || !entry.item.filename" class="button is-info is-light"
type="button" @click="requeueItem(entry.item)">
<span class="icon"><i class="fa-solid fa-rotate-right" /></span>
<span class="icon"><i class="fas fa-rotate-right" /></span>
<span>Requeue</span>
</button>
<button class="button is-danger" type="button" @click="deleteHistoryItem(entry.item)">
<span class="icon"><i class="fa-solid fa-trash" /></span>
<span class="icon"><i class="fas fa-trash" /></span>
<span>Delete</span>
</button>
</div>
@ -132,6 +169,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useStorage } from '@vueuse/core'
import type { item_request } from '~/types/item'
import type { ItemStatus, StoreItem } from '~/types/store'
import { useNotification } from '~/composables/useNotification'
@ -142,19 +180,24 @@ import { isEmbedable, getEmbedable } from '~/utils/embedable'
import EmbedPlayer from '~/components/EmbedPlayer.vue'
import { ag, encodePath, makeDownload, request, stripPath, ucFirst, uri } from '~/utils'
defineEmits<{ (e: 'show_settings'): void }>()
const configStore = useConfigStore()
const stateStore = useStateStore()
const socketStore = useSocketStore()
const toast = useNotification()
const { app, paused } = storeToRefs(configStore)
const { app, paused, presets } = storeToRefs(configStore)
const { queue, history } = storeToRefs(stateStore)
const embedUrl = ref<string>('')
const videoItem = ref<StoreItem | null>(null)
const formUrl = ref<string>('')
const formPreset = ref<{ preset: string }>({ preset: app.value.default_preset || '' })
const addInProgress = ref<boolean>(false)
const showPresets = ref<boolean>(false)
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const sortByNewest = (items: StoreItem[]): StoreItem[] => items.slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0))
@ -189,7 +232,7 @@ const addDownload = async (): Promise<void> => {
const payload: item_request[] = [{
url,
preset: app.value.default_preset,
preset: formPreset.value.preset || app.value.default_preset,
auto_start: true,
}]
@ -212,8 +255,8 @@ const addDownload = async (): Promise<void> => {
return
}
toast.success('Added to queue.')
formUrl.value = ''
formPreset.value.preset = app.value.default_preset || ''
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to add download.'
toast.error(message)
@ -223,6 +266,10 @@ const addDownload = async (): Promise<void> => {
}
const resolveThumbnail = (entry: DisplayEntry): string => {
if (!show_thumbnail.value) {
return '/images/placeholder.png'
}
const { item, source } = entry
const sidecarImage = item.sidecar?.image?.[0]?.file
@ -463,6 +510,8 @@ const deleteHistoryItem = (item: StoreItem): void => {
socketStore.emit('item_delete', { id: item._id, remove_file: app.value.remove_files })
toast.info('Removed from history queue.')
}
const filter_presets = (flag: boolean = true) => presets.value.filter(item => item.default === flag)
</script>
<style scoped>
@ -608,4 +657,8 @@ const deleteHistoryItem = (item: StoreItem): void => {
max-width: 100%;
}
}
.progress-bar {
border-radius: 0.75rem 0.75rem 0 0
}
</style>

View file

@ -307,6 +307,7 @@ import { CronExpressionParser } from 'cron-parser'
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
import type { AutoCompleteOptions } from '~/types/autocomplete'
import type { exported_task, task_item } from '~/types/tasks'
import {useConfirm} from '~/composables/useConfirm'
const props = defineProps<{
reference?: string | null | undefined
@ -408,7 +409,7 @@ const importItem = async (): Promise<void> => {
}
if (form.url || form.timer) {
if (false === (await box.confirm('Overwrite the current form fields?', true))) {
if (false === (await box.confirm('Overwrite the current form fields?'))) {
return
}
}

View file

@ -1,42 +1,29 @@
import { useStorage } from '@vueuse/core'
import { useDialog } from './useDialog'
import { useDialog, type ConfirmOptions, type AlertOptions, type PromptOptions } from './useDialog'
const dialog = useDialog()
const reduceConfirm = useStorage<boolean>('reduce_confirm', false)
const confirm = async (msg: string, force: boolean = false) => {
if (false === force && true === reduceConfirm.value) {
return true
}
const { status } = await dialog.confirmDialog({
const confirm = async (msg: string, opts: ConfirmOptions = {}) => {
const { status } = await dialog.confirmDialog(Object.assign({
title: 'Please Confirm',
message: msg,
cancelText: 'Cancel',
confirmText: 'OK',
})
} as ConfirmOptions, opts || {}))
return status
}
const alert = async (msg: string) => {
const { status } = await dialog.alertDialog({
const alert = async (msg: string, opts: AlertOptions = {}) => {
const { status } = await dialog.alertDialog(Object.assign({
title: 'Alert',
message: msg,
confirmText: 'OK',
})
} as AlertOptions, opts || {}))
return status
}
const prompt = async (msg: string, defaultValue: string = '') => {
const { status, value } = await dialog.promptDialog({
title: 'Input Required',
message: msg,
initial: defaultValue,
cancelText: 'Cancel',
confirmText: 'OK',
})
const prompt = async (msg: string, opts: PromptOptions = {}) => {
const { status, value } = await dialog.promptDialog(Object.assign({ message: msg } as PromptOptions, opts || {}))
if (status) {
return value
@ -45,6 +32,4 @@ const prompt = async (msg: string, defaultValue: string = '') => {
return null
}
export default function useConfirm() {
return { confirm, alert, prompt, reduceConfirm }
}
export const useConfirm = () => ({ confirm, alert, prompt })

View file

@ -1,8 +1,16 @@
<template>
<template v-if="config.app.simple_mode">
<Simple />
<template v-if="simpleMode">
<Simple @show_settings="() => show_settings = true" />
</template>
<template v-else>
<template v-if="show_settings">
<Modal @close="show_settings = false" content-class="modal-content-max is-overflow-visible">
<Settings v-if="show_settings" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
</Modal>
</template>
<template v-if="!simpleMode">
<Shutdown v-if="app_shutdown" />
<div id="main_container" class="container" v-else>
<NewVersion v-if="newVersionIsAvailable" />
@ -124,11 +132,9 @@
</div>
</div>
</nav>
<div>
<Settings v-if="show_settings" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
<NuxtLoadingIndicator />
<NuxtPage v-if="!config.app.simple_mode && config.is_loaded" />
<NuxtPage v-if="config.is_loaded" />
<Message v-if="!config.is_loaded" class="has-background-info-90 has-text-dark mt-5"
title="Loading Configuration" icon="fas fa-spinner fa-spin">
<p>Loading application configuration. This usually takes less than a second.</p>
@ -197,6 +203,8 @@ const bg_opacity = useStorage('random_bg_opacity', 0.95)
const showMenu = ref(false)
const isMobile = useMediaQuery({ maxWidth: 1024 })
const app_shutdown = ref<boolean>(false)
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false)
const doc = ref<{ file: string }>({ file: '' })
const applyPreferredColorScheme = (scheme: string) => {

View file

@ -129,6 +129,7 @@
<script setup lang="ts">
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
import {useConfirm} from '~/composables/useConfirm'
type ConditionItemWithUI = ConditionItem & { raw?: boolean }
@ -226,7 +227,7 @@ const updateItems = async (newItems: ConditionItem[]): Promise<boolean> => {
}
const deleteItem = async (cond: ConditionItem): Promise<void> => {
if (true !== (await box.confirm(`Delete '${cond.name}'?`, true))) {
if (true !== (await box.confirm(`Delete '${cond.name}'?`))) {
return
}

View file

@ -219,6 +219,7 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import type { notification, notificationImport } from '~/types/notification'
import {useConfirm} from '~/composables/useConfirm'
const toast = useNotification()
const socket = useSocketStore()

View file

@ -204,6 +204,7 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
import type { Preset } from '~/types/presets'
import {useConfirm} from '~/composables/useConfirm'
const toast = useNotification()
const config = useConfigStore()
@ -295,7 +296,7 @@ const updatePresets = async (items: Preset[]): Promise<boolean | undefined> => {
}
const deleteItem = async (item: Preset) => {
if (true !== (await box.confirm(`Delete preset '${item.name}'?`, true))) {
if (true !== (await box.confirm(`Delete preset '${item.name}'?`))) {
return
}

View file

@ -402,6 +402,7 @@ import moment from 'moment'
import { useStorage } from '@vueuse/core'
import { CronExpressionParser } from 'cron-parser'
import Modal from '~/components/Modal.vue'
import { useConfirm } from '~/composables/useConfirm'
import TaskInspect from '~/components/TaskInspect.vue'
import type { task_item, exported_task, error_response } from '~/types/tasks'
@ -595,7 +596,7 @@ const deleteSelected = async () => {
}
const deleteItem = async (item: task_item) => {
if (true !== (await box.confirm(`Delete '${item.name}' task?`, true))) {
if (true !== (await box.confirm(`Delete '${item.name}' task?`))) {
return
}

View file

@ -875,15 +875,15 @@ wheels = [
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2025.8"
version = "2025.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/d6/e5b378b7d4add8c879295c531309b0320e9c07a70458665d091760ffdc87/pyinstaller_hooks_contrib-2025.8.tar.gz", hash = "sha256:3402ad41dfe9b5110af134422e37fc5d421ba342c6cb980bd67cb30b7415641c", size = 164214 }
sdist = { url = "https://files.pythonhosted.org/packages/7d/83/be0f57c0b77b66c33c2283ebd4ea341022b5a743e97c5fb3bebab82b38b9/pyinstaller_hooks_contrib-2025.9.tar.gz", hash = "sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6", size = 165189 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/34/1d973d0dae849683e53fbcda84443ce016f315e6f4dc7605ede4f56a28c3/pyinstaller_hooks_contrib-2025.8-py3-none-any.whl", hash = "sha256:8d0b8cfa0cb689a619294ae200497374234bd4e3994b3ace2a4442274c899064", size = 442346 },
{ url = "https://files.pythonhosted.org/packages/a2/26/23b4cfc77d7f808c69f59070e1e8293a579ec281a547c61562357160b346/pyinstaller_hooks_contrib-2025.9-py3-none-any.whl", hash = "sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038", size = 444283 },
]
[[package]]