itube-specs 0.0.746 → 0.0.747

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/components/grids/s-grid-videos.vue +7 -9
  2. package/components/page-components/s-filter-popup.vue +134 -155
  3. package/components/page-components/s-filter-slider.vue +43 -16
  4. package/components/page-components/s-filter.vue +290 -12
  5. package/components/page-components/s-info-grid.vue +38 -15
  6. package/components/page-components/s-like.vue +5 -1
  7. package/components/page-components/s-model-filters.vue +177 -42
  8. package/components/page-components/s-pagination.vue +27 -21
  9. package/components/page-components/s-section-title.vue +4 -4
  10. package/components/ui/s-chips.vue +7 -0
  11. package/components/ui/s-link.vue +13 -1
  12. package/components/ui/s-select.vue +6 -1
  13. package/components/ui/s-slider.vue +10 -0
  14. package/components/ui/s-timestamp.vue +17 -17
  15. package/composables/use-fetch-channels-by-model.ts +1 -11
  16. package/composables/use-fetch-channels-by-network-name.ts +6 -7
  17. package/composables/use-fetch-channels.ts +5 -6
  18. package/composables/use-fetch-models-by-letter.ts +4 -5
  19. package/composables/use-fetch-models-by-phrases.ts +3 -4
  20. package/composables/use-fetch-models.ts +3 -4
  21. package/composables/use-fetch-playlists-by-niche.ts +4 -5
  22. package/composables/use-fetch-playlists-get-videos-by-id.ts +5 -6
  23. package/composables/use-fetch-related-videos.ts +4 -5
  24. package/composables/use-fetch-videos-by-categories.ts +4 -5
  25. package/composables/use-fetch-videos-by-channel.ts +4 -5
  26. package/composables/use-fetch-videos-by-model-and-channel.ts +5 -6
  27. package/composables/use-fetch-videos-by-model-and-tag.ts +5 -6
  28. package/composables/use-fetch-videos-by-model.ts +4 -5
  29. package/composables/use-fetch-videos-by-tag.ts +4 -5
  30. package/composables/use-fetch-videos-search-by-niche.ts +4 -5
  31. package/composables/use-fetch-videos.ts +5 -6
  32. package/package.json +1 -1
@@ -5,7 +5,7 @@
5
5
  {'--first-hidden': hideFirstRow},
6
6
  ]"
7
7
  >
8
- <slot name="grid-start" />
8
+ <slot name="grid-start"/>
9
9
  <FVideoCard
10
10
  v-for="(item, index) in eagerItems"
11
11
  :key="`video-${item.guid}`"
@@ -18,7 +18,7 @@
18
18
  :playlist="playlist"
19
19
  />
20
20
  <slot/>
21
- <NuxtLazyHydrate when-idle v-if="idleItems.length > 0">
21
+ <NuxtLazyHydrate when-idle v-if="idleItems.length > 0" :key="`idle-${route.fullPath}`">
22
22
  <div class="s-grid-videos__idle">
23
23
  <FVideoCard
24
24
  v-for="item in idleItems"
@@ -32,7 +32,7 @@
32
32
  />
33
33
  </div>
34
34
  </NuxtLazyHydrate>
35
- <NuxtLazyHydrate when-visible v-if="lazyItems.length > 0">
35
+ <NuxtLazyHydrate when-visible v-if="lazyItems.length > 0" :key="`lazy-${route.fullPath}`">
36
36
  <div class="s-grid-videos__lazy">
37
37
  <FVideoCard
38
38
  v-for="item in lazyItems"
@@ -46,13 +46,12 @@
46
46
  />
47
47
  </div>
48
48
  </NuxtLazyHydrate>
49
- <slot name="grid-end" />
49
+ <slot name="grid-end"/>
50
50
  </div>
51
51
  </template>
52
52
 
53
53
  <script setup lang="ts">
54
54
  import type { IVideoCard } from '../../types';
55
- import { isMobileDevice } from '../../runtime';
56
55
 
57
56
  const IDLE_COUNT = 4;
58
57
 
@@ -64,11 +63,10 @@ const props = defineProps<{
64
63
  playlist?: boolean
65
64
  playlistId?: string
66
65
  topChips?: string[]
67
- }>()
66
+ }>();
68
67
 
