itube-specs 0.0.893 → 0.0.896
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.
- package/assets/scss/themes/dark/_theme.scss +16 -12
- package/assets/scss/themes/light/_theme.scss +16 -12
- package/components/card/video/index.vue +17 -2
- package/components/library/feed.vue +340 -0
- package/components/library/follows.vue +147 -0
- package/components/ui/expand-row.vue +21 -2
- package/package.json +1 -1
- package/pages/library/categories.vue +61 -0
- package/pages/library/channels.vue +62 -0
- package/pages/library/index.vue +2 -2
- package/pages/library/models.vue +25 -130
- package/stubs/library-follows.ts +135 -0
- package/types/index.d.ts +1 -0
- package/types/library-feed.d.ts +34 -0
- package/components/library/model.vue +0 -307
- package/stubs/library-models.ts +0 -75
|
@@ -964,18 +964,22 @@ $library-item-badge-text: $color-white;
|
|
|
964
964
|
$library-item-badge-bordered-border: $color-zinc-600;
|
|
965
965
|
$library-item-badge-bordered-text: $color-zinc-400;
|
|
966
966
|
|
|
967
|
-
// ─── Library
|
|
968
|
-
$library-
|
|
969
|
-
$library-
|
|
970
|
-
$library-
|
|
971
|
-
$library-
|
|
972
|
-
$library-
|
|
973
|
-
$library-
|
|
974
|
-
$library-
|
|
975
|
-
$library-
|
|
976
|
-
$library-
|
|
977
|
-
$library-
|
|
978
|
-
$library-
|
|
967
|
+
// ─── Library feed (блок подписки: модель / канал / категория) ───
|
|
968
|
+
$library-feed-bg: $color-zinc-900-40;
|
|
969
|
+
$library-feed-border: $color-zinc-800;
|
|
970
|
+
$library-feed-fade: $color-zinc-950;
|
|
971
|
+
$library-feed-avatar-border: $color-zinc-700;
|
|
972
|
+
$library-feed-avatar-border--hover: $color-zinc-500;
|
|
973
|
+
$library-feed-initials: $color-zinc-400;
|
|
974
|
+
$library-feed-initials-gradient-from: $color-zinc-700;
|
|
975
|
+
$library-feed-initials-gradient-to: $color-zinc-800;
|
|
976
|
+
$library-feed-name: $color-white;
|
|
977
|
+
$library-feed-name--hover: $color-indigo-400;
|
|
978
|
+
$library-feed-badge-bg: $color-indigo-600;
|
|
979
|
+
$library-feed-badge-text: $color-white;
|
|
980
|
+
$library-feed-meta: $color-zinc-500;
|
|
981
|
+
$library-feed-meta-dot: $color-zinc-600;
|
|
982
|
+
$library-feed-title: $color-zinc-500;
|
|
979
983
|
|
|
980
984
|
// ─── Page: library ───
|
|
981
985
|
$page-library-heading: $color-white;
|
|
@@ -994,18 +994,22 @@ $library-item-badge-text: $color-white;
|
|
|
994
994
|
$library-item-badge-bordered-border: $color-gray-300;
|
|
995
995
|
$library-item-badge-bordered-text: $color-gray-600;
|
|
996
996
|
|
|
997
|
-
// ─── Library
|
|
998
|
-
$library-
|
|
999
|
-
$library-
|
|
1000
|
-
$library-
|
|
1001
|
-
$library-
|
|
1002
|
-
$library-
|
|
1003
|
-
$library-
|
|
1004
|
-
$library-
|
|
1005
|
-
$library-
|
|
1006
|
-
$library-
|
|
1007
|
-
$library-
|
|
1008
|
-
$library-
|
|
997
|
+
// ─── Library feed (блок подписки: модель / канал / категория) ───
|
|
998
|
+
$library-feed-bg: $color-gray-100;
|
|
999
|
+
$library-feed-border: transparent;
|
|
1000
|
+
$library-feed-fade: $color-gray-100;
|
|
1001
|
+
$library-feed-avatar-border: $color-gray-300;
|
|
1002
|
+
$library-feed-avatar-border--hover: $color-indigo-500-50;
|
|
1003
|
+
$library-feed-initials: $color-gray-600;
|
|
1004
|
+
$library-feed-initials-gradient-from: $color-gray-200;
|
|
1005
|
+
$library-feed-initials-gradient-to: $color-gray-300;
|
|
1006
|
+
$library-feed-name: $color-gray-900;
|
|
1007
|
+
$library-feed-name--hover: $color-indigo-600;
|
|
1008
|
+
$library-feed-badge-bg: $color-indigo-600;
|
|
1009
|
+
$library-feed-badge-text: $color-white;
|
|
1010
|
+
$library-feed-meta: $color-gray-500;
|
|
1011
|
+
$library-feed-meta-dot: $color-gray-400;
|
|
1012
|
+
$library-feed-title: $color-gray-500;
|
|
1009
1013
|
|
|
1010
1014
|
// ─── Page: library ───
|
|
1011
1015
|
$page-library-heading: $color-gray-900;
|
|
@@ -150,6 +150,8 @@ const { generateLink } = useGenerateLink();
|
|
|
150
150
|
|
|
151
151
|
const pageName = useGetPureRouteName();
|
|
152
152
|
const isChannelPage = computed(() => pageName === 'channels-channel');
|
|
153
|
+
const isModelPage = computed(() => pageName.startsWith('models-slug'));
|
|
154
|
+
const isSearchPage = computed(() => pageName.startsWith('search-slug'));
|
|
153
155
|
|
|
154
156
|
const previewId = useId();
|
|
155
157
|
const { activePreviewId, startPreviewByHover, stopPreviewByHover, startPreviewByTouch } = useVideoPreview();
|
|
@@ -193,10 +195,23 @@ const channelLink = computed(() => props.card.channelName
|
|
|
193
195
|
const slug = useSlug();
|
|
194
196
|
|
|
195
197
|
const cardTags = computed(() => {
|
|
196
|
-
const
|
|
198
|
+
const query = slug.value.toLowerCase();
|
|
199
|
+
|
|
200
|
+
const models = props.card.models
|
|
201
|
+
? [ ...props.card.models ].filter(item => !isModelPage.value || item.toLowerCase() !== query)
|
|
202
|
+
: [];
|
|
203
|
+
|
|
204
|
+
const searchRank = (item: string) => {
|
|
205
|
+
if (!isSearchPage.value || !query) return 0;
|
|
206
|
+
const name = item.toLowerCase();
|
|
207
|
+
if (name === query) return 0;
|
|
208
|
+
return name.includes(query) ? 1 : 2;
|
|
209
|
+
};
|
|
210
|
+
|
|
197
211
|
const sortModels = models.sort((a, b) =>
|
|
198
|
-
a.localeCompare(b)
|
|
212
|
+
searchRank(a) - searchRank(b) || a.localeCompare(b)
|
|
199
213
|
);
|
|
214
|
+
|
|
200
215
|
return sortModels?.map(item => convertString().toChips(item, 'models'));
|
|
201
216
|
});
|
|
202
217
|
</script>
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
<!-- Блок подписки (модель / канал / категория): шапка с аватаром, тегами и счётчиками + лента роликов -->
|
|
2
|
+
<template>
|
|
3
|
+
<article class="library-feed">
|
|
4
|
+
<header class="library-feed__header">
|
|
5
|
+
<NuxtLink
|
|
6
|
+
:to="item.to"
|
|
7
|
+
:title="item.title"
|
|
8
|
+
class="library-feed__avatar-link"
|
|
9
|
+
>
|
|
10
|
+
<UiImg
|
|
11
|
+
v-if="currentAvatar"
|
|
12
|
+
:src="currentAvatar"
|
|
13
|
+
:alt="item.title"
|
|
14
|
+
:loading="priority ? 'eager' : 'lazy'"
|
|
15
|
+
:priority="priority"
|
|
16
|
+
class="library-feed__avatar"
|
|
17
|
+
width="96"
|
|
18
|
+
height="96"
|
|
19
|
+
sizes="96px"
|
|
20
|
+
@error="avatarError = true"
|
|
21
|
+
/>
|
|
22
|
+
<span
|
|
23
|
+
v-else
|
|
24
|
+
class="library-feed__avatar library-feed__initials"
|
|
25
|
+
>{{ initials }}</span>
|
|
26
|
+
</NuxtLink>
|
|
27
|
+
|
|
28
|
+
<div class="library-feed__info">
|
|
29
|
+
<div class="library-feed__heading">
|
|
30
|
+
<NuxtLink
|
|
31
|
+
:to="item.to"
|
|
32
|
+
class="library-feed__name"
|
|
33
|
+
>
|
|
34
|
+
{{ item.title }}
|
|
35
|
+
<img
|
|
36
|
+
v-if="item.country"
|
|
37
|
+
:alt="item.country.title"
|
|
38
|
+
:title="`${t('birthplace_country')}: ${item.country.title}`"
|
|
39
|
+
:src="`/icons/flags/${item.country.name}.svg`"
|
|
40
|
+
class="library-feed__flag"
|
|
41
|
+
width="20"
|
|
42
|
+
height="14"
|
|
43
|
+
>
|
|
44
|
+
</NuxtLink>
|
|
45
|
+
|
|
46
|
+
<span
|
|
47
|
+
v-if="item.newCount"
|
|
48
|
+
class="library-feed__badge"
|
|
49
|
+
>{{ t('library.new_count', { count: item.newCount }) }}</span>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<LazyUiExpandRow
|
|
53
|
+
v-if="item.tags && item.tags.length"
|
|
54
|
+
:items="item.tags"
|
|
55
|
+
class="library-feed__tags"
|
|
56
|
+
hydrate-on-visible
|
|
57
|
+
micro
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<p class="library-feed__meta">
|
|
61
|
+
<span
|
|
62
|
+
v-for="(entry, index) in item.meta"
|
|
63
|
+
:key="`meta-${index}`"
|
|
64
|
+
class="library-feed__meta-item"
|
|
65
|
+
>
|
|
66
|
+
<UiIcon
|
|
67
|
+
v-if="entry.icon"
|
|
68
|
+
:name="entry.icon"
|
|
69
|
+
size="14"
|
|
70
|
+
/>
|
|
71
|
+
{{ entry.text }}
|
|
72
|
+
</span>
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="library-feed__actions _from-sm">
|
|
77
|
+
<UiBtn
|
|
78
|
+
:theme="following ? 'secondary' : 'primary'"
|
|
79
|
+
class="library-feed__follow"
|
|
80
|
+
@click="emit('toggle-follow', item)"
|
|
81
|
+
>
|
|
82
|
+
<UiIcon
|
|
83
|
+
:name="following ? 'check' : 'plus'"
|
|
84
|
+
size="16"
|
|
85
|
+
/>
|
|
86
|
+
{{ following ? t('library.following_state') : t('follow') }}
|
|
87
|
+
</UiBtn>
|
|
88
|
+
|
|
89
|
+
<UiLink
|
|
90
|
+
:to="item.to"
|
|
91
|
+
theme="secondary"
|
|
92
|
+
>
|
|
93
|
+
{{ videosText }}
|
|
94
|
+
<UiIcon
|
|
95
|
+
name="arrow-right"
|
|
96
|
+
size="16"
|
|
97
|
+
/>
|
|
98
|
+
</UiLink>
|
|
99
|
+
</div>
|
|
100
|
+
</header>
|
|
101
|
+
|
|
102
|
+
<h3
|
|
103
|
+
v-if="item.feedTitle"
|
|
104
|
+
class="library-feed__feed-title"
|
|
105
|
+
>{{ item.feedTitle }}</h3>
|
|
106
|
+
|
|
107
|
+
<div class="library-feed__grid">
|
|
108
|
+
<CardVideo
|
|
109
|
+
v-for="(video, index) in item.videos"
|
|
110
|
+
:key="video.guid"
|
|
111
|
+
:card="video"
|
|
112
|
+
:loading="priority && index < 3 ? 'eager' : 'lazy'"
|
|
113
|
+
:priority="priority && index < 3"
|
|
114
|
+
compact
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<UiLink
|
|
119
|
+
:to="item.to"
|
|
120
|
+
theme="secondary"
|
|
121
|
+
class="library-feed__all _to-sm"
|
|
122
|
+
wide
|
|
123
|
+
>
|
|
124
|
+
{{ videosText }}
|
|
125
|
+
<UiIcon
|
|
126
|
+
name="arrow-right"
|
|
127
|
+
size="16"
|
|
128
|
+
/>
|
|
129
|
+
</UiLink>
|
|
130
|
+
</article>
|
|
131
|
+
</template>
|
|
132
|
+
|
|
133
|
+
<script setup lang="ts">
|
|
134
|
+
import type { ILibraryFeedItem } from '../../types';
|
|
135
|
+
|
|
136
|
+
const props = defineProps<{
|
|
137
|
+
item: ILibraryFeedItem
|
|
138
|
+
following?: boolean
|
|
139
|
+
priority?: boolean
|
|
140
|
+
}>();
|
|
141
|
+
|
|
142
|
+
const emit = defineEmits<{
|
|
143
|
+
(eventName: 'toggle-follow', item: ILibraryFeedItem): void,
|
|
144
|
+
}>();
|
|
145
|
+
|
|
146
|
+
const { t } = useI18n();
|
|
147
|
+
|
|
148
|
+
const avatarError = ref(false);
|
|
149
|
+
|
|
150
|
+
const currentAvatar = computed(() => avatarError.value ? undefined : props.item.avatarUrl);
|
|
151
|
+
|
|
152
|
+
/** Заглушка вместо аватара: «Top To Bottom» → TTB, «Bareback» → B. */
|
|
153
|
+
const initials = computed(() => props.item.title
|
|
154
|
+
.split(/\s+/)
|
|
155
|
+
.slice(0, 3)
|
|
156
|
+
.map(word => word.charAt(0))
|
|
157
|
+
.join('')
|
|
158
|
+
.toUpperCase());
|
|
159
|
+
|
|
160
|
+
const videosText = computed(() => t('plural.videos', props.item.videosCount));
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<style lang="scss">
|
|
164
|
+
.library-feed {
|
|
165
|
+
/* Растушёвка «ещё» у тегов уходит в фон карточки, а не страницы */
|
|
166
|
+
--expand-row-fade-end: #{$library-feed-fade};
|
|
167
|
+
|
|
168
|
+
padding: 16px;
|
|
169
|
+
background-color: $library-feed-bg;
|
|
170
|
+
border: 1px solid $library-feed-border;
|
|
171
|
+
border-radius: $radius-xl;
|
|
172
|
+
|
|
173
|
+
@include from-br(sm) {
|
|
174
|
+
padding: 20px;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.library-feed__header {
|
|
179
|
+
display: grid;
|
|
180
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
181
|
+
column-gap: 16px;
|
|
182
|
+
row-gap: 12px;
|
|
183
|
+
align-items: start;
|
|
184
|
+
margin-bottom: 20px;
|
|
185
|
+
|
|
186
|
+
@include from-br(sm) {
|
|
187
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.library-feed__avatar {
|
|
192
|
+
width: 80px;
|
|
193
|
+
height: 80px;
|
|
194
|
+
object-fit: cover;
|
|
195
|
+
object-position: center 30%;
|
|
196
|
+
border-radius: $radius-full;
|
|
197
|
+
border: 2px solid $library-feed-avatar-border;
|
|
198
|
+
|
|
199
|
+
@include from-br(sm) {
|
|
200
|
+
width: 96px;
|
|
201
|
+
height: 96px;
|
|
202
|
+
transition: var(--transition) border-color;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@include from-br(sm) {
|
|
207
|
+
.library-feed__avatar-link:is(:hover, :focus-visible) .library-feed__avatar {
|
|
208
|
+
border-color: $library-feed-avatar-border--hover;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.library-feed__initials {
|
|
213
|
+
@include font-lg;
|
|
214
|
+
|
|
215
|
+
display: grid;
|
|
216
|
+
place-content: center;
|
|
217
|
+
font-weight: 700;
|
|
218
|
+
text-transform: uppercase;
|
|
219
|
+
color: $library-feed-initials;
|
|
220
|
+
background: linear-gradient(to bottom right, $library-feed-initials-gradient-from, $library-feed-initials-gradient-to);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.library-feed__info {
|
|
224
|
+
min-width: 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.library-feed__heading {
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-wrap: wrap;
|
|
230
|
+
align-items: center;
|
|
231
|
+
column-gap: 10px;
|
|
232
|
+
row-gap: 6px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.library-feed__name {
|
|
236
|
+
@include font-lg;
|
|
237
|
+
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
column-gap: 8px;
|
|
241
|
+
font-weight: 700;
|
|
242
|
+
text-transform: uppercase;
|
|
243
|
+
color: $library-feed-name;
|
|
244
|
+
|
|
245
|
+
@include from-br(sm) {
|
|
246
|
+
transition: var(--transition) color;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@include hover(color) {
|
|
250
|
+
color: $library-feed-name--hover;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.library-feed__flag {
|
|
255
|
+
flex-shrink: 0;
|
|
256
|
+
border-radius: 2px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.library-feed__badge {
|
|
260
|
+
@include font-xs;
|
|
261
|
+
|
|
262
|
+
flex-shrink: 0;
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
padding-block: 4px;
|
|
266
|
+
padding-inline: 10px;
|
|
267
|
+
color: $library-feed-badge-text;
|
|
268
|
+
background-color: $library-feed-badge-bg;
|
|
269
|
+
border-radius: $radius-full;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.library-feed__tags {
|
|
273
|
+
margin-top: 8px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.library-feed__meta {
|
|
277
|
+
@include font-sm;
|
|
278
|
+
|
|
279
|
+
display: flex;
|
|
280
|
+
flex-wrap: wrap;
|
|
281
|
+
align-items: center;
|
|
282
|
+
column-gap: 8px;
|
|
283
|
+
row-gap: 4px;
|
|
284
|
+
margin-top: 8px;
|
|
285
|
+
color: $library-feed-meta;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.library-feed__meta-item {
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
column-gap: 6px;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.library-feed__meta-item:not(:last-child)::after {
|
|
295
|
+
content: '·';
|
|
296
|
+
margin-inline-start: 2px;
|
|
297
|
+
color: $library-feed-meta-dot;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.library-feed__actions {
|
|
301
|
+
display: flex;
|
|
302
|
+
align-items: center;
|
|
303
|
+
column-gap: 8px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.library-feed__follow {
|
|
307
|
+
min-width: 124px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.library-feed__feed-title {
|
|
311
|
+
@include font-sm;
|
|
312
|
+
|
|
313
|
+
margin: 0 0 12px;
|
|
314
|
+
font-weight: 400;
|
|
315
|
+
color: $library-feed-title;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.library-feed__grid {
|
|
319
|
+
display: grid;
|
|
320
|
+
gap: 16px;
|
|
321
|
+
grid-template-columns: repeat(2, 1fr);
|
|
322
|
+
|
|
323
|
+
@include from-br(xm) {
|
|
324
|
+
grid-template-columns: repeat(3, 1fr);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@include from-br(sm) {
|
|
328
|
+
gap: 24px 16px;
|
|
329
|
+
grid-template-columns: repeat(4, 1fr);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
@include from-br(md) {
|
|
333
|
+
grid-template-columns: repeat(5, 1fr);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.library-feed__all {
|
|
338
|
+
margin-top: 16px;
|
|
339
|
+
}
|
|
340
|
+
</style>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<!-- Список подписок «Библиотеки»: сортировка, блоки лент, подтверждение отписки -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="library-follows">
|
|
4
|
+
<UiSectionTitle
|
|
5
|
+
:title="title"
|
|
6
|
+
:count="items.length"
|
|
7
|
+
:icon="icon"
|
|
8
|
+
class="library-follows__title"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<div class="library-follows__sort">
|
|
12
|
+
<UiBtn
|
|
13
|
+
v-for="option in sortItems"
|
|
14
|
+
:key="option.name"
|
|
15
|
+
:theme="sort === option.name ? 'primary' : 'secondary'"
|
|
16
|
+
@click="sort = option.name"
|
|
17
|
+
>{{ option.title }}</UiBtn>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div
|
|
21
|
+
v-if="sortedItems.length > 0"
|
|
22
|
+
class="library-follows__list"
|
|
23
|
+
>
|
|
24
|
+
<LibraryFeed
|
|
25
|
+
v-for="(item, index) in sortedItems"
|
|
26
|
+
:key="item.guid"
|
|
27
|
+
:item="item"
|
|
28
|
+
:following="isFollowing(item)"
|
|
29
|
+
:priority="index === 0"
|
|
30
|
+
@toggle-follow="onToggleFollow"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<UiEmptyState
|
|
35
|
+
v-else
|
|
36
|
+
without-reset
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<UiConfirm
|
|
40
|
+
v-model="isUnfollowOpen"
|
|
41
|
+
:title="t('library.unfollow_title', { name: pendingUnfollow?.title })"
|
|
42
|
+
:confirm-text="t('library.unfollow_confirm')"
|
|
43
|
+
@confirm="onUnfollowConfirm"
|
|
44
|
+
>
|
|
45
|
+
<p class="library-follows__confirm">{{ t('library.unfollow_text') }}</p>
|
|
46
|
+
</UiConfirm>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup lang="ts">
|
|
51
|
+
import type { ILibraryFeedItem } from '../../types';
|
|
52
|
+
|
|
53
|
+
const LibrarySort = {
|
|
54
|
+
New: 'new',
|
|
55
|
+
Alphabet: 'alphabet',
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
type LibrarySortValue = typeof LibrarySort[keyof typeof LibrarySort];
|
|
59
|
+
|
|
60
|
+
const props = defineProps<{
|
|
61
|
+
items: ILibraryFeedItem[]
|
|
62
|
+
title: string
|
|
63
|
+
icon: string
|
|
64
|
+
}>();
|
|
65
|
+
|
|
66
|
+
const { t } = useI18n();
|
|
67
|
+
|
|
68
|
+
const sort = ref<LibrarySortValue>(LibrarySort.New);
|
|
69
|
+
|
|
70
|
+
const sortItems = computed(() => [
|
|
71
|
+
{ name: LibrarySort.New, title: t('library.sort_new_first') },
|
|
72
|
+
{ name: LibrarySort.Alphabet, title: t('library.sort_alphabet') },
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* «Сначала новые» — по числу непросмотренных новинок, а не по дате обновления: подписка
|
|
77
|
+
* с тремя новыми роликами важнее свежей, но с одним. Порядок равных сохраняет сортировка.
|
|
78
|
+
*/
|
|
79
|
+
const sortedItems = computed(() => {
|
|
80
|
+
const items = [...props.items];
|
|
81
|
+
|
|
82
|
+
return sort.value === LibrarySort.Alphabet
|
|
83
|
+
? items.sort((a, b) => a.title.localeCompare(b.title))
|
|
84
|
+
: items.sort((a, b) => b.newCount - a.newCount);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const unfollowed = ref<string[]>([]);
|
|
88
|
+
|
|
89
|
+
function isFollowing(item: ILibraryFeedItem) {
|
|
90
|
+
return !unfollowed.value.includes(item.guid);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Сущность, для которой открыт попап отписки. */
|
|
94
|
+
const pendingUnfollow = ref<ILibraryFeedItem | null>(null);
|
|
95
|
+
|
|
96
|
+
const isUnfollowOpen = computed({
|
|
97
|
+
get: () => pendingUnfollow.value !== null,
|
|
98
|
+
set: value => {
|
|
99
|
+
if (!value) pendingUnfollow.value = null;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/** Подписка — сразу, отписка — только через подтверждение: она сносит ленту обновлений. */
|
|
104
|
+
function onToggleFollow(item: ILibraryFeedItem) {
|
|
105
|
+
if (isFollowing(item)) {
|
|
106
|
+
pendingUnfollow.value = item;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
unfollowed.value = unfollowed.value.filter(guid => guid !== item.guid);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onUnfollowConfirm() {
|
|
114
|
+
if (!pendingUnfollow.value) return;
|
|
115
|
+
|
|
116
|
+
unfollowed.value = [...unfollowed.value, pendingUnfollow.value.guid];
|
|
117
|
+
pendingUnfollow.value = null;
|
|
118
|
+
}
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<style lang="scss">
|
|
122
|
+
.library-follows__title {
|
|
123
|
+
margin-bottom: 16px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.library-follows__sort {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-wrap: wrap;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 8px;
|
|
131
|
+
margin-bottom: 24px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.library-follows__list {
|
|
135
|
+
display: grid;
|
|
136
|
+
row-gap: 24px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.library-follows__confirm {
|
|
140
|
+
@include font-sm;
|
|
141
|
+
|
|
142
|
+
max-width: 400px;
|
|
143
|
+
margin: 0 auto;
|
|
144
|
+
text-align: center;
|
|
145
|
+
color: $page-library-confirm-text;
|
|
146
|
+
}
|
|
147
|
+
</style>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
{ '--expand-show': isExpandOpened },
|
|
6
6
|
{ '--alphabet': alphabet },
|
|
7
7
|
{ '--mobile-expand': mobileExpand },
|
|
8
|
+
{ '--micro': micro },
|
|
8
9
|
]"
|
|
9
10
|
class="ui-expand-row"
|
|
10
11
|
>
|
|
@@ -27,12 +28,13 @@
|
|
|
27
28
|
:alphabet="alphabet"
|
|
28
29
|
is-link
|
|
29
30
|
:mini="mini"
|
|
31
|
+
:micro="micro"
|
|
30
32
|
/>
|
|
31
33
|
</slot>
|
|
32
34
|
<div
|
|
33
35
|
v-if="!hideMore"
|
|
34
36
|
v-show="isElementsOverflow"
|
|
35
|
-
:class="{'--small': mini}"
|
|
37
|
+
:class="{'--small': mini, '--micro': micro}"
|
|
36
38
|
class="ui-expand-row__more-wrapper"
|
|
37
39
|
>
|
|
38
40
|
<UiChips
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
icon: isExpandOpened ? 'minus' : 'plus',
|
|
42
44
|
}"
|
|
43
45
|
:mini="mini"
|
|
46
|
+
:micro="micro"
|
|
44
47
|
class="ui-expand-row__button --more"
|
|
45
48
|
@click="onMoreClick"
|
|
46
49
|
/>
|
|
@@ -57,6 +60,7 @@ defineProps<{
|
|
|
57
60
|
alphabet?: boolean;
|
|
58
61
|
hideMore?: boolean;
|
|
59
62
|
mini?: boolean;
|
|
63
|
+
micro?: boolean;
|
|
60
64
|
mobileExpand?: boolean;
|
|
61
65
|
preText?: string;
|
|
62
66
|
}>();
|
|
@@ -167,6 +171,12 @@ onBeforeUnmount(() => {
|
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
|
|
174
|
+
/* Обрезка идёт по высоте одной строки, а она зависит от размера чипса — иначе из-под края
|
|
175
|
+
выглядывает следующий ряд. Правило стоит до `--expand-show`: у них равная специфичность. */
|
|
176
|
+
.ui-expand-row.--micro {
|
|
177
|
+
max-height: var(--chips-height-micro);
|
|
178
|
+
}
|
|
179
|
+
|
|
170
180
|
.ui-expand-row__wrapper {
|
|
171
181
|
display: inline-flex;
|
|
172
182
|
flex-wrap: wrap;
|
|
@@ -193,7 +203,7 @@ onBeforeUnmount(() => {
|
|
|
193
203
|
height: var(--chips-height);
|
|
194
204
|
z-index: $z-index-plus;
|
|
195
205
|
padding-left: 40px;
|
|
196
|
-
background: linear-gradient(90deg, $expand-row-fade-start 0%, $expand-row-fade-end 22.64%);
|
|
206
|
+
background: linear-gradient(90deg, $expand-row-fade-start 0%, var(--expand-row-fade-end, #{$expand-row-fade-end}) 22.64%);
|
|
197
207
|
}
|
|
198
208
|
|
|
199
209
|
.ui-expand-row__more-wrapper.--small {
|
|
@@ -204,6 +214,15 @@ onBeforeUnmount(() => {
|
|
|
204
214
|
}
|
|
205
215
|
}
|
|
206
216
|
|
|
217
|
+
.ui-expand-row__more-wrapper.--micro {
|
|
218
|
+
top: 0;
|
|
219
|
+
height: var(--chips-height-micro);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.ui-expand-row.--micro .ui-expand-row__wrapper {
|
|
223
|
+
gap: 4px;
|
|
224
|
+
}
|
|
225
|
+
|
|
207
226
|
.ui-expand-row__button.--more {
|
|
208
227
|
text-transform: capitalize;
|
|
209
228
|
}
|