itube-specs 0.0.1061 → 0.0.1064
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/SITE_UPDATES.md +7 -0
- package/assets/scss/themes/dark/_theme.scss +31 -1
- package/assets/scss/themes/light/_theme.scss +31 -1
- package/components/layout/footer-language.vue +1 -0
- package/components/layout/header.vue +6 -4
- package/components/layout/notifications.vue +29 -1
- package/components/library/upload-card.vue +448 -0
- package/components/library/uploads-grid.vue +145 -0
- package/components/library/uploads.vue +146 -0
- package/components/upload/sent.vue +1 -1
- package/composables/use-my-uploads.ts +26 -0
- package/i18n/locales/en-US.json +44 -0
- package/package.json +1 -1
- package/pages/library/index.vue +31 -0
- package/pages/library/uploads.vue +41 -0
- package/runtime/constants/upload.ts +9 -0
- package/stubs/uploads.ts +80 -0
- package/types/upload.d.ts +17 -1
package/SITE_UPDATES.md
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
коммиты до бампа); `npm run patch` сам проставит ей номер версии. Формат пункта: что сделать и где,
|
|
12
12
|
кому именно (всем / сайтам с N), что будет, если не сделать.
|
|
13
13
|
|
|
14
|
+
## 0.0.1062 — 2026-11-02
|
|
15
|
+
|
|
16
|
+
- **Всем — к сведению:** за тем же флагом `FeatureUploadEnabled` — страница «My videos» `/library/uploads` (загруженные
|
|
17
|
+
ролики пользователя: вкладки по статусу, видимость, ссылка, правка названия, удаление; данные — заглушка). Туда
|
|
18
|
+
ведёт «Go to my videos» после загрузки, а в `/library` появилась секция «My videos» (4 последних ролика, «Show all»). Новые общие ключи `my_uploads` и три ключа в `snackbar` (пока только en-US).
|
|
19
|
+
Делать ничего не надо.
|
|
20
|
+
|
|
14
21
|
## 0.0.1061 — 2026-11-02
|
|
15
22
|
|
|
16
23
|
- **Всем — решить флаг:** новая страница загрузки видео `/upload` (выбор файла с проверкой в браузере → детали
|
|
@@ -467,7 +467,9 @@ $header-language-menu-shadow: 0 20px 25px -5px #{$mono-black-25}, 0 8px 10px -6p
|
|
|
467
467
|
$header-language-menu-item--hover: $color-zinc-800-50;
|
|
468
468
|
|
|
469
469
|
// ─── notifications ───
|
|
470
|
-
$notifications-icon: $color-zinc-
|
|
470
|
+
$notifications-icon: $color-zinc-400;
|
|
471
|
+
$notifications-bg: $color-zinc-800-50;
|
|
472
|
+
$notifications-bg--hover: $color-zinc-700;
|
|
471
473
|
$notifications-icon--mobile: $color-zinc-200;
|
|
472
474
|
$notifications-icon--hover: $color-white;
|
|
473
475
|
$notifications-badge-bg: $color-indigo-600;
|
|
@@ -1270,3 +1272,31 @@ $upload-radio-ring: $color-zinc-700;
|
|
|
1270
1272
|
$upload-radio-dot: $color-white;
|
|
1271
1273
|
$upload-rules-text: $color-zinc-400;
|
|
1272
1274
|
$upload-rules-dot: $color-zinc-600;
|
|
1275
|
+
|
|
1276
|
+
// ─── My videos (library uploads) ───
|
|
1277
|
+
$library-upload-placeholder-from: $color-zinc-700;
|
|
1278
|
+
$library-upload-placeholder-to: $color-zinc-800;
|
|
1279
|
+
$library-upload-badge-bg: $color-black-75;
|
|
1280
|
+
$library-upload-badge-text: $color-white;
|
|
1281
|
+
$library-upload-dot: $color-indigo-400;
|
|
1282
|
+
$library-upload-dot--error: $color-red-500;
|
|
1283
|
+
$library-upload-title: $color-white;
|
|
1284
|
+
$library-upload-title--hover: $color-indigo-400;
|
|
1285
|
+
$library-upload-untitled: $color-zinc-500;
|
|
1286
|
+
$library-upload-meta: $color-zinc-500;
|
|
1287
|
+
$library-upload-meta--error: $color-red-400;
|
|
1288
|
+
$library-upload-menu-icon: $color-zinc-500;
|
|
1289
|
+
$library-upload-menu-icon--hover: $color-white;
|
|
1290
|
+
$library-upload-menu-value: $color-zinc-500;
|
|
1291
|
+
$library-upload-delete-text: $color-red-400;
|
|
1292
|
+
$library-upload-radio: $color-zinc-500;
|
|
1293
|
+
$library-upload-radio--checked: $color-indigo-600;
|
|
1294
|
+
$library-upload-radio-dot: $color-white;
|
|
1295
|
+
$library-upload-empty-icon-bg: $color-zinc-900;
|
|
1296
|
+
$library-upload-empty-icon-border: $color-zinc-800;
|
|
1297
|
+
$library-upload-empty-icon: $color-zinc-400;
|
|
1298
|
+
$library-upload-empty-title: $color-white;
|
|
1299
|
+
$library-upload-note: $color-zinc-500;
|
|
1300
|
+
$library-upload-tab-count: $color-zinc-500;
|
|
1301
|
+
$library-upload-tab-count--active: $color-white-70;
|
|
1302
|
+
$library-upload-fix-text: $color-zinc-400;
|
|
@@ -523,7 +523,9 @@ $header-language-menu-bg: $color-slate-850;
|
|
|
523
523
|
$header-language-menu-border: $color-white-10;
|
|
524
524
|
$header-language-menu-shadow: 0 20px 25px -5px #{$mono-black-25}, 0 8px 10px -6px #{$mono-black-25};
|
|
525
525
|
$header-language-menu-item--hover: $color-white-5;
|
|
526
|
-
$notifications-icon: $color-gray-
|
|
526
|
+
$notifications-icon: $color-gray-400;
|
|
527
|
+
$notifications-bg: $color-slate-750;
|
|
528
|
+
$notifications-bg--hover: $color-slate-700;
|
|
527
529
|
$notifications-icon--mobile: $color-gray-200;
|
|
528
530
|
$notifications-icon--hover: $color-white;
|
|
529
531
|
$notifications-badge-bg: $color-indigo-600;
|
|
@@ -1299,3 +1301,31 @@ $upload-radio-ring: $color-gray-300;
|
|
|
1299
1301
|
$upload-radio-dot: $color-white;
|
|
1300
1302
|
$upload-rules-text: $color-gray-300;
|
|
1301
1303
|
$upload-rules-dot: $color-gray-500;
|
|
1304
|
+
|
|
1305
|
+
// ─── My videos (library uploads) ───
|
|
1306
|
+
$library-upload-placeholder-from: $color-gray-200;
|
|
1307
|
+
$library-upload-placeholder-to: $color-gray-300;
|
|
1308
|
+
$library-upload-badge-bg: $color-black-75;
|
|
1309
|
+
$library-upload-badge-text: $color-white;
|
|
1310
|
+
$library-upload-dot: $color-indigo-400;
|
|
1311
|
+
$library-upload-dot--error: $color-red-500;
|
|
1312
|
+
$library-upload-title: $color-gray-900;
|
|
1313
|
+
$library-upload-title--hover: $color-indigo-600;
|
|
1314
|
+
$library-upload-untitled: $color-gray-500;
|
|
1315
|
+
$library-upload-meta: $color-gray-500;
|
|
1316
|
+
$library-upload-meta--error: $color-red-600;
|
|
1317
|
+
$library-upload-menu-icon: $color-gray-500;
|
|
1318
|
+
$library-upload-menu-icon--hover: $color-gray-900;
|
|
1319
|
+
$library-upload-menu-value: $color-gray-500;
|
|
1320
|
+
$library-upload-delete-text: $color-red-400;
|
|
1321
|
+
$library-upload-radio: $color-gray-400;
|
|
1322
|
+
$library-upload-radio--checked: $color-indigo-600;
|
|
1323
|
+
$library-upload-radio-dot: $color-white;
|
|
1324
|
+
$library-upload-empty-icon-bg: $color-gray-100;
|
|
1325
|
+
$library-upload-empty-icon-border: $color-gray-200;
|
|
1326
|
+
$library-upload-empty-icon: $color-gray-500;
|
|
1327
|
+
$library-upload-empty-title: $color-gray-900;
|
|
1328
|
+
$library-upload-note: $color-gray-500;
|
|
1329
|
+
$library-upload-tab-count: $color-gray-500;
|
|
1330
|
+
$library-upload-tab-count--active: $color-white-70;
|
|
1331
|
+
$library-upload-fix-text: $color-gray-300;
|
|
@@ -22,10 +22,6 @@
|
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
24
|
<div class="layout-header__right">
|
|
25
|
-
<LayoutNotifications
|
|
26
|
-
v-if="featureFlags.FeatureFollowEnabled"
|
|
27
|
-
class="layout-header__notifications"
|
|
28
|
-
/>
|
|
29
25
|
<NuxtLink
|
|
30
26
|
v-if="featureFlags.FeatureUploadEnabled"
|
|
31
27
|
:to="uploadLink"
|
|
@@ -76,6 +72,11 @@
|
|
|
76
72
|
</transition>
|
|
77
73
|
</div>
|
|
78
74
|
|
|
75
|
+
<LayoutNotifications
|
|
76
|
+
v-if="featureFlags.FeatureFollowEnabled"
|
|
77
|
+
class="layout-header__notifications"
|
|
78
|
+
/>
|
|
79
|
+
|
|
79
80
|
<LazyLayoutHeaderUser
|
|
80
81
|
v-if="isAuthorized"
|
|
81
82
|
class="layout-header__user _from-sd"
|
|
@@ -303,6 +304,7 @@ function closeLanguage() {
|
|
|
303
304
|
.layout-header__language-badge {
|
|
304
305
|
@include locale-badge;
|
|
305
306
|
|
|
307
|
+
padding: 0;
|
|
306
308
|
color: $header-language-badge;
|
|
307
309
|
}
|
|
308
310
|
|
|
@@ -51,8 +51,29 @@ $badge-raise: 6px;
|
|
|
51
51
|
color: $notifications-icon--mobile;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
@include
|
|
54
|
+
@include from-br($brkpnt-burger) {
|
|
55
|
+
position: relative;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
width: 36px;
|
|
58
|
+
height: 36px;
|
|
59
|
+
padding: 0;
|
|
60
|
+
background-color: $notifications-bg;
|
|
61
|
+
border-radius: $radius-md;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@include hover(color, background-color) {
|
|
55
65
|
color: $notifications-icon--hover;
|
|
66
|
+
|
|
67
|
+
@include from-br($brkpnt-burger) {
|
|
68
|
+
background-color: $notifications-bg--hover;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.layout-notifications__icon {
|
|
74
|
+
@include from-br($brkpnt-burger) {
|
|
75
|
+
width: 20px;
|
|
76
|
+
height: 20px;
|
|
56
77
|
}
|
|
57
78
|
}
|
|
58
79
|
|
|
@@ -75,5 +96,12 @@ $badge-raise: 6px;
|
|
|
75
96
|
background-color: $notifications-badge-bg;
|
|
76
97
|
border-radius: $radius-full;
|
|
77
98
|
box-shadow: 0 0 0 2px $notifications-badge-ring;
|
|
99
|
+
|
|
100
|
+
@include from-br($brkpnt-burger) {
|
|
101
|
+
position: absolute;
|
|
102
|
+
top: -6px;
|
|
103
|
+
right: -6px;
|
|
104
|
+
margin: 0;
|
|
105
|
+
}
|
|
78
106
|
}
|
|
79
107
|
</style>
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
<!-- Card of the user's uploaded video in «My videos»: cover (placeholder until AI picks one), status badge, duration and visibility, menu with visibility / copy link / title fix / delete -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="library-upload-card">
|
|
4
|
+
<component
|
|
5
|
+
:is="link ? NuxtLink : 'div'"
|
|
6
|
+
:to="link"
|
|
7
|
+
class="library-upload-card__poster"
|
|
8
|
+
>
|
|
9
|
+
<UiImg
|
|
10
|
+
v-if="upload.thumbUrls"
|
|
11
|
+
:urls="upload.thumbUrls"
|
|
12
|
+
:alt="name"
|
|
13
|
+
sizes="auto, (min-width: 1440px) 25vw, (min-width: 900px) 33vw, (min-width: 370px) 50vw, 100vw"
|
|
14
|
+
class="library-upload-card__image"
|
|
15
|
+
width="480"
|
|
16
|
+
height="270"
|
|
17
|
+
/>
|
|
18
|
+
<span
|
|
19
|
+
v-else
|
|
20
|
+
class="library-upload-card__placeholder"
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<span
|
|
25
|
+
v-if="upload.status !== MyUploadStatus.Published"
|
|
26
|
+
class="library-upload-card__badge --status"
|
|
27
|
+
>
|
|
28
|
+
<span
|
|
29
|
+
:class="{'--error': upload.status === MyUploadStatus.Declined}"
|
|
30
|
+
class="library-upload-card__dot"
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
/>
|
|
33
|
+
{{ t(`my_uploads.status.${upload.status}`) }}
|
|
34
|
+
</span>
|
|
35
|
+
|
|
36
|
+
<span class="library-upload-card__bottom">
|
|
37
|
+
<span class="library-upload-card__badge">{{ getDuration(upload.duration) }}</span>
|
|
38
|
+
<span
|
|
39
|
+
v-if="upload.visibility !== UploadVisibility.Public"
|
|
40
|
+
:title="t(`my_uploads.visibility.${upload.visibility}`)"
|
|
41
|
+
class="library-upload-card__badge --icon"
|
|
42
|
+
>
|
|
43
|
+
<UiIcon
|
|
44
|
+
:name="upload.visibility === UploadVisibility.Private ? 'lock' : 'link'"
|
|
45
|
+
size="12"
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
/>
|
|
48
|
+
<span class="_visually-hidden">{{ t(`my_uploads.visibility.${upload.visibility}`) }}</span>
|
|
49
|
+
</span>
|
|
50
|
+
</span>
|
|
51
|
+
</component>
|
|
52
|
+
|
|
53
|
+
<div class="library-upload-card__info">
|
|
54
|
+
<div class="library-upload-card__head">
|
|
55
|
+
<component
|
|
56
|
+
:is="link ? NuxtLink : 'span'"
|
|
57
|
+
:to="link"
|
|
58
|
+
:class="{'--untitled': !upload.title, '--link': link}"
|
|
59
|
+
class="library-upload-card__title _truncate"
|
|
60
|
+
>{{ name }}</component>
|
|
61
|
+
|
|
62
|
+
<UiDropdown
|
|
63
|
+
ref="dropdownRef"
|
|
64
|
+
class="library-upload-card__menu"
|
|
65
|
+
right
|
|
66
|
+
use-v-if
|
|
67
|
+
>
|
|
68
|
+
<template #trigger>
|
|
69
|
+
<button
|
|
70
|
+
:aria-label="t('my_uploads.menu.label')"
|
|
71
|
+
class="library-upload-card__menu-trigger"
|
|
72
|
+
type="button"
|
|
73
|
+
>
|
|
74
|
+
<UiIcon
|
|
75
|
+
name="dots-vertical"
|
|
76
|
+
size="18"
|
|
77
|
+
/>
|
|
78
|
+
</button>
|
|
79
|
+
</template>
|
|
80
|
+
<template #items="className">
|
|
81
|
+
<template v-if="step === 'menu'">
|
|
82
|
+
<button
|
|
83
|
+
:class="[className, {'--disabled': !canChangeVisibility(upload)}]"
|
|
84
|
+
:disabled="!canChangeVisibility(upload)"
|
|
85
|
+
type="button"
|
|
86
|
+
@click.stop="step = 'visibility'"
|
|
87
|
+
>
|
|
88
|
+
<UiIcon
|
|
89
|
+
name="eye"
|
|
90
|
+
size="20"
|
|
91
|
+
/>
|
|
92
|
+
<span class="library-upload-card__menu-label">{{ t('my_uploads.menu.visibility') }}</span>
|
|
93
|
+
<span class="library-upload-card__menu-value">{{ t(`upload_page.visibility_options.${upload.visibility}.title`) }}</span>
|
|
94
|
+
<UiIcon
|
|
95
|
+
name="angle-right"
|
|
96
|
+
size="16"
|
|
97
|
+
/>
|
|
98
|
+
</button>
|
|
99
|
+
<button
|
|
100
|
+
:class="className"
|
|
101
|
+
type="button"
|
|
102
|
+
@click="emit('copy')"
|
|
103
|
+
>
|
|
104
|
+
<UiIcon
|
|
105
|
+
name="link"
|
|
106
|
+
size="20"
|
|
107
|
+
/>
|
|
108
|
+
{{ t('my_uploads.menu.copy_link') }}
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
:class="className"
|
|
112
|
+
type="button"
|
|
113
|
+
@click="emit('fix')"
|
|
114
|
+
>
|
|
115
|
+
<UiIcon
|
|
116
|
+
name="pen"
|
|
117
|
+
size="20"
|
|
118
|
+
/>
|
|
119
|
+
{{ t('my_uploads.menu.suggest_fix') }}
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
:class="[className, 'library-upload-card__delete']"
|
|
123
|
+
type="button"
|
|
124
|
+
@click="emit('delete')"
|
|
125
|
+
>
|
|
126
|
+
<UiIcon
|
|
127
|
+
name="trash-bin"
|
|
128
|
+
size="20"
|
|
129
|
+
/>
|
|
130
|
+
{{ t('my_uploads.menu.delete') }}
|
|
131
|
+
</button>
|
|
132
|
+
</template>
|
|
133
|
+
|
|
134
|
+
<template v-else>
|
|
135
|
+
<button
|
|
136
|
+
:class="className"
|
|
137
|
+
type="button"
|
|
138
|
+
@click.stop="step = 'menu'"
|
|
139
|
+
>
|
|
140
|
+
<UiIcon
|
|
141
|
+
name="angle-left"
|
|
142
|
+
size="20"
|
|
143
|
+
/>
|
|
144
|
+
{{ t('my_uploads.menu.visibility') }}
|
|
145
|
+
</button>
|
|
146
|
+
<button
|
|
147
|
+
v-for="option in VISIBILITY_OPTIONS"
|
|
148
|
+
:key="option"
|
|
149
|
+
:class="className"
|
|
150
|
+
:aria-checked="option === upload.visibility"
|
|
151
|
+
type="button"
|
|
152
|
+
role="menuitemradio"
|
|
153
|
+
@click="emit('visibility', option)"
|
|
154
|
+
>
|
|
155
|
+
<span
|
|
156
|
+
:class="{'--checked': option === upload.visibility}"
|
|
157
|
+
class="library-upload-card__radio"
|
|
158
|
+
aria-hidden="true"
|
|
159
|
+
/>
|
|
160
|
+
<span class="library-upload-card__menu-label">
|
|
161
|
+
{{ t(`upload_page.visibility_options.${option}.title`) }}
|
|
162
|
+
<span class="library-upload-card__menu-hint">{{ t(`upload_page.visibility_options.${option}.text`) }}</span>
|
|
163
|
+
</span>
|
|
164
|
+
</button>
|
|
165
|
+
</template>
|
|
166
|
+
</template>
|
|
167
|
+
</UiDropdown>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<p class="library-upload-card__meta">
|
|
171
|
+
<UiIcon
|
|
172
|
+
v-if="nicheIcon"
|
|
173
|
+
:name="nicheIcon"
|
|
174
|
+
class="library-upload-card__niche"
|
|
175
|
+
size="14"
|
|
176
|
+
/>
|
|
177
|
+
<span
|
|
178
|
+
:class="{'--error': upload.status === MyUploadStatus.Declined}"
|
|
179
|
+
:title="upload.status === MyUploadStatus.Declined ? meta : undefined"
|
|
180
|
+
class="library-upload-card__meta-text _truncate"
|
|
181
|
+
>{{ meta }}</span>
|
|
182
|
+
</p>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</template>
|
|
186
|
+
|
|
187
|
+
<script setup lang="ts">
|
|
188
|
+
import { NuxtLink } from '#components';
|
|
189
|
+
import { MyUploadStatus, UploadVisibility, getDuration } from '../../runtime';
|
|
190
|
+
import type { IMyUpload } from '../../types';
|
|
191
|
+
|
|
192
|
+
const VISIBILITY_OPTIONS = Object.values(UploadVisibility);
|
|
193
|
+
|
|
194
|
+
const props = defineProps<{
|
|
195
|
+
upload: IMyUpload
|
|
196
|
+
}>();
|
|
197
|
+
|
|
198
|
+
const emit = defineEmits<{
|
|
199
|
+
(eventName: 'copy' | 'fix' | 'delete'): void
|
|
200
|
+
(eventName: 'visibility', value: UploadVisibility): void
|
|
201
|
+
}>();
|
|
202
|
+
|
|
203
|
+
const { t, n, locale } = useI18n();
|
|
204
|
+
const { generateLink } = useGenerateLink();
|
|
205
|
+
const { niches } = useNiches();
|
|
206
|
+
const { canChangeVisibility } = useMyUploads();
|
|
207
|
+
|
|
208
|
+
const dropdownRef = ref<{ open: boolean }>();
|
|
209
|
+
const step = ref<'menu' | 'visibility'>('menu');
|
|
210
|
+
|
|
211
|
+
watch(() => dropdownRef.value?.open, (open) => {
|
|
212
|
+
if (!open) step.value = 'menu';
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const name = computed(() => props.upload.title ?? props.upload.fileName);
|
|
216
|
+
|
|
217
|
+
const link = computed(() => props.upload.videoId ? generateLink(`/videos/${props.upload.videoId}`) : undefined);
|
|
218
|
+
|
|
219
|
+
const nicheIcon = computed(() => niches.value.length > 1
|
|
220
|
+
? niches.value.find(item => item.name === props.upload.niche)?.icon
|
|
221
|
+
: undefined);
|
|
222
|
+
|
|
223
|
+
const meta = computed(() => {
|
|
224
|
+
const { status } = props.upload;
|
|
225
|
+
|
|
226
|
+
if (status === MyUploadStatus.Declined) return props.upload.declineReason ?? t('my_uploads.meta.declined');
|
|
227
|
+
if (status !== MyUploadStatus.Published) return t(`my_uploads.meta.${status}`);
|
|
228
|
+
|
|
229
|
+
return t('my_uploads.meta.published', {
|
|
230
|
+
views: n(props.upload.views ?? 0, 'compact'),
|
|
231
|
+
ago: props.upload.publishedAt ? useFormatTimeAgo(props.upload.publishedAt, locale.value) : '',
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
</script>
|
|
235
|
+
|
|
236
|
+
<style lang="scss">
|
|
237
|
+
.library-upload-card {
|
|
238
|
+
@include video-card;
|
|
239
|
+
|
|
240
|
+
row-gap: 12px;
|
|
241
|
+
|
|
242
|
+
@include from-br(sm) {
|
|
243
|
+
row-gap: 16px;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.library-upload-card__poster {
|
|
248
|
+
@include video-card-poster;
|
|
249
|
+
|
|
250
|
+
display: block;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.library-upload-card__image,
|
|
254
|
+
.library-upload-card__placeholder {
|
|
255
|
+
display: block;
|
|
256
|
+
width: 100%;
|
|
257
|
+
height: 100%;
|
|
258
|
+
transition: transform 0.3s;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.library-upload-card__image {
|
|
262
|
+
object-fit: cover;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.library-upload-card__placeholder {
|
|
266
|
+
background-image: linear-gradient(to bottom right, $library-upload-placeholder-from, $library-upload-placeholder-to);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@media (hover: hover) {
|
|
270
|
+
.library-upload-card:has(.library-upload-card__poster:hover) .library-upload-card__image,
|
|
271
|
+
.library-upload-card:has(.library-upload-card__poster:hover) .library-upload-card__placeholder {
|
|
272
|
+
transform: scale(1.05);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.library-upload-card:has(.library-upload-card__poster:hover) .library-upload-card__title.--link {
|
|
276
|
+
color: $library-upload-title--hover;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.library-upload-card__badge {
|
|
281
|
+
@include font-xs;
|
|
282
|
+
|
|
283
|
+
display: flex;
|
|
284
|
+
align-items: center;
|
|
285
|
+
gap: 6px;
|
|
286
|
+
padding: 2px 6px;
|
|
287
|
+
font-weight: 500;
|
|
288
|
+
color: $library-upload-badge-text;
|
|
289
|
+
background-color: $library-upload-badge-bg;
|
|
290
|
+
border-radius: $radius-small;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.library-upload-card__badge.--status {
|
|
294
|
+
position: absolute;
|
|
295
|
+
top: 8px;
|
|
296
|
+
left: 8px;
|
|
297
|
+
padding: 4px 8px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.library-upload-card__badge.--icon {
|
|
301
|
+
justify-content: center;
|
|
302
|
+
width: 20px;
|
|
303
|
+
height: 20px;
|
|
304
|
+
padding: 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.library-upload-card__bottom {
|
|
308
|
+
position: absolute;
|
|
309
|
+
bottom: 8px;
|
|
310
|
+
left: 8px;
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
gap: 4px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.library-upload-card__dot {
|
|
317
|
+
width: 6px;
|
|
318
|
+
height: 6px;
|
|
319
|
+
background-color: $library-upload-dot;
|
|
320
|
+
border-radius: $radius-full;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.library-upload-card__dot.--error {
|
|
324
|
+
background-color: $library-upload-dot--error;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.library-upload-card__info {
|
|
328
|
+
display: flex;
|
|
329
|
+
flex-direction: column;
|
|
330
|
+
gap: 6px;
|
|
331
|
+
min-width: 0;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.library-upload-card__head {
|
|
335
|
+
display: flex;
|
|
336
|
+
justify-content: space-between;
|
|
337
|
+
align-items: flex-start;
|
|
338
|
+
gap: 8px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.library-upload-card__title {
|
|
342
|
+
@include font-sm;
|
|
343
|
+
|
|
344
|
+
--line-clamp: 2;
|
|
345
|
+
|
|
346
|
+
font-weight: 500;
|
|
347
|
+
color: $library-upload-title;
|
|
348
|
+
transition: color var(--transition);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.library-upload-card__title.--link {
|
|
352
|
+
@include hover(color) {
|
|
353
|
+
color: $library-upload-title--hover;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.library-upload-card__title.--untitled {
|
|
358
|
+
--word-break: break-all;
|
|
359
|
+
|
|
360
|
+
color: $library-upload-untitled;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.library-upload-card__menu {
|
|
364
|
+
flex-shrink: 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.library-upload-card__menu-trigger {
|
|
368
|
+
display: grid;
|
|
369
|
+
place-content: center;
|
|
370
|
+
margin-top: -2px;
|
|
371
|
+
padding: 2px;
|
|
372
|
+
color: $library-upload-menu-icon;
|
|
373
|
+
|
|
374
|
+
@include hover(color) {
|
|
375
|
+
color: $library-upload-menu-icon--hover;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.library-upload-card__menu-label {
|
|
380
|
+
display: flex;
|
|
381
|
+
flex: 1;
|
|
382
|
+
flex-direction: column;
|
|
383
|
+
text-align: left;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.library-upload-card__menu-value,
|
|
387
|
+
.library-upload-card__menu-hint {
|
|
388
|
+
@include font-xs;
|
|
389
|
+
|
|
390
|
+
color: $library-upload-menu-value;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.library-upload-card .ui-dropdown__item.--disabled {
|
|
394
|
+
opacity: 0.4;
|
|
395
|
+
cursor: not-allowed;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.ui-dropdown__menu-items > .library-upload-card__delete.ui-dropdown__item {
|
|
399
|
+
color: $library-upload-delete-text;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.ui-dropdown__menu-items > .library-upload-card__delete.ui-dropdown__item .ui-icon {
|
|
403
|
+
color: inherit;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.library-upload-card__radio {
|
|
407
|
+
position: relative;
|
|
408
|
+
flex-shrink: 0;
|
|
409
|
+
width: 16px;
|
|
410
|
+
height: 16px;
|
|
411
|
+
border: 1px solid $library-upload-radio;
|
|
412
|
+
border-radius: $radius-full;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.library-upload-card__radio.--checked {
|
|
416
|
+
background-color: $library-upload-radio--checked;
|
|
417
|
+
border-color: $library-upload-radio--checked;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.library-upload-card__radio.--checked::after {
|
|
421
|
+
position: absolute;
|
|
422
|
+
inset: 0;
|
|
423
|
+
width: 6px;
|
|
424
|
+
height: 6px;
|
|
425
|
+
margin: auto;
|
|
426
|
+
background-color: $library-upload-radio-dot;
|
|
427
|
+
border-radius: $radius-full;
|
|
428
|
+
content: '';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.library-upload-card__meta {
|
|
432
|
+
@include font-xs;
|
|
433
|
+
|
|
434
|
+
display: flex;
|
|
435
|
+
align-items: center;
|
|
436
|
+
gap: 6px;
|
|
437
|
+
min-width: 0;
|
|
438
|
+
color: $library-upload-meta;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.library-upload-card__niche {
|
|
442
|
+
flex-shrink: 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.library-upload-card__meta-text.--error {
|
|
446
|
+
color: $library-upload-meta--error;
|
|
447
|
+
}
|
|
448
|
+
</style>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<!-- Grid of the user's uploads with the card menu actions: delete confirmation, title-fix popup, copy link, visibility. Used by «My videos» and its section in the library -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="library-uploads-grid">
|
|
4
|
+
<LibraryUploadCard
|
|
5
|
+
v-for="upload in uploads"
|
|
6
|
+
:key="upload.id"
|
|
7
|
+
:upload="upload"
|
|
8
|
+
@copy="copyLink(upload)"
|
|
9
|
+
@fix="openFix(upload)"
|
|
10
|
+
@delete="deleteTarget = upload"
|
|
11
|
+
@visibility="setVisibility(upload.id, $event)"
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
<UiConfirm
|
|
15
|
+
:model-value="!!deleteTarget"
|
|
16
|
+
:title="t('my_uploads.delete_title')"
|
|
17
|
+
:confirm-text="t('my_uploads.menu.delete')"
|
|
18
|
+
@update:model-value="deleteTarget = null"
|
|
19
|
+
@confirm="confirmDelete"
|
|
20
|
+
>
|
|
21
|
+
<p class="library-uploads-grid__text">{{ t('my_uploads.delete_text') }}</p>
|
|
22
|
+
</UiConfirm>
|
|
23
|
+
|
|
24
|
+
<transition mode="out-in">
|
|
25
|
+
<UiPopup
|
|
26
|
+
v-if="fixTarget"
|
|
27
|
+
:model-value="!!fixTarget"
|
|
28
|
+
class="library-uploads-grid__fix"
|
|
29
|
+
sheet
|
|
30
|
+
@update:model-value="fixTarget = null"
|
|
31
|
+
>
|
|
32
|
+
<template #title>{{ t('my_uploads.fix_title') }}</template>
|
|
33
|
+
<form
|
|
34
|
+
:id="`${id}-fix`"
|
|
35
|
+
class="library-uploads-grid__fix-form"
|
|
36
|
+
@submit.prevent="sendFix"
|
|
37
|
+
>
|
|
38
|
+
<p class="library-uploads-grid__text">{{ t('my_uploads.fix_text') }}</p>
|
|
39
|
+
<UiInput
|
|
40
|
+
v-model="fixTitle"
|
|
41
|
+
:placeholder="t('my_uploads.fix_placeholder')"
|
|
42
|
+
maxlength="100"
|
|
43
|
+
autofocus
|
|
44
|
+
/>
|
|
45
|
+
</form>
|
|
46
|
+
<template #footer>
|
|
47
|
+
<div class="library-uploads-grid__fix-buttons">
|
|
48
|
+
<UiBtn
|
|
49
|
+
size="l"
|
|
50
|
+
theme="muted"
|
|
51
|
+
wide
|
|
52
|
+
@click="fixTarget = null"
|
|
53
|
+
>{{ t('cancel') }}
|
|
54
|
+
</UiBtn>
|
|
55
|
+
<UiBtn
|
|
56
|
+
:disabled="!fixTitle.trim()"
|
|
57
|
+
:form="`${id}-fix`"
|
|
58
|
+
size="l"
|
|
59
|
+
theme="primary"
|
|
60
|
+
type="submit"
|
|
61
|
+
wide
|
|
62
|
+
>{{ t('my_uploads.send') }}
|
|
63
|
+
</UiBtn>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
</UiPopup>
|
|
67
|
+
</transition>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup lang="ts">
|
|
72
|
+
import type { IMyUpload } from '../../types';
|
|
73
|
+
|
|
74
|
+
defineProps<{
|
|
75
|
+
uploads: IMyUpload[]
|
|
76
|
+
}>();
|
|
77
|
+
|
|
78
|
+
const { t } = useI18n();
|
|
79
|
+
const { generateLink } = useGenerateLink();
|
|
80
|
+
const { show, showError } = useSnackbar();
|
|
81
|
+
const { setVisibility, remove } = useMyUploads();
|
|
82
|
+
const id = useId();
|
|
83
|
+
|
|
84
|
+
const deleteTarget = ref<IMyUpload | null>(null);
|
|
85
|
+
const fixTarget = ref<IMyUpload | null>(null);
|
|
86
|
+
const fixTitle = ref('');
|
|
87
|
+
|
|
88
|
+
async function copyLink(upload: IMyUpload) {
|
|
89
|
+
const path = generateLink(upload.videoId ? `/videos/${upload.videoId}` : '/library/uploads');
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await navigator.clipboard.writeText(`${window.location.origin}${path}`);
|
|
93
|
+
show('link_copied');
|
|
94
|
+
} catch {
|
|
95
|
+
showError('link_not_copied');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function openFix(upload: IMyUpload) {
|
|
100
|
+
fixTitle.value = upload.title ?? '';
|
|
101
|
+
fixTarget.value = upload;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sendFix() {
|
|
105
|
+
if (!fixTitle.value.trim()) return;
|
|
106
|
+
|
|
107
|
+
fixTarget.value = null;
|
|
108
|
+
show('title_fix_sent');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function confirmDelete() {
|
|
112
|
+
if (deleteTarget.value) remove(deleteTarget.value.id);
|
|
113
|
+
deleteTarget.value = null;
|
|
114
|
+
show('video_deleted');
|
|
115
|
+
}
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<style lang="scss">
|
|
119
|
+
.library-uploads-grid {
|
|
120
|
+
@include video-card-grid;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.library-uploads-grid__text {
|
|
124
|
+
@include font-sm;
|
|
125
|
+
|
|
126
|
+
color: $library-upload-fix-text;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.library-uploads-grid__fix .ui-popup__content {
|
|
130
|
+
padding: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.library-uploads-grid__fix-form {
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-direction: column;
|
|
136
|
+
gap: 16px;
|
|
137
|
+
padding: 24px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.library-uploads-grid__fix-buttons {
|
|
141
|
+
display: grid;
|
|
142
|
+
grid-template-columns: 1fr 1fr;
|
|
143
|
+
gap: 12px;
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<!-- «My videos»: status tabs with counts and the grid of the user's uploads. Data is a stub (useMyUploads) -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="library-uploads">
|
|
4
|
+
<h1 class="_title library-uploads__title">{{ t('my_uploads.title') }}</h1>
|
|
5
|
+
|
|
6
|
+
<div
|
|
7
|
+
v-if="!uploads.length"
|
|
8
|
+
class="library-uploads__empty"
|
|
9
|
+
>
|
|
10
|
+
<span
|
|
11
|
+
class="library-uploads__empty-icon"
|
|
12
|
+
aria-hidden="true"
|
|
13
|
+
>
|
|
14
|
+
<UiIcon
|
|
15
|
+
name="upload-arrow"
|
|
16
|
+
size="24"
|
|
17
|
+
/>
|
|
18
|
+
</span>
|
|
19
|
+
<p class="library-uploads__empty-title">{{ t('my_uploads.empty') }}</p>
|
|
20
|
+
<UiLink
|
|
21
|
+
:to="generateLink('/upload')"
|
|
22
|
+
size="l"
|
|
23
|
+
theme="primary"
|
|
24
|
+
>{{ t('my_uploads.upload_first') }}
|
|
25
|
+
</UiLink>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<template v-else>
|
|
29
|
+
<div
|
|
30
|
+
:aria-label="t('my_uploads.filter_label')"
|
|
31
|
+
class="library-uploads__tabs"
|
|
32
|
+
role="tablist"
|
|
33
|
+
>
|
|
34
|
+
<UiBtn
|
|
35
|
+
v-for="tab in tabs"
|
|
36
|
+
:key="tab.id"
|
|
37
|
+
:active="tab.id === activeTab"
|
|
38
|
+
:aria-selected="tab.id === activeTab"
|
|
39
|
+
class="library-uploads__tab"
|
|
40
|
+
role="tab"
|
|
41
|
+
theme="secondary"
|
|
42
|
+
@click="activeTab = tab.id"
|
|
43
|
+
>
|
|
44
|
+
{{ t(`my_uploads.tabs.${tab.id}`) }}
|
|
45
|
+
<span class="library-uploads__tab-count">{{ tab.count }}</span>
|
|
46
|
+
</UiBtn>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<LibraryUploadsGrid
|
|
50
|
+
v-if="filtered.length"
|
|
51
|
+
:uploads="filtered"
|
|
52
|
+
/>
|
|
53
|
+
<p
|
|
54
|
+
v-else
|
|
55
|
+
class="library-uploads__note"
|
|
56
|
+
>{{ t('my_uploads.no_status') }}</p>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
import { MyUploadStatus } from '../../runtime';
|
|
64
|
+
import type { IMyUpload } from '../../types';
|
|
65
|
+
|
|
66
|
+
const TABS = [
|
|
67
|
+
{ id: 'all', match: () => true },
|
|
68
|
+
{ id: 'published', match: (upload: IMyUpload) => upload.status === MyUploadStatus.Published },
|
|
69
|
+
{ id: 'processing', match: (upload: IMyUpload) => upload.status === MyUploadStatus.Processing || upload.status === MyUploadStatus.Review },
|
|
70
|
+
{ id: 'declined', match: (upload: IMyUpload) => upload.status === MyUploadStatus.Declined },
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
type TabId = typeof TABS[number]['id'];
|
|
74
|
+
|
|
75
|
+
const { t } = useI18n();
|
|
76
|
+
const { generateLink } = useGenerateLink();
|
|
77
|
+
const { uploads } = useMyUploads();
|
|
78
|
+
|
|
79
|
+
const activeTab = ref<TabId>('all');
|
|
80
|
+
|
|
81
|
+
const tabs = computed(() => TABS.map(tab => ({ id: tab.id, count: uploads.value.filter(tab.match).length })));
|
|
82
|
+
|
|
83
|
+
const filtered = computed(() => {
|
|
84
|
+
const tab = TABS.find(item => item.id === activeTab.value) ?? TABS[0];
|
|
85
|
+
|
|
86
|
+
return uploads.value.filter(tab.match);
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<style lang="scss">
|
|
91
|
+
.library-uploads__title {
|
|
92
|
+
margin-bottom: 16px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.library-uploads__tabs {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-wrap: wrap;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 8px;
|
|
100
|
+
margin-bottom: 32px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.library-uploads__tab-count {
|
|
104
|
+
color: $library-upload-tab-count;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.library-uploads__tab.--active .library-uploads__tab-count {
|
|
108
|
+
color: $library-upload-tab-count--active;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.library-uploads__note {
|
|
112
|
+
@include font-sm;
|
|
113
|
+
|
|
114
|
+
padding-block: 64px;
|
|
115
|
+
text-align: center;
|
|
116
|
+
color: $library-upload-note;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.library-uploads__empty {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 16px;
|
|
125
|
+
padding-block: 96px;
|
|
126
|
+
text-align: center;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.library-uploads__empty-icon {
|
|
130
|
+
display: grid;
|
|
131
|
+
place-content: center;
|
|
132
|
+
width: 56px;
|
|
133
|
+
height: 56px;
|
|
134
|
+
color: $library-upload-empty-icon;
|
|
135
|
+
background-color: $library-upload-empty-icon-bg;
|
|
136
|
+
border: 1px solid $library-upload-empty-icon-border;
|
|
137
|
+
border-radius: $radius-2xl;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.library-uploads__empty-title {
|
|
141
|
+
@include font-lg;
|
|
142
|
+
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
color: $library-upload-empty-title;
|
|
145
|
+
}
|
|
146
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { MyUploadStatus } from '../runtime';
|
|
2
|
+
import type { UploadVisibility } from '../runtime';
|
|
3
|
+
import type { IMyUpload } from '../types';
|
|
4
|
+
import { getUploadsStub } from '../stubs/uploads';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The user's uploaded videos («My videos»). No API yet — a stub list kept in state, so visibility changes and
|
|
8
|
+
* deletions live until reload. Swap the stub and the two mutations for requests when the endpoints land.
|
|
9
|
+
*/
|
|
10
|
+
export function useMyUploads() {
|
|
11
|
+
const uploads = useState<IMyUpload[]>('my-uploads', () => getUploadsStub());
|
|
12
|
+
|
|
13
|
+
function canChangeVisibility(upload: IMyUpload) {
|
|
14
|
+
return upload.status !== MyUploadStatus.Processing && upload.status !== MyUploadStatus.Declined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function setVisibility(id: string, visibility: UploadVisibility) {
|
|
18
|
+
uploads.value = uploads.value.map(upload => upload.id === id ? { ...upload, visibility } : upload);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function remove(id: string) {
|
|
22
|
+
uploads.value = uploads.value.filter(upload => upload.id !== id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { uploads, canChangeVisibility, setVisibility, remove };
|
|
26
|
+
}
|
package/i18n/locales/en-US.json
CHANGED
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
+
"my_uploads": {
|
|
3
|
+
"title": "My videos",
|
|
4
|
+
"filter_label": "Filter by status",
|
|
5
|
+
"tabs": {
|
|
6
|
+
"all": "All",
|
|
7
|
+
"published": "Published",
|
|
8
|
+
"processing": "Processing",
|
|
9
|
+
"declined": "Not approved"
|
|
10
|
+
},
|
|
11
|
+
"status": {
|
|
12
|
+
"processing": "Processing",
|
|
13
|
+
"review": "In review",
|
|
14
|
+
"declined": "Not approved"
|
|
15
|
+
},
|
|
16
|
+
"meta": {
|
|
17
|
+
"processing": "AI processing · up to 30 min",
|
|
18
|
+
"review": "In review · up to 24 h",
|
|
19
|
+
"declined": "Not approved",
|
|
20
|
+
"published": "{views} · {ago}"
|
|
21
|
+
},
|
|
22
|
+
"visibility": {
|
|
23
|
+
"unlisted": "Unlisted",
|
|
24
|
+
"private": "Private"
|
|
25
|
+
},
|
|
26
|
+
"menu": {
|
|
27
|
+
"label": "Video options",
|
|
28
|
+
"visibility": "Visibility",
|
|
29
|
+
"copy_link": "Copy link",
|
|
30
|
+
"suggest_fix": "Suggest a title fix",
|
|
31
|
+
"delete": "Delete"
|
|
32
|
+
},
|
|
33
|
+
"delete_title": "Delete this video?",
|
|
34
|
+
"delete_text": "It will be removed from the site. This can't be undone.",
|
|
35
|
+
"fix_title": "Suggest a title fix",
|
|
36
|
+
"fix_text": "Our team will review it before it goes live.",
|
|
37
|
+
"fix_placeholder": "New title",
|
|
38
|
+
"send": "Send",
|
|
39
|
+
"empty": "No videos yet",
|
|
40
|
+
"upload_first": "Upload your first video",
|
|
41
|
+
"no_status": "No videos with this status"
|
|
42
|
+
},
|
|
2
43
|
"upload_page": {
|
|
3
44
|
"title_pick": "Upload video",
|
|
4
45
|
"title_details": "Video details",
|
|
@@ -476,6 +517,9 @@
|
|
|
476
517
|
"hide_description": "Hide description",
|
|
477
518
|
"advertising_banner": "Advertising",
|
|
478
519
|
"snackbar": {
|
|
520
|
+
"link_copied": "Link copied",
|
|
521
|
+
"link_not_copied": "Couldn't copy the link",
|
|
522
|
+
"title_fix_sent": "Thanks, we'll review your title fix",
|
|
479
523
|
"added": "Added:",
|
|
480
524
|
"removed": "Removed:",
|
|
481
525
|
"added_to": "Added to:",
|
package/package.json
CHANGED
package/pages/library/index.vue
CHANGED
|
@@ -56,6 +56,33 @@
|
|
|
56
56
|
</div>
|
|
57
57
|
</section>
|
|
58
58
|
|
|
59
|
+
<section
|
|
60
|
+
v-if="myUploads.length > 0"
|
|
61
|
+
class="library__section"
|
|
62
|
+
>
|
|
63
|
+
<header class="library__section-header">
|
|
64
|
+
<UiSectionTitle
|
|
65
|
+
:title="t('my_uploads.title')"
|
|
66
|
+
icon="upload-arrow"
|
|
67
|
+
title-tag="h2"
|
|
68
|
+
variant="section"
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
<UiLink
|
|
72
|
+
:to="generateLink('/library/uploads')"
|
|
73
|
+
theme="simple"
|
|
74
|
+
>
|
|
75
|
+
{{ t('show_all') }}
|
|
76
|
+
<UiIcon
|
|
77
|
+
name="arrow-right"
|
|
78
|
+
size="16"
|
|
79
|
+
/>
|
|
80
|
+
</UiLink>
|
|
81
|
+
</header>
|
|
82
|
+
|
|
83
|
+
<LibraryUploadsGrid :uploads="myUploads"/>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
59
86
|
<section class="library__section">
|
|
60
87
|
<header class="library__section-header">
|
|
61
88
|
<UiSectionTitle
|
|
@@ -153,6 +180,7 @@ type LibraryLink = {
|
|
|
153
180
|
};
|
|
154
181
|
|
|
155
182
|
const CONTINUE_LIMIT = 4;
|
|
183
|
+
const MY_UPLOADS_LIMIT = 4;
|
|
156
184
|
|
|
157
185
|
/** Сколько аватарок помещается в ряд «что внутри» — остальные уходят в плашку «+N». */
|
|
158
186
|
const AVATARS_LIMIT = 4;
|
|
@@ -162,6 +190,9 @@ const featureFlags = useRuntimeConfig().public.featureFlags;
|
|
|
162
190
|
|
|
163
191
|
const { generateLink } = useGenerateLink();
|
|
164
192
|
|
|
193
|
+
const { uploads } = useMyUploads();
|
|
194
|
+
const myUploads = computed(() => featureFlags.FeatureUploadEnabled ? uploads.value.slice(0, MY_UPLOADS_LIMIT) : []);
|
|
195
|
+
|
|
165
196
|
const saveHistory = ref(true);
|
|
166
197
|
|
|
167
198
|
const removedGuids = ref<string[]>([]);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!-- My videos: the user's uploads with status and visibility, behind the FeatureUploadEnabled flag. Data is a stub -->
|
|
2
|
+
<template>
|
|
3
|
+
<div class="_page _container">
|
|
4
|
+
<UiBreadcrumbs
|
|
5
|
+
:items="breadcrumbs"
|
|
6
|
+
class="_breadcrumbs"
|
|
7
|
+
/>
|
|
8
|
+
|
|
9
|
+
<LibraryUploads/>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import type { IBreadcrumbItem } from '../../types';
|
|
15
|
+
|
|
16
|
+
definePageMeta({ featureFlag: 'FeatureUploadEnabled' });
|
|
17
|
+
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
|
|
20
|
+
useNoindex();
|
|
21
|
+
|
|
22
|
+
const { brandName } = useAppConfig();
|
|
23
|
+
|
|
24
|
+
useHead(() => ({
|
|
25
|
+
title: `${t('my_uploads.title')} — ${brandName}`,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const breadcrumbs = [
|
|
29
|
+
{
|
|
30
|
+
name: `${t('navigation.main')}`,
|
|
31
|
+
to: '/',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: `${t('navigation.library')}`,
|
|
35
|
+
to: '/library',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: `${t('my_uploads.title')}`,
|
|
39
|
+
},
|
|
40
|
+
] as IBreadcrumbItem[];
|
|
41
|
+
</script>
|
|
@@ -59,3 +59,12 @@ export const UPLOAD_LIMITS = {
|
|
|
59
59
|
maxTags: 15,
|
|
60
60
|
dailyUploads: 10,
|
|
61
61
|
} as const;
|
|
62
|
+
|
|
63
|
+
export const MyUploadStatus = {
|
|
64
|
+
Processing: 'processing',
|
|
65
|
+
Review: 'review',
|
|
66
|
+
Declined: 'declined',
|
|
67
|
+
Published: 'published',
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
export type MyUploadStatus = typeof MyUploadStatus[keyof typeof MyUploadStatus];
|
package/stubs/uploads.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { MyUploadStatus, Niche, UploadVisibility } from '../runtime';
|
|
2
|
+
import type { IMyUpload } from '../types';
|
|
3
|
+
import { VIDEO_POOL } from './videos';
|
|
4
|
+
|
|
5
|
+
const DAY = 24 * 3600;
|
|
6
|
+
|
|
7
|
+
export function getUploadsStub(now = Math.floor(Date.now() / 1000)): IMyUpload[] {
|
|
8
|
+
const [lake, roommates, road, cabin, gym] = [VIDEO_POOL[0]!, VIDEO_POOL[4]!, VIDEO_POOL[8]!, VIDEO_POOL[2]!, VIDEO_POOL[6]!];
|
|
9
|
+
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
id: 'upload-1',
|
|
13
|
+
fileName: 'IMG_4821_final.mp4',
|
|
14
|
+
duration: 872,
|
|
15
|
+
status: MyUploadStatus.Processing,
|
|
16
|
+
visibility: UploadVisibility.Public,
|
|
17
|
+
niche: Niche.Gay,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'upload-2',
|
|
21
|
+
fileName: 'cabin_weekend.mov',
|
|
22
|
+
title: 'Cabin weekend with my boyfriend, slow morning in bed',
|
|
23
|
+
duration: cabin.duration,
|
|
24
|
+
thumbUrls: cabin.thumbUrls,
|
|
25
|
+
status: MyUploadStatus.Review,
|
|
26
|
+
visibility: UploadVisibility.Unlisted,
|
|
27
|
+
niche: Niche.Gay,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'upload-3',
|
|
31
|
+
fileName: 'gym_shower_v2.mp4',
|
|
32
|
+
title: 'Gym shower after leg day',
|
|
33
|
+
duration: gym.duration,
|
|
34
|
+
thumbUrls: gym.thumbUrls,
|
|
35
|
+
status: MyUploadStatus.Declined,
|
|
36
|
+
visibility: UploadVisibility.Public,
|
|
37
|
+
declineReason: 'Watermark from another site is visible in the corner of the video',
|
|
38
|
+
niche: Niche.Gay,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'upload-4',
|
|
42
|
+
fileName: 'lake_house.mp4',
|
|
43
|
+
title: lake.title,
|
|
44
|
+
duration: lake.duration,
|
|
45
|
+
thumbUrls: lake.thumbUrls,
|
|
46
|
+
videoId: lake.id,
|
|
47
|
+
status: MyUploadStatus.Published,
|
|
48
|
+
visibility: UploadVisibility.Public,
|
|
49
|
+
views: 1420,
|
|
50
|
+
publishedAt: now - 2 * DAY,
|
|
51
|
+
niche: Niche.Gay,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'upload-5',
|
|
55
|
+
fileName: 'roommates_night.mp4',
|
|
56
|
+
title: roommates.title,
|
|
57
|
+
duration: roommates.duration,
|
|
58
|
+
thumbUrls: roommates.thumbUrls,
|
|
59
|
+
videoId: roommates.id,
|
|
60
|
+
status: MyUploadStatus.Published,
|
|
61
|
+
visibility: UploadVisibility.Private,
|
|
62
|
+
views: 18240,
|
|
63
|
+
publishedAt: now - 14 * DAY,
|
|
64
|
+
niche: Niche.Gay,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'upload-6',
|
|
68
|
+
fileName: 'road_trip_motel.mp4',
|
|
69
|
+
title: road.title,
|
|
70
|
+
duration: road.duration,
|
|
71
|
+
thumbUrls: road.thumbUrls,
|
|
72
|
+
videoId: road.id,
|
|
73
|
+
status: MyUploadStatus.Published,
|
|
74
|
+
visibility: UploadVisibility.Unlisted,
|
|
75
|
+
views: 640,
|
|
76
|
+
publishedAt: now - 31 * DAY,
|
|
77
|
+
niche: Niche.Gay,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
package/types/upload.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { UploadCheck, UploadCheckStatus, UploadVisibility } from '../runtime';
|
|
1
|
+
import type { MyUploadStatus, Niche, UploadCheck, UploadCheckStatus, UploadVisibility } from '../runtime';
|
|
2
|
+
import type { IThumbUrls } from './thumbs-urls';
|
|
2
3
|
|
|
3
4
|
export interface IUploadFile {
|
|
4
5
|
name: string;
|
|
@@ -38,3 +39,18 @@ export interface IUploadComboboxChip {
|
|
|
38
39
|
label: string;
|
|
39
40
|
image?: string | null;
|
|
40
41
|
}
|
|
42
|
+
|
|
43
|
+
export interface IMyUpload {
|
|
44
|
+
id: string;
|
|
45
|
+
fileName: string;
|
|
46
|
+
title?: string;
|
|
47
|
+
duration: number;
|
|
48
|
+
status: MyUploadStatus;
|
|
49
|
+
visibility: UploadVisibility;
|
|
50
|
+
niche: Niche;
|
|
51
|
+
thumbUrls?: IThumbUrls;
|
|
52
|
+
videoId?: string;
|
|
53
|
+
declineReason?: string;
|
|
54
|
+
views?: number;
|
|
55
|
+
publishedAt?: number;
|
|
56
|
+
}
|