69
- const isMobile = useState('isMobile', () =>
70
- isMobileDevice(useRequestHeaders()['user-agent'] ?? '')
71
- );
68
+ const route = useRoute();
69
+ const isMobile = useState<boolean>('isMobile');
72
70
  const eagerCount = computed(() => isMobile.value ? 4 : 12);
73
71
 
74
72
  const eagerItems = computed(() => props.items.slice(0, eagerCount.value));
@@ -1,176 +1,155 @@
1
1
  <template>
2
2
  <SPopup
3
- v-if="isOpen"
4
- v-model="isOpen"
3
+ v-if="model"
4
+ v-model="model"
5
5
  drawer
6
- @close="closePopup"
6
+ class="s-filter-popup"
7
7
  >
8
- <template #title>{{ filterTitle }}</template>
9
- <div class="s-filter-popup__filters">
10
- <div class="s-filter-popup__main-filters">
11
- <SSelect
12
- v-model="durationValue"
13
- name="duration"
14
- :items="translateTitle(durationItems)"
15
- :placeholder="t('duration')"
16
- icon="time"
17
- />
18
- <SSelect
19
- v-model="addedValue"
20
- name="added"
21
- :items="translateTitle(addedItems)"
22
- :placeholder="t('added')"
23
- icon="date"
24
- />
8
+ <template #title>
9
+ <div class="s-filter-popup__title-wrapper">
10
+ {{ title ?? t('filter') }}
11
+ <span
12
+ v-if="activeCount"
13
+ class="s-filter-popup__active"
14
+ >{{ activeCount }} active</span>
25
15
  </div>
26
- <template v-if="scheme.length">
27
- <SSelect
28
- v-for="(item, index) in scheme"
29
- :key="`scheme-${index}`"
30
- v-model="filterValue[item.name.toLowerCase()]"
31
- :name="item.title"
32
- :items="item.items"
33
- :label="item?.label"
34
- :label-icon="{
35
- icon: item.icon,
36
- prefix: 'categories'
37
- }"
38
- :placeholder="item.placeholder"
39
- />
40
- </template>
16
+ </template>
17
+
18
+ <div
19
+ class="s-filter-popup__content"
20
+ :class="{ '_loading': loading }"
21
+ >
22
+ <div
23
+ v-if="activeChips && activeChips.length > 0"
24
+ class="s-filter-popup__active-filters"
25
+ >
26
+ <span class="s-filter-popup__active-filters-title">{{ t('filter_popup.active_title') }}</span>
27
+ <div class="s-filter-popup__active-chips">
28
+ <SChips
29
+ v-for="(chip, index) in activeChips"
30
+ :key="`s-filter-popup-active-${index}`"
31
+ class="s-filter-popup__chips"
32
+ with-close
33
+ :index="`s-filter-popup-chips-${index}`"
34
+ :item="chip"
35
+ @click="$emit('chip-click', chip)"
36
+ />
37
+ </div>
38
+ </div>
39
+
41
40
  <div
42
- v-else
43
- class="s-filter-popup__loading _loading"
44
- ></div>
41
+ v-if="quick && quick.length > 0"
42
+ class="s-filter-popup__quick"
43
+ >
44
+ <span class="s-filter-popup__quick-title">{{ t('filter_popup.quick_title') }}</span>
45
+ <div class="s-filter-popup__grid">
46
+ <template v-for="(item, index) in quick" :key="`quick-${index}`">
47
+ <slot
48
+ name="quick-field"
49
+ :item="item"
50
+ :index="index"
51
+ >
52
+ <SSelect
53
+ class="s-filter-popup__select"
54
+ :name="(item as any).name"
55
+ :model-value="(item as any).value"
56
+ :items="(item as any).items"
57
+ size="s"
58
+ :is-first-placeholder="modelsPage"
59
+ :active="(item as any).active"
60
+ :label="(item as any).label ?? (item as any).title"
61
+ :label-icon="(item as any).icon ? { icon: (item as any).icon, prefix: (item as any).iconPrefix ?? (modelsPage ? 'models' : 'categories') } : undefined"
62
+ :placeholder="(item as any).placeholder"
63
+ @update:model-value="val => $emit('field-update', { name: (item as any).name, value: val })"
64
+ />
65
+ </slot>
66
+ </template>
67
+ </div>
68
+ </div>
69
+
70
+ <details
71
+ v-for="(group, groupIndex) in groups"
72
+ :key="`group-${groupIndex}`"
73
+ class="s-filter-popup__group"
74
+ >
75
+ <summary class="s-filter-popup__summary">
76
+ <SIcon
77
+ name="angle-right"
78
+ size="16"
79
+ class="s-filter-popup__summary-arrow"
80
+ />
81
+ <span class="s-filter-popup__summary-title">{{ group.title }}</span>
82
+ <span class="s-filter-popup__summary-count">{{ t('filter_popup.fields', { count: group.count ?? group.fields.length }) }}</span>
83
+ <span
84
+ v-if="group.description"
85
+ class="s-filter-popup__summary-description"
86
+ >{{ group.description }}</span>
87
+ </summary>
88
+ <div class="s-filter-popup__group-content">
89
+ <template
90
+ v-for="(field, fieldIndex) in group.fields"
91
+ :key="`field-${groupIndex}-${fieldIndex}`"
92
+ >
93
+ <slot
94
+ name="field"
95
+ :field="field"
96
+ :group="group"
97
+ :index="fieldIndex"
98
+ >
99
+ <SSelect
100
+ class="s-filter-popup__select"
101
+ :name="(field as any).name"
102
+ :model-value="(field as any).value"
103
+ :items="(field as any).items"
104
+ size="s"
105
+ :active="(field as any).active"
106
+ :label="(field as any).label ?? (field as any).title"
107
+ :label-icon="(field as any).icon ? { icon: (field as any).icon, prefix: (field as any).iconPrefix ?? (modelsPage ? 'models' : 'categories') } : undefined"
108
+ :placeholder="(field as any).placeholder"
109
+ @update:model-value="val => $emit('field-update', { name: (field as any).name, value: val })"
110
+ />
111
+ </slot>
112
+ </template>
113
+ </div>
114
+ </details>
45
115
  </div>
46
116
 
47
117
  <template #footer>
48
- <div class="s-filter-popup__buttons">
49
- <SButton
50
- theme="secondary"
51
- wide
52
- size="l"
53
- :disabled="!isFiltered"
54
- @click="reset"
55
- >{{ t('reset_all') }}
56
- </SButton>
57
- <SButton
58
- wide
59
- size="l"
60
- theme="primary"
61
- @click="saveResult"
62
- >{{ t('apply') }}
63
- </SButton>
64
- </div>
118
+ <slot name="footer"/>
65
119
  </template>
66
120
  </SPopup>
67
121
  </template>
68
122
 
69
- <script setup lang="ts">
70
- import type { IFilterScheme, ISelectItem } from '../../types';
123
+ <script setup lang="ts" generic="TField">
124
+ import type { IChipsItem } from '../../types';
71
125
 
72
- const { t } = useI18n();
126
+ export interface FilterGroup<T> {
127
+ key: string
128
+ title: string
129
+ description?: string
130
+ count?: number
131
+ fields: T[]
132
+ }
73
133
 
74
- const props = defineProps<{
75
- addedItems: ISelectItem[]
76
- durationItems: ISelectItem[]
77
- modelValue: boolean
78
- scheme: IFilterScheme[]
79
- count: number
134
+ defineProps<{
135
+ title?: string
136
+ activeCount?: number
137
+ activeChips?: IChipsItem[]
138
+ quick?: TField[]
139
+ groups?: FilterGroup<TField>[]
140
+ loading?: boolean
141
+ modelsPage?: boolean
80
142
  }>();
81
143
 
82
- const durationValue = ref('');
83
- const addedValue = ref('');
84
- const filterValue = ref({} as Record<string, string>);
85
-
86
- const route = useRoute();
87
-
88
- const categoriesFromQuery = route.query.categories
89
- ? (typeof route.query.categories === 'string'
90
- ? route.query.categories.split(',')
91
- : route.query.categories.flatMap(cat => cat.split(',')))
92
- : [];
93
-
94
- categoriesFromQuery.forEach(str => {
95
- const [key, ...rest] = str.split('_');
96
- if (key && rest.length > 0) {
97
- filterValue.value[key] = rest.join('_');
98
- }
99
- });
100
-
101
- durationValue.value = route.query['duration'] as string || '';
102
- addedValue.value = route.query['added'] as string || '';
103
-
104
- const emit = defineEmits<{
105
- (eventName: 'update:modelValue', value: boolean): void
144
+ defineEmits<{
145
+ (e: 'chip-click', item: IChipsItem): void
146
+ (e: 'field-update', payload: { name: string, value: any }): void
106
147
  }>();
107
148
 
108
- const isOpen = ref(props.modelValue);
109
-
110
- function closePopup() {
111
- isOpen.value = false;
112
- emit('update:modelValue', isOpen.value);
113
- }
149
+ const model = defineModel<boolean>();
114
150
 
115
- const router = useRouter();
116
-
117
- function saveResult() {
118
- const oldQuery = { ...route.query };
119
- delete oldQuery['page'];
120
-
121
- const mainFiltersValue = {
122
- ...(durationValue.value ? { duration: durationValue.value } : {}),
123
- ...(addedValue.value ? { added: addedValue.value } : {}),
124
- };
125
-
126
- const preserved = {
127
- ...mainFiltersValue,
128
- ...(oldQuery.sort ? { sort: oldQuery.sort } : {}),
129
- ...(oldQuery.page ? { page: oldQuery.page } : {}),
130
- };
131
-
132
- const categories = Object.entries(filterValue.value)
133
- .filter(([, value]) => typeof value === 'string' && value.length > 0)
134
- .map(([key, value]) => `${key}_${value}`);
135
-
136
- const categoriesString = categories.join(',');
137
-
138
- router.push({
139
- query: {
140
- ...preserved,
141
- categories: categories.length ? categoriesString : undefined,
142
- },
143
- });
144
-
145
- closePopup();
146
- }
147
-
148
- function reset() {
149
- durationValue.value = '';
150
- addedValue.value = '';
151
- filterValue.value = {};
152
- router.push({
153
- query: {
154
- categories: null,
155
- },
156
- });
157
- }
158
-
159
- const filterTitle = computed(() => {
160
- const hasDuration = durationValue.value ? 1 : 0;
161
- const hasAdded = addedValue.value ? 1 : 0;
162
- const count = Object.keys(filterValue.value).length + hasAdded + hasDuration;
163
- const filterText = count > 1 ? t('filters') : t('filter');
164
- return count > 0 ? `${filterText}: ${count}` : t('filter');
165
- });
166
-
167
- const isFiltered = computed(() => !!route.query['categories'] || Object.keys(filterValue.value).length > 0 || durationValue.value || addedValue.value);
168
-
169
- function translateTitle(array: ISelectItem[]) {
170
- return array.map((item: ISelectItem) => ({
171
- title: t(item.title),
172
- key: item.key,
173
- value: item.value,
174
- }));
175
- }
151
+ const { t } = useI18n();
176
152
  </script>
153
+
154
+ <style scoped lang="scss">
155
+ </style>
@@ -11,27 +11,54 @@
11
11
  maxRangeValue(item)
12
12
  ]"
13
13
  :title="item.title"
14
+ :icon="icon"
15
+ :icon-prefix="iconPrefix"
14
16
  :active="isActive(item.name)"
15
- :format="formatterSlider(item.options)"
17
+ :format="formatterSlider()"
16
18
  :between="betweenText(item.options, getRangeInfo(item.options)?.min, getRangeInfo(item.options)?.max)"
17
19
  @update:model-value="val => updateRangeFilter(item, val)"
18
- />
20
+ >
21
+ <template
22
+ v-for="(_, name) in $slots"
23
+ #[name]="slotData"
24
+ >
25
+ <slot :name="name" v-bind="slotData"/>
26
+ </template>
27
+ </SSlider>
19
28
  </ClientOnly>
20
29
  </template>
21
30
 
22
31
  <script setup lang="ts">
23
32
  import type { IModelFilter, IModelFilterOptions } from '../../types';
33
+ import type { LocationQuery } from '#vue-router';
24
34
  import { useRoute, useRouter } from '#vue-router';
25
35
 
26
- defineProps<{
36
+ const props = defineProps<{
27
37
  item: IModelFilter
28
38
  index: number
29
39
  groupName: string
30
- }>()
40
+ query?: LocationQuery
41
+ icon?: string
42
+ iconPrefix?: string
43
+ }>();
44
+
45
+ const emit = defineEmits<{
46
+ (e: 'update:query', value: LocationQuery): void
47
+ }>();
31
48
 
32
49
  const route = useRoute();
33
50
  const router = useRouter();
34
51
 
52
+ const effectiveQuery = computed<LocationQuery>(() => props.query ?? route.query);
53
+
54
+ function commitQuery(newQuery: LocationQuery) {
55
+ if (props.query !== undefined) {
56
+ emit('update:query', newQuery);
57
+ } else {
58
+ router.replace({ query: newQuery });
59
+ }
60
+ }
61
+
35
62
  function getRangeInfo(options: { name: string; quantity: number; title: string }[]): {
36
63
  min: number,
37
64
  step: number,
@@ -46,8 +73,8 @@ function getRangeInfo(options: { name: string; quantity: number; title: string }
46
73
  return null;
47
74
  }
48
75
 
49
- const min = numericValues[ 0 ] || 0;
50
- const max = numericValues[ numericValues.length - 1 ] || 100;
76
+ const min = numericValues[0] || 0;
77
+ const max = numericValues[numericValues.length - 1] || 100;
51
78
  const step = numericValues.length > 1 ? 1 : 0;
52
79
 
53
80
  return { min, step, max };
@@ -58,22 +85,22 @@ function updateRangeFilter(item: IModelFilter, val: string[] | number[]) {
58
85
  const rangeInfo = getRangeInfo(item.options);
59
86
  if (!rangeInfo) return;
60
87
  const [from, to] = val;
61
- const query = { ...route.query };
88
+ const query = { ...effectiveQuery.value };
62
89
 
63
- query[ `filter_${item.name}_from` ] = from;
64
- query[ `filter_${item.name}_to` ] = to;
90
+ query[`filter_${item.name}_from`] = String(from);
91
+ query[`filter_${item.name}_to`] = String(to);
65
92
 
66
93
  if (from === rangeInfo.min && to === rangeInfo.max) {
67
- delete query[ `filter_${item.name}_from` ];
68
- delete query[ `filter_${item.name}_to` ];
94
+ delete query[`filter_${item.name}_from`];
95
+ delete query[`filter_${item.name}_to`];
69
96
  }
70
97
 
71
- router.replace({ query });
98
+ commitQuery(query);
72
99
  });
73
100
  }
74
101
 
75
102
  function getRangeValue(name: string, defaultValue: number | undefined) {
76
- const value = route.query[ `filter_${name}` ];
103
+ const value = effectiveQuery.value[`filter_${name}`];
77
104
  if (value !== undefined) {
78
105
  return Number(value);
79
106
  } else {
@@ -92,7 +119,7 @@ function maxRangeValue(item: IModelFilter) {
92
119
  }
93
120
 
94
121
  function isSliderHasTitle(options: IModelFilterOptions[] | undefined): boolean | undefined {
95
- return options && options[ 0 ] && isNaN(Number(options[ 0 ].title));
122
+ return options && options[0] && isNaN(Number(options[0].title));
96
123
  }
97
124
 
98
125
  function formatterSlider() {
@@ -103,14 +130,14 @@ function betweenText(options: IModelFilterOptions[], min: number | undefined, ma
103
130
  if (isSliderHasTitle(options)) {
104
131
  const getText = (number: number | undefined): string | undefined => {
105
132
  return options.find((item: IModelFilterOptions) => Number(item.name) === number)?.title;
106
- }
133
+ };
107
134
  return [getText(min) || '', getText(max) || ''];
108
135
  }
109
136
  }
110
137
 
111
138
  function isActive(name: string) {
112
139
  const baseName = name.replace(/_(from|to)$/, '');
113
- return Object.keys(route.query).some(key => key.startsWith(`filter_${baseName}`));
140
+ return Object.keys(effectiveQuery.value).some(key => key.startsWith(`filter_${baseName}`));
114
141
  }
115
142
  </script>
116
143