mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 09:49:20 +02:00
Redesign manage my videos
* Use a table to list my videos for a clearer overview and so it's easier to add bulk actions in the future * Use a "Manage" video page with a proper URL navigation * Add "Stats" and "Studio" in this "Manage" page
This commit is contained in:
parent
f0f44e1704
commit
b295dd5820
342 changed files with 9452 additions and 6376 deletions
|
@ -61,7 +61,8 @@
|
|||
"exportDeclaration.forceMultiLine": "never",
|
||||
"importDeclaration.forceMultiLine": "never",
|
||||
"arrayExpression.spaceAround": true,
|
||||
"arrayPattern.spaceAround": true
|
||||
"arrayPattern.spaceAround": true,
|
||||
"importDeclaration.preferSingleLine": true
|
||||
},
|
||||
"json": {},
|
||||
"markdown": {},
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
import { browserSleep, getCheckbox, go, isCheckboxSelected } from '../utils'
|
||||
import { browserSleep, go, setCheckboxEnabled } from '../utils'
|
||||
|
||||
export class AdminConfigPage {
|
||||
|
||||
async navigateTo (tab: 'instance-homepage' | 'basic-configuration' | 'instance-information') {
|
||||
async navigateTo (tab: 'instance-homepage' | 'basic-configuration' | 'instance-information' | 'live') {
|
||||
const waitTitles = {
|
||||
'instance-homepage': 'INSTANCE HOMEPAGE',
|
||||
'basic-configuration': 'APPEARANCE',
|
||||
'instance-information': 'INSTANCE'
|
||||
'instance-information': 'INSTANCE',
|
||||
'live': 'LIVE'
|
||||
}
|
||||
|
||||
const url = '/admin/settings/config/edit-custom#' + tab
|
||||
|
||||
if (await browser.getUrl() !== url) {
|
||||
await go('/admin/settings/config/edit-custom#' + tab)
|
||||
}
|
||||
await go('/admin/settings/config/edit-custom#' + tab)
|
||||
|
||||
await $('h2=' + waitTitles[tab]).waitForDisplayed()
|
||||
}
|
||||
|
||||
async updateNSFWSetting (newValue: 'do_not_list' | 'blur' | 'display') {
|
||||
await this.navigateTo('instance-information')
|
||||
|
||||
const elem = $('#instanceDefaultNSFWPolicy')
|
||||
|
||||
await elem.waitForDisplayed()
|
||||
|
@ -23,35 +30,34 @@ export class AdminConfigPage {
|
|||
return elem.selectByAttribute('value', newValue)
|
||||
}
|
||||
|
||||
updateHomepage (newValue: string) {
|
||||
async updateHomepage (newValue: string) {
|
||||
await this.navigateTo('instance-homepage')
|
||||
|
||||
return $('#instanceCustomHomepageContent').setValue(newValue)
|
||||
}
|
||||
|
||||
async toggleSignup (enabled: boolean) {
|
||||
if (await isCheckboxSelected('signupEnabled') === enabled) return
|
||||
await this.navigateTo('basic-configuration')
|
||||
|
||||
const checkbox = await getCheckbox('signupEnabled')
|
||||
|
||||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
return setCheckboxEnabled('signupEnabled', enabled)
|
||||
}
|
||||
|
||||
async toggleSignupApproval (required: boolean) {
|
||||
if (await isCheckboxSelected('signupRequiresApproval') === required) return
|
||||
await this.navigateTo('basic-configuration')
|
||||
|
||||
const checkbox = await getCheckbox('signupRequiresApproval')
|
||||
|
||||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
return setCheckboxEnabled('signupRequiresApproval', required)
|
||||
}
|
||||
|
||||
async toggleSignupEmailVerification (required: boolean) {
|
||||
if (await isCheckboxSelected('signupRequiresEmailVerification') === required) return
|
||||
await this.navigateTo('basic-configuration')
|
||||
|
||||
const checkbox = await getCheckbox('signupRequiresEmailVerification')
|
||||
return setCheckboxEnabled('signupRequiresEmailVerification', required)
|
||||
}
|
||||
|
||||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
async toggleLive (enabled: boolean) {
|
||||
await this.navigateTo('live')
|
||||
|
||||
return setCheckboxEnabled('liveEnabled', enabled)
|
||||
}
|
||||
|
||||
async save () {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { getCheckbox, go, selectCustomSelect } from '../utils'
|
||||
|
||||
export class MyAccountPage {
|
||||
|
||||
navigateToMyVideos () {
|
||||
return $('a[href="/my-library/videos"]').click()
|
||||
}
|
||||
|
@ -54,7 +53,7 @@ export class MyAccountPage {
|
|||
// My account Videos
|
||||
|
||||
async removeVideo (name: string) {
|
||||
const container = await this.getVideoElement(name)
|
||||
const container = await this.getVideoRow(name)
|
||||
|
||||
await container.$('my-action-dropdown .dropdown-toggle').click()
|
||||
|
||||
|
@ -76,8 +75,8 @@ export class MyAccountPage {
|
|||
}
|
||||
|
||||
async countVideos (names: string[]) {
|
||||
const elements = await $$('.video').filter(async e => {
|
||||
const t = await e.$('.video-name').getText()
|
||||
const elements = await $$('.video-cell-name .name').filter(async e => {
|
||||
const t = await e.getText()
|
||||
|
||||
return names.some(n => t.includes(n))
|
||||
})
|
||||
|
@ -137,22 +136,16 @@ export class MyAccountPage {
|
|||
|
||||
// My account Videos
|
||||
|
||||
private async getVideoElement (name: string) {
|
||||
const video = async () => {
|
||||
const videos = await $$('.video').filter(async e => {
|
||||
const t = await e.$('.video-name').getText()
|
||||
private async getVideoRow (name: string) {
|
||||
let el = $('.name*=' + name)
|
||||
|
||||
return t.includes(name)
|
||||
})
|
||||
await el.waitForDisplayed()
|
||||
|
||||
return videos[0]
|
||||
while (await el.getTagName() !== 'tr') {
|
||||
el = el.parentElement()
|
||||
}
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
return (await video()).isDisplayed()
|
||||
})
|
||||
|
||||
return video()
|
||||
return el
|
||||
}
|
||||
|
||||
// My account playlists
|
||||
|
|
130
client/e2e/src/po/video-manage.ts
Normal file
130
client/e2e/src/po/video-manage.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
import { clickOnRadio, getCheckbox, go, isRadioSelected, selectCustomSelect } from '../utils'
|
||||
|
||||
export abstract class VideoManage {
|
||||
async clickOnSave () {
|
||||
const button = this.getSaveButton()
|
||||
await button.waitForClickable()
|
||||
await button.click()
|
||||
|
||||
await this.waitForSaved()
|
||||
}
|
||||
|
||||
async clickOnWatch () {
|
||||
// Simulate the click, because the button opens a new tab
|
||||
const button = $('.watch-save > my-button[icon=external-link] a')
|
||||
await button.waitForClickable()
|
||||
|
||||
await go(await button.getAttribute('href'))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async setAsNSFW () {
|
||||
await this.goOnPage('Moderation')
|
||||
|
||||
const checkbox = await getCheckbox('nsfw')
|
||||
await checkbox.waitForClickable()
|
||||
|
||||
return checkbox.click()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async setAsPublic () {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
return selectCustomSelect('privacy', 'Public')
|
||||
}
|
||||
|
||||
async setAsPrivate () {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
return selectCustomSelect('privacy', 'Private')
|
||||
}
|
||||
|
||||
async setAsPasswordProtected (videoPassword: string) {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
selectCustomSelect('privacy', 'Password protected')
|
||||
|
||||
const videoPasswordInput = $('input#videoPassword')
|
||||
await videoPasswordInput.waitForClickable()
|
||||
await videoPasswordInput.clearValue()
|
||||
|
||||
return videoPasswordInput.setValue(videoPassword)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async scheduleUpload () {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
selectCustomSelect('privacy', 'Scheduled')
|
||||
|
||||
const input = this.getScheduleInput()
|
||||
await input.waitForClickable()
|
||||
await input.click()
|
||||
|
||||
const nextDay = $('.p-datepicker-today + td > span')
|
||||
await nextDay.waitForClickable()
|
||||
await nextDay.click()
|
||||
await nextDay.waitForDisplayed({ reverse: true })
|
||||
}
|
||||
|
||||
getScheduleInput () {
|
||||
return $('#schedulePublicationAt input')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async setNormalLive () {
|
||||
await this.goOnPage('Live settings')
|
||||
|
||||
await clickOnRadio('permanentLiveFalse')
|
||||
}
|
||||
|
||||
async setPermanentLive () {
|
||||
await this.goOnPage('Live settings')
|
||||
|
||||
await clickOnRadio('permanentLiveTrue')
|
||||
}
|
||||
|
||||
async getLiveState () {
|
||||
await this.goOnPage('Live settings')
|
||||
|
||||
if (await isRadioSelected('#permanentLiveTrue')) return 'permanent'
|
||||
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async refresh (videoName: string) {
|
||||
await browser.refresh()
|
||||
await browser.waitUntil(async () => {
|
||||
const url = await browser.getUrl()
|
||||
|
||||
return url.includes('/videos/manage')
|
||||
})
|
||||
|
||||
await browser.waitUntil(async () => {
|
||||
return await $('#name').getValue() === videoName
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
protected getSaveButton () {
|
||||
return $('.save-button > button:not([disabled])')
|
||||
}
|
||||
|
||||
protected waitForSaved () {
|
||||
return $('.save-button > button[disabled], my-manage-errors').waitForDisplayed()
|
||||
}
|
||||
|
||||
protected async goOnPage (page: 'Main information' | 'Moderation' | 'Live settings') {
|
||||
const el = $('my-video-manage-container .menu').$('*=' + page)
|
||||
await el.waitForClickable()
|
||||
await el.click()
|
||||
}
|
||||
}
|
61
client/e2e/src/po/video-publish.po.ts
Normal file
61
client/e2e/src/po/video-publish.po.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { join } from 'path'
|
||||
import { VideoManage } from './video-manage'
|
||||
|
||||
export class VideoPublishPage extends VideoManage {
|
||||
async navigateTo (tab?: 'Go live') {
|
||||
const publishButton = await $('.publish-button > a')
|
||||
|
||||
await publishButton.waitForClickable()
|
||||
await publishButton.click()
|
||||
|
||||
await $('.upload-video-container').waitForDisplayed()
|
||||
|
||||
if (tab) {
|
||||
const el = $(`.nav-link*=${tab}`)
|
||||
await el.waitForClickable()
|
||||
await el.click()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async uploadVideo (fixtureName: 'video.mp4' | 'video2.mp4' | 'video3.mp4') {
|
||||
const fileToUpload = join(__dirname, '../../fixtures/' + fixtureName)
|
||||
const fileInputSelector = '.upload-video-container input[type=file]'
|
||||
const parentFileInput = '.upload-video-container .button-file'
|
||||
|
||||
// Avoid sending keys on non visible element
|
||||
await browser.execute(`document.querySelector('${fileInputSelector}').style.opacity = 1`)
|
||||
await browser.execute(`document.querySelector('${parentFileInput}').style.overflow = 'initial'`)
|
||||
|
||||
await browser.pause(1000)
|
||||
|
||||
const elem = await $(fileInputSelector)
|
||||
await elem.chooseFile(fileToUpload)
|
||||
|
||||
// Wait for the upload to finish
|
||||
await this.getSaveButton().waitForClickable()
|
||||
}
|
||||
|
||||
async publishLive () {
|
||||
await $('#permanentLiveTrue').parentElement().click()
|
||||
|
||||
const submit = $('.upload-video-container .primary-button:not([disabled])')
|
||||
await submit.waitForClickable()
|
||||
await submit.click()
|
||||
|
||||
await this.getSaveButton().waitForClickable()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async validSecondStep (videoName: string) {
|
||||
await this.goOnPage('Main information')
|
||||
|
||||
const nameInput = $('input#name')
|
||||
await nameInput.clearValue()
|
||||
await nameInput.setValue(videoName)
|
||||
|
||||
await this.clickOnSave()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
export class VideoUpdatePage {
|
||||
import { VideoManage } from './video-manage'
|
||||
|
||||
export class VideoUpdatePage extends VideoManage {
|
||||
async updateName (videoName: string) {
|
||||
const nameInput = $('input#name')
|
||||
|
||||
|
@ -7,14 +8,4 @@ export class VideoUpdatePage {
|
|||
await nameInput.clearValue()
|
||||
await nameInput.setValue(videoName)
|
||||
}
|
||||
|
||||
async validUpdate () {
|
||||
const submitButton = await this.getSubmitButton()
|
||||
|
||||
return submitButton.click()
|
||||
}
|
||||
|
||||
private getSubmitButton () {
|
||||
return $('.submit-container .action-button')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import { join } from 'path'
|
||||
import { getCheckbox, selectCustomSelect } from '../utils'
|
||||
|
||||
export class VideoUploadPage {
|
||||
async navigateTo () {
|
||||
const publishButton = await $('.publish-button > a')
|
||||
|
||||
await publishButton.waitForClickable()
|
||||
await publishButton.click()
|
||||
|
||||
await $('.upload-video-container').waitForDisplayed()
|
||||
}
|
||||
|
||||
async uploadVideo (fixtureName: 'video.mp4' | 'video2.mp4' | 'video3.mp4') {
|
||||
const fileToUpload = join(__dirname, '../../fixtures/' + fixtureName)
|
||||
const fileInputSelector = '.upload-video-container input[type=file]'
|
||||
const parentFileInput = '.upload-video-container .button-file'
|
||||
|
||||
// Avoid sending keys on non visible element
|
||||
await browser.execute(`document.querySelector('${fileInputSelector}').style.opacity = 1`)
|
||||
await browser.execute(`document.querySelector('${parentFileInput}').style.overflow = 'initial'`)
|
||||
|
||||
await browser.pause(1000)
|
||||
|
||||
const elem = await $(fileInputSelector)
|
||||
await elem.chooseFile(fileToUpload)
|
||||
|
||||
// Wait for the upload to finish
|
||||
await browser.waitUntil(async () => {
|
||||
const warning = await $('=Publish will be available when upload is finished').isDisplayed()
|
||||
const progress = await $('.progress-container=100%').isDisplayed()
|
||||
|
||||
return !warning && progress
|
||||
})
|
||||
}
|
||||
|
||||
async setAsNSFW () {
|
||||
const checkbox = await getCheckbox('nsfw')
|
||||
await checkbox.waitForClickable()
|
||||
|
||||
return checkbox.click()
|
||||
}
|
||||
|
||||
async validSecondUploadStep (videoName: string) {
|
||||
const nameInput = $('input#name')
|
||||
await nameInput.clearValue()
|
||||
await nameInput.setValue(videoName)
|
||||
|
||||
const button = this.getSecondStepSubmitButton()
|
||||
await button.waitForClickable()
|
||||
|
||||
await button.click()
|
||||
|
||||
return browser.waitUntil(async () => {
|
||||
return (await browser.getUrl()).includes('/w/')
|
||||
})
|
||||
}
|
||||
|
||||
setAsPublic () {
|
||||
return selectCustomSelect('privacy', 'Public')
|
||||
}
|
||||
|
||||
setAsPrivate () {
|
||||
return selectCustomSelect('privacy', 'Private')
|
||||
}
|
||||
|
||||
async setAsPasswordProtected (videoPassword: string) {
|
||||
selectCustomSelect('privacy', 'Password protected')
|
||||
|
||||
const videoPasswordInput = $('input#videoPassword')
|
||||
await videoPasswordInput.waitForClickable()
|
||||
await videoPasswordInput.clearValue()
|
||||
|
||||
return videoPasswordInput.setValue(videoPassword)
|
||||
}
|
||||
|
||||
private getSecondStepSubmitButton () {
|
||||
return $('.submit-container my-button')
|
||||
}
|
||||
}
|
|
@ -1,24 +1,16 @@
|
|||
import { browserSleep, FIXTURE_URLS, go } from '../utils'
|
||||
|
||||
export class VideoWatchPage {
|
||||
|
||||
constructor (private isMobileDevice: boolean, private isSafari: boolean) {
|
||||
|
||||
}
|
||||
|
||||
waitWatchVideoName (videoName: string) {
|
||||
waitWatchVideoName (videoName: string, maxTime?: number) {
|
||||
if (this.isSafari) return browserSleep(5000)
|
||||
|
||||
// On mobile we display the first node, on desktop the second one
|
||||
const index = this.isMobileDevice ? 0 : 1
|
||||
|
||||
return browser.waitUntil(async () => {
|
||||
if (!await $('.video-info .video-info-name').isExisting()) return false
|
||||
|
||||
const elem = await $$('.video-info .video-info-name')[index]
|
||||
|
||||
return (await elem.getText()).includes(videoName) && elem.isDisplayed()
|
||||
})
|
||||
return (await this.getVideoName()) === videoName
|
||||
}, { timeout: maxTime })
|
||||
}
|
||||
|
||||
getVideoName () {
|
||||
|
@ -82,16 +74,17 @@ export class VideoWatchPage {
|
|||
return go(FIXTURE_URLS.HLS_PLAYLIST_EMBED)
|
||||
}
|
||||
|
||||
async clickOnUpdate () {
|
||||
async clickOnManage () {
|
||||
await this.clickOnMoreDropdownIcon()
|
||||
|
||||
const items = await $$('.dropdown-menu.show .dropdown-item')
|
||||
|
||||
for (const item of items) {
|
||||
const href = await item.getAttribute('href')
|
||||
const content = await item.getText()
|
||||
|
||||
if (href?.includes('/update/')) {
|
||||
if (content.includes('Manage')) {
|
||||
await item.click()
|
||||
await $('#name').waitForClickable()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -123,12 +116,6 @@ export class VideoWatchPage {
|
|||
return playlist().click()
|
||||
}
|
||||
|
||||
waitUntilVideoName (name: string, maxTime: number) {
|
||||
return browser.waitUntil(async () => {
|
||||
return (await this.getVideoName()) === name
|
||||
}, { timeout: maxTime })
|
||||
}
|
||||
|
||||
async clickOnMoreDropdownIcon () {
|
||||
const dropdown = $('my-video-actions-dropdown .action-button')
|
||||
await dropdown.click()
|
||||
|
|
|
@ -3,7 +3,7 @@ import { MyAccountPage } from '../po/my-account.po'
|
|||
import { PlayerPage } from '../po/player.po'
|
||||
import { VideoListPage } from '../po/video-list.po'
|
||||
import { VideoUpdatePage } from '../po/video-update.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isIOS, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
|
||||
|
@ -19,7 +19,7 @@ function isUploadUnsupported () {
|
|||
describe('Videos all workflow', () => {
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let videoListPage: VideoListPage
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let videoUpdatePage: VideoUpdatePage
|
||||
let myAccountPage: MyAccountPage
|
||||
let loginPage: LoginPage
|
||||
|
@ -46,7 +46,7 @@ describe('Videos all workflow', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoUpdatePage = new VideoUpdatePage()
|
||||
myAccountPage = new MyAccountPage()
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
|
@ -70,10 +70,10 @@ describe('Videos all workflow', () => {
|
|||
it('Should upload a video', async () => {
|
||||
if (isUploadUnsupported()) return
|
||||
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoPublishPage.navigateTo()
|
||||
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
return videoUploadPage.validSecondUploadStep(videoName)
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep(videoName)
|
||||
})
|
||||
|
||||
it('Should list videos', async () => {
|
||||
|
@ -124,12 +124,12 @@ describe('Videos all workflow', () => {
|
|||
|
||||
await go(videoWatchUrl)
|
||||
|
||||
await videoWatchPage.clickOnUpdate()
|
||||
await videoWatchPage.clickOnManage()
|
||||
|
||||
videoName += ' updated'
|
||||
await videoUpdatePage.updateName(videoName)
|
||||
|
||||
await videoUpdatePage.validUpdate()
|
||||
await videoUpdatePage.clickOnSave()
|
||||
await videoUpdatePage.clickOnWatch()
|
||||
|
||||
const name = await videoWatchPage.getVideoName()
|
||||
expect(name).toEqual(videoName)
|
||||
|
@ -145,10 +145,11 @@ describe('Videos all workflow', () => {
|
|||
await videoWatchPage.saveToPlaylist(playlistName)
|
||||
await browser.pause(5000)
|
||||
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoPublishPage.navigateTo()
|
||||
|
||||
await videoUploadPage.uploadVideo('video2.mp4')
|
||||
await videoUploadPage.validSecondUploadStep(video2Name)
|
||||
await videoPublishPage.uploadVideo('video2.mp4')
|
||||
await videoPublishPage.validSecondStep(video2Name)
|
||||
await videoPublishPage.clickOnWatch()
|
||||
|
||||
await videoWatchPage.clickOnSave()
|
||||
await videoWatchPage.saveToPlaylist(playlistName)
|
||||
|
@ -173,7 +174,7 @@ describe('Videos all workflow', () => {
|
|||
|
||||
await myAccountPage.playPlaylist()
|
||||
|
||||
await videoWatchPage.waitUntilVideoName(video2Name, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(video2Name, 40 * 1000)
|
||||
})
|
||||
|
||||
it('Should watch the Web Video playlist in the embed', async () => {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
|
||||
describe('Custom server defaults', () => {
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
|
||||
|
@ -12,7 +12,7 @@ describe('Custom server defaults', () => {
|
|||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
|
@ -24,10 +24,11 @@ describe('Custom server defaults', () => {
|
|||
})
|
||||
|
||||
it('Should upload a video with custom default values', async function () {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoUploadPage.validSecondUploadStep('video')
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
|
||||
const videoUrl = await browser.getUrl()
|
||||
|
@ -66,11 +67,12 @@ describe('Custom server defaults', () => {
|
|||
|
||||
before(async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video2.mp4')
|
||||
await videoUploadPage.setAsPublic()
|
||||
await videoUploadPage.validSecondUploadStep('video')
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video2.mp4')
|
||||
await videoPublishPage.setAsPublic()
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
|
||||
videoUrl = await browser.getUrl()
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { AdminPluginPage } from '../po/admin-plugin.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { getCheckbox, isMobileDevice, waitServerUp } from '../utils'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { getCheckbox, getScreenshotPath, isMobileDevice, waitServerUp } from '../utils'
|
||||
|
||||
describe('Plugins', () => {
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let adminPluginPage: AdminPluginPage
|
||||
|
||||
|
@ -12,11 +12,11 @@ describe('Plugins', () => {
|
|||
return getCheckbox('hello-world-field-4')
|
||||
}
|
||||
|
||||
async function expectSubmitState ({ disabled }: { disabled: boolean }) {
|
||||
const disabledSubmit = await $('my-button [disabled]')
|
||||
async function expectSubmitError (hasError: boolean) {
|
||||
await videoPublishPage.clickOnSave()
|
||||
|
||||
if (disabled) expect(await disabledSubmit.isDisplayed()).toBeTruthy()
|
||||
else expect(await disabledSubmit.isDisplayed()).toBeFalsy()
|
||||
await $('.form-error*=Should be enabled').waitForDisplayed({ reverse: !hasError })
|
||||
await $('li*=Should be enabled').waitForDisplayed({ reverse: !hasError })
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
|
@ -25,7 +25,7 @@ describe('Plugins', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
adminPluginPage = new AdminPluginPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
|
@ -41,15 +41,23 @@ describe('Plugins', () => {
|
|||
})
|
||||
|
||||
it('Should have checkbox in video edit page', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
|
||||
await $('span=Super field 4 in main tab').waitForDisplayed()
|
||||
const el = () => $('span=Super field 4 in main tab')
|
||||
await el().waitForDisplayed()
|
||||
|
||||
// Only displayed if the video is public
|
||||
await videoPublishPage.setAsPrivate()
|
||||
await el().waitForDisplayed({ reverse: true })
|
||||
|
||||
await videoPublishPage.setAsPublic()
|
||||
await el().waitForDisplayed()
|
||||
|
||||
const checkbox = await getPluginCheckbox()
|
||||
expect(await checkbox.isDisplayed()).toBeTruthy()
|
||||
|
||||
await expectSubmitState({ disabled: true })
|
||||
await expectSubmitError(true)
|
||||
})
|
||||
|
||||
it('Should check the checkbox and be able to submit the video', async function () {
|
||||
|
@ -58,7 +66,7 @@ describe('Plugins', () => {
|
|||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
|
||||
await expectSubmitState({ disabled: false })
|
||||
await expectSubmitError(false)
|
||||
})
|
||||
|
||||
it('Should uncheck the checkbox and not be able to submit the video', async function () {
|
||||
|
@ -67,16 +75,16 @@ describe('Plugins', () => {
|
|||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
|
||||
await expectSubmitState({ disabled: true })
|
||||
|
||||
const error = await $('.form-error*=Should be enabled')
|
||||
|
||||
expect(await error.isDisplayed()).toBeTruthy()
|
||||
await expectSubmitError(true)
|
||||
})
|
||||
|
||||
it('Should change the privacy and should hide the checkbox', async function () {
|
||||
await videoUploadPage.setAsPrivate()
|
||||
await videoPublishPage.setAsPrivate()
|
||||
|
||||
await expectSubmitState({ disabled: false })
|
||||
await expectSubmitError(false)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
||||
})
|
||||
})
|
||||
|
|
61
client/e2e/src/suites-local/publish-live.e2e-spec.ts
Normal file
61
client/e2e/src/suites-local/publish-live.e2e-spec.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { AdminConfigPage } from '../po/admin-config.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
|
||||
describe('Publish live', function () {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let adminConfigPage: AdminConfigPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
adminConfigPage = new AdminConfigPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
||||
it('Should enable live', async function () {
|
||||
await adminConfigPage.toggleLive(true)
|
||||
await adminConfigPage.save()
|
||||
})
|
||||
|
||||
it('Should create a classic permanent live', async function () {
|
||||
await videoPublishPage.navigateTo('Go live')
|
||||
|
||||
await videoPublishPage.publishLive()
|
||||
await videoPublishPage.validSecondStep('Permanent live test')
|
||||
|
||||
expect(await videoPublishPage.getLiveState()).toEqual('permanent')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
|
||||
await videoWatchPage.waitWatchVideoName('Permanent live test')
|
||||
})
|
||||
|
||||
it('Should create a permanent live and update it to a normal live', async function () {
|
||||
await videoPublishPage.navigateTo('Go live')
|
||||
|
||||
await videoPublishPage.publishLive()
|
||||
await videoPublishPage.setNormalLive()
|
||||
await videoPublishPage.validSecondStep('Normal live test')
|
||||
await videoPublishPage.clickOnWatch()
|
||||
|
||||
await videoWatchPage.waitWatchVideoName('Normal live test')
|
||||
await videoWatchPage.clickOnManage()
|
||||
|
||||
expect(await videoPublishPage.getLiveState()).toEqual('normal')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
||||
})
|
||||
})
|
56
client/e2e/src/suites-local/publish.e2e-spec.ts
Normal file
56
client/e2e/src/suites-local/publish.e2e-spec.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { getScreenshotPath, isMobileDevice, waitServerUp } from '../utils'
|
||||
|
||||
describe('Publish video', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
})
|
||||
|
||||
describe('Common', function () {
|
||||
before(async function () {
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
||||
it('Should upload a video and on refresh being redirected to the manage page', async function () {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep('first video')
|
||||
|
||||
await videoPublishPage.refresh('first video')
|
||||
})
|
||||
|
||||
it('Should upload a video and schedule upload date', async function () {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
|
||||
await videoPublishPage.scheduleUpload()
|
||||
await videoPublishPage.validSecondStep('scheduled')
|
||||
|
||||
await videoPublishPage.refresh('scheduled')
|
||||
|
||||
// check if works? screenshot
|
||||
expect(videoPublishPage.getScheduleInput()).toBeDisplayed()
|
||||
|
||||
const nextDay = new Date()
|
||||
nextDay.setDate(nextDay.getDate() + 1)
|
||||
|
||||
const inputDate = new Date(await videoPublishPage.getScheduleInput().getValue())
|
||||
expect(nextDay.getDate()).toEqual(inputDate.getDate())
|
||||
expect(nextDay.getMonth()).toEqual(inputDate.getMonth())
|
||||
expect(nextDay.getFullYear()).toEqual(inputDate.getFullYear())
|
||||
})
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath('after-test.png'))
|
||||
})
|
||||
})
|
|
@ -75,7 +75,6 @@ describe('Signup', () => {
|
|||
}) {
|
||||
await loginPage.loginAsRootUser()
|
||||
|
||||
await adminConfigPage.navigateTo('basic-configuration')
|
||||
await adminConfigPage.toggleSignup(options.enabled)
|
||||
|
||||
if (options.enabled) {
|
||||
|
@ -116,9 +115,7 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
describe('Email verification disabled', function () {
|
||||
|
||||
describe('Direct registration', function () {
|
||||
|
||||
it('Should enable signup without approval', async () => {
|
||||
await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: false })
|
||||
|
||||
|
@ -171,7 +168,6 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
describe('Registration with approval', function () {
|
||||
|
||||
it('Should enable signup with approval', async () => {
|
||||
await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: false })
|
||||
|
||||
|
@ -252,7 +248,6 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
describe('Direct registration', function () {
|
||||
|
||||
it('Should enable signup without approval', async () => {
|
||||
await prepareSignup({ enabled: true, requiresApproval: false, requiresEmailVerification: true })
|
||||
|
||||
|
@ -320,7 +315,6 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
describe('Registration with approval', function () {
|
||||
|
||||
it('Should enable signup without approval', async () => {
|
||||
await prepareSignup({ enabled: true, requiresApproval: true, requiresEmailVerification: true })
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { AnonymousSettingsPage } from '../po/anonymous-settings.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
|
||||
describe('User settings', () => {
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let myAccountPage: MyAccountPage
|
||||
|
@ -16,7 +16,7 @@ describe('User settings', () => {
|
|||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
myAccountPage = new MyAccountPage()
|
||||
anonymousSettingsPage = new AnonymousSettingsPage()
|
||||
|
@ -42,10 +42,11 @@ describe('User settings', () => {
|
|||
|
||||
before(async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoUploadPage.validSecondUploadStep('video')
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
|
||||
videoUrl = await browser.getUrl()
|
||||
|
|
|
@ -2,12 +2,12 @@ import { LoginPage } from '../po/login.po'
|
|||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { PlayerPage } from '../po/player.po'
|
||||
import { SignupPage } from '../po/signup.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
|
||||
describe('Password protected videos', () => {
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let signupPage: SignupPage
|
||||
|
@ -44,7 +44,7 @@ describe('Password protected videos', () => {
|
|||
await waitServerUp()
|
||||
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
signupPage = new SignupPage()
|
||||
playerPage = new PlayerPage()
|
||||
|
@ -59,9 +59,12 @@ describe('Password protected videos', () => {
|
|||
})
|
||||
|
||||
it('Should login, upload a public video and save it to a playlist', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoUploadPage.validSecondUploadStep(publicVideoName1)
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.validSecondStep(publicVideoName1)
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName1)
|
||||
|
||||
await videoWatchPage.clickOnSave()
|
||||
|
||||
|
@ -69,15 +72,15 @@ describe('Password protected videos', () => {
|
|||
|
||||
await videoWatchPage.saveToPlaylist(playlistName)
|
||||
await browser.pause(5000)
|
||||
|
||||
})
|
||||
|
||||
it('Should upload a password protected video', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video2.mp4')
|
||||
await videoUploadPage.setAsPasswordProtected(videoPassword)
|
||||
await videoUploadPage.validSecondUploadStep(passwordProtectedVideoName)
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video2.mp4')
|
||||
await videoPublishPage.setAsPasswordProtected(videoPassword)
|
||||
await videoPublishPage.validSecondStep(passwordProtectedVideoName)
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName)
|
||||
|
||||
passwordProtectedVideoUrl = await browser.getUrl()
|
||||
|
@ -89,11 +92,13 @@ describe('Password protected videos', () => {
|
|||
})
|
||||
|
||||
it('Should upload a second public video and save it to playlist', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoPublishPage.navigateTo()
|
||||
|
||||
await videoUploadPage.uploadVideo('video3.mp4')
|
||||
await videoUploadPage.validSecondUploadStep(publicVideoName2)
|
||||
await videoPublishPage.uploadVideo('video3.mp4')
|
||||
await videoPublishPage.validSecondStep(publicVideoName2)
|
||||
await videoPublishPage.clickOnWatch()
|
||||
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName2)
|
||||
await videoWatchPage.clickOnSave()
|
||||
await videoWatchPage.saveToPlaylist(playlistName)
|
||||
})
|
||||
|
@ -143,11 +148,11 @@ describe('Password protected videos', () => {
|
|||
await myAccountPage.clickOnPlaylist(playlistName)
|
||||
await myAccountPage.playPlaylist()
|
||||
|
||||
await videoWatchPage.waitUntilVideoName(publicVideoName1, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName1, 40 * 1000)
|
||||
playlistUrl = await browser.getUrl()
|
||||
|
||||
await videoWatchPage.waitUntilVideoName(passwordProtectedVideoName, 40 * 1000)
|
||||
await videoWatchPage.waitUntilVideoName(publicVideoName2, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(passwordProtectedVideoName, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName2, 40 * 1000)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
|
@ -156,7 +161,6 @@ describe('Password protected videos', () => {
|
|||
})
|
||||
|
||||
describe('Regular users', function () {
|
||||
|
||||
before(async () => {
|
||||
await signupPage.fullSignup({
|
||||
accountInfo: {
|
||||
|
@ -192,7 +196,7 @@ describe('Password protected videos', () => {
|
|||
it('Should watch the playlist without password protected video', async () => {
|
||||
await go(playlistUrl)
|
||||
await playerPage.playVideo()
|
||||
await videoWatchPage.waitUntilVideoName(publicVideoName2, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName2, 40 * 1000)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
|
@ -222,7 +226,7 @@ describe('Password protected videos', () => {
|
|||
it('Should watch the playlist without password protected video', async () => {
|
||||
await go(playlistUrl)
|
||||
await playerPage.playVideo()
|
||||
await videoWatchPage.waitUntilVideoName(publicVideoName2, 40 * 1000)
|
||||
await videoWatchPage.waitWatchVideoName(publicVideoName2, 40 * 1000)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -3,14 +3,14 @@ import { LoginPage } from '../po/login.po'
|
|||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { VideoListPage } from '../po/video-list.po'
|
||||
import { VideoSearchPage } from '../po/video-search.po'
|
||||
import { VideoUploadPage } from '../po/video-upload.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { NSFWPolicy } from '../types/common'
|
||||
import { isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
|
||||
describe('Videos list', () => {
|
||||
let videoListPage: VideoListPage
|
||||
let videoUploadPage: VideoUploadPage
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let adminConfigPage: AdminConfigPage
|
||||
let loginPage: LoginPage
|
||||
let myAccountPage: MyAccountPage
|
||||
|
@ -87,7 +87,6 @@ describe('Videos list', () => {
|
|||
}
|
||||
|
||||
async function updateAdminNSFW (nsfw: NSFWPolicy) {
|
||||
await adminConfigPage.navigateTo('instance-information')
|
||||
await adminConfigPage.updateNSFWSetting(nsfw)
|
||||
await adminConfigPage.save()
|
||||
}
|
||||
|
@ -105,7 +104,7 @@ describe('Videos list', () => {
|
|||
videoListPage = new VideoListPage(isMobileDevice(), isSafari())
|
||||
adminConfigPage = new AdminConfigPage()
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoUploadPage = new VideoUploadPage()
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
myAccountPage = new MyAccountPage()
|
||||
videoSearchPage = new VideoSearchPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
@ -119,20 +118,19 @@ describe('Videos list', () => {
|
|||
})
|
||||
|
||||
it('Should set the homepage', async () => {
|
||||
await adminConfigPage.navigateTo('instance-homepage')
|
||||
await adminConfigPage.updateHomepage('<peertube-videos-list data-sort="-publishedAt"></peertube-videos-list>')
|
||||
await adminConfigPage.save()
|
||||
})
|
||||
|
||||
it('Should upload 2 videos (NSFW and classic videos)', async () => {
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video.mp4')
|
||||
await videoUploadPage.setAsNSFW()
|
||||
await videoUploadPage.validSecondUploadStep(nsfwVideo)
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video.mp4')
|
||||
await videoPublishPage.setAsNSFW()
|
||||
await videoPublishPage.validSecondStep(nsfwVideo)
|
||||
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video2.mp4')
|
||||
await videoUploadPage.validSecondUploadStep(normalVideo)
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video2.mp4')
|
||||
await videoPublishPage.validSecondStep(normalVideo)
|
||||
})
|
||||
|
||||
it('Should logout', async function () {
|
||||
|
@ -140,7 +138,6 @@ describe('Videos list', () => {
|
|||
})
|
||||
|
||||
describe('Anonymous users', function () {
|
||||
|
||||
it('Should correctly handle do not list', async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
await updateAdminNSFW('do_not_list')
|
||||
|
@ -170,7 +167,6 @@ describe('Videos list', () => {
|
|||
})
|
||||
|
||||
describe('Logged in users', function () {
|
||||
|
||||
before(async () => {
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
@ -199,13 +195,13 @@ describe('Videos list', () => {
|
|||
})
|
||||
|
||||
describe('Default upload values', function () {
|
||||
|
||||
it('Should have default video values', async function () {
|
||||
await loginPage.loginAsRootUser()
|
||||
await videoUploadPage.navigateTo()
|
||||
await videoUploadPage.uploadVideo('video3.mp4')
|
||||
await videoUploadPage.validSecondUploadStep('video')
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video3.mp4')
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
|
||||
expect(await videoWatchPage.getPrivacy()).toBe('Public')
|
||||
|
|
|
@ -1,15 +1,41 @@
|
|||
async function getCheckbox (name: string) {
|
||||
export async function getCheckbox (name: string) {
|
||||
const input = $(`my-peertube-checkbox input[id=${name}]`)
|
||||
await input.waitForExist()
|
||||
|
||||
return input.parentElement()
|
||||
}
|
||||
|
||||
function isCheckboxSelected (name: string) {
|
||||
export function isCheckboxSelected (name: string) {
|
||||
return $(`input[id=${name}]`).isSelected()
|
||||
}
|
||||
|
||||
async function selectCustomSelect (id: string, valueLabel: string) {
|
||||
export async function setCheckboxEnabled (name: string, enabled: boolean) {
|
||||
if (await isCheckboxSelected(name) === enabled) return
|
||||
|
||||
const checkbox = await getCheckbox(name)
|
||||
|
||||
await checkbox.waitForClickable()
|
||||
await checkbox.click()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function isRadioSelected (name: string) {
|
||||
await $(`input[id=${name}] + label`).waitForClickable()
|
||||
|
||||
return $(`input[id=${name}]`).isSelected()
|
||||
}
|
||||
|
||||
export async function clickOnRadio (name: string) {
|
||||
const label = $(`input[id=${name}] + label`)
|
||||
|
||||
await label.waitForClickable()
|
||||
await label.click()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function selectCustomSelect (id: string, valueLabel: string) {
|
||||
const wrapper = $(`[formcontrolname=${id}] span[role=combobox]`)
|
||||
|
||||
await wrapper.waitForClickable()
|
||||
|
@ -26,7 +52,7 @@ async function selectCustomSelect (id: string, valueLabel: string) {
|
|||
return option.click()
|
||||
}
|
||||
|
||||
async function findParentElement (
|
||||
export async function findParentElement (
|
||||
el: WebdriverIO.Element,
|
||||
finder: (el: WebdriverIO.Element) => Promise<boolean>
|
||||
) {
|
||||
|
@ -34,10 +60,3 @@ async function findParentElement (
|
|||
|
||||
return findParentElement(await el.parentElement(), finder)
|
||||
}
|
||||
|
||||
export {
|
||||
getCheckbox,
|
||||
isCheckboxSelected,
|
||||
selectCustomSelect,
|
||||
findParentElement
|
||||
}
|
||||
|
|
|
@ -32,6 +32,15 @@ module.exports = {
|
|||
prefs
|
||||
}
|
||||
}
|
||||
// {
|
||||
// 'browserName': 'firefox',
|
||||
// 'moz:firefoxOptions': {
|
||||
// binary: '/usr/bin/firefox-developer-edition',
|
||||
// args: [ '--headless', windowSizeArg ],
|
||||
|
||||
// prefs
|
||||
// }
|
||||
// }
|
||||
],
|
||||
|
||||
services: [ 'shared-store' ],
|
||||
|
|
|
@ -117,6 +117,7 @@
|
|||
"tinykeys": "^2.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.4.0",
|
||||
"type-fest": "^4.37.0",
|
||||
"typescript": "~5.7.3",
|
||||
"video.js": "^7.19.2",
|
||||
"vite": "^6.0.11",
|
||||
|
|
|
@ -439,23 +439,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
this.router.navigate([], { fragment: this.activeNav })
|
||||
}
|
||||
|
||||
grabAllErrors (errorObjectArg?: any) {
|
||||
const errorObject = errorObjectArg || this.formErrors
|
||||
|
||||
let acc: string[] = []
|
||||
|
||||
for (const key of Object.keys(errorObject)) {
|
||||
const value = errorObject[key]
|
||||
if (!value) continue
|
||||
|
||||
if (typeof value === 'string') {
|
||||
acc.push(value)
|
||||
} else {
|
||||
acc = acc.concat(this.grabAllErrors(value))
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
grabAllErrors () {
|
||||
return this.formReactiveService.grabAllErrors(this.formErrors)
|
||||
}
|
||||
|
||||
private updateForm () {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
inputId="instanceCustomHomepageContent" formControlName="content"
|
||||
[customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
|
||||
[formError]="formErrors()['instanceCustomHomepage.content']"
|
||||
dir="ltr"
|
||||
dir="ltr" monospace="true"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors().instanceCustomHomepage.content" class="form-error" role="alert">{{ formErrors().instanceCustomHomepage.content }}</div>
|
||||
|
|
|
@ -46,10 +46,13 @@
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<my-video-cell [video]="videoBlock.video">
|
||||
<span name>
|
||||
<my-global-icon *ngIf="videoBlock.type === 2" i18n-title title="The video was blocked due to automatic blocking of new videos" iconName="robot"></my-global-icon>
|
||||
</span>
|
||||
<my-video-cell [video]="videoBlock.video" size="small">
|
||||
<div>
|
||||
<span
|
||||
*ngIf="videoBlock.type === 2"
|
||||
i18n-title title="The video was blocked due to automatic blocking of new videos" class="pt-badge badge-info" i18n
|
||||
>Auto block</span>
|
||||
</div>
|
||||
</my-video-cell>
|
||||
</td>
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import { TableModule } from 'primeng/table'
|
|||
import { switchMap } from 'rxjs/operators'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component'
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { ActionDropdownComponent, DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component'
|
||||
import { AutoColspanDirective } from '../../../shared/shared-main/common/auto-colspan.directive'
|
||||
import { EmbedComponent } from '../../../shared/shared-main/video/embed.component'
|
||||
|
@ -26,7 +25,6 @@ import { VideoCellComponent } from '../../../shared/shared-tables/video-cell.com
|
|||
templateUrl: './video-block-list.component.html',
|
||||
styleUrls: [ '../../../shared/shared-moderation/moderation.scss' ],
|
||||
imports: [
|
||||
GlobalIconComponent,
|
||||
TableModule,
|
||||
SharedModule,
|
||||
AdvancedInputFilterComponent,
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
RestPagination,
|
||||
RestTable
|
||||
} from '@app/core'
|
||||
import { formatICU, getAPIHost } from '@app/helpers'
|
||||
import { formatICU, getBackendHost } from '@app/helpers'
|
||||
import { Actor } from '@app/shared/shared-main/account/actor.model'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { ProgressBarComponent } from '@app/shared/shared-main/common/progress-bar.component'
|
||||
|
@ -373,10 +373,10 @@ export class UserListComponent extends RestTable<User> implements OnInit, OnDest
|
|||
}
|
||||
|
||||
private loadMutedStatus () {
|
||||
this.blocklist.getStatus({ accounts: this.users.map(u => u.username + '@' + getAPIHost()) })
|
||||
this.blocklist.getStatus({ accounts: this.users.map(u => u.username + '@' + getBackendHost()) })
|
||||
.subscribe(blockStatus => {
|
||||
for (const user of this.users) {
|
||||
user.accountMutedStatus.mutedByInstance = blockStatus.accounts[user.username + '@' + getAPIHost()].blockedByServer
|
||||
user.accountMutedStatus.mutedByInstance = blockStatus.accounts[user.username + '@' + getBackendHost()].blockedByServer
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<my-video-cell [video]="video"></my-video-cell>
|
||||
<my-video-cell [video]="video" size="small"></my-video-cell>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
@ -72,7 +72,7 @@
|
|||
<span class="pt-badge badge-purple" i18n>Remote</span>
|
||||
}
|
||||
|
||||
<span [ngClass]="getPrivacyBadgeClass(video)" class="pt-badge">{{ video.privacy.label }}</span>
|
||||
<my-video-privacy-badge [video]="video"></my-video-privacy-badge>
|
||||
|
||||
<span *ngIf="video.nsfw" class="pt-badge badge-red" i18n>NSFW</span>
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@ my-embed {
|
|||
width: 50%;
|
||||
}
|
||||
|
||||
.pt-badge {
|
||||
.pt-badge,
|
||||
my-video-privacy-badge {
|
||||
@include margin-right(5px);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import { VideoBlockComponent } from '@app/shared/shared-moderation/video-block.c
|
|||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { getAllFiles } from '@peertube/peertube-core-utils'
|
||||
import { FileStorage, UserRight, VideoFile, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
import { FileStorage, UserRight, VideoFile, VideoState, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
import { videoRequiresFileToken } from '@root-helpers/video'
|
||||
import { SharedModule, SortMeta } from 'primeng/api'
|
||||
import { TableModule, TableRowExpandEvent } from 'primeng/table'
|
||||
|
@ -31,6 +31,7 @@ import {
|
|||
VideoActionsDisplayType,
|
||||
VideoActionsDropdownComponent
|
||||
} from '../../../shared/shared-video-miniature/video-actions-dropdown.component'
|
||||
import { VideoPrivacyBadgeComponent } from '../../../shared/shared-video/video-privacy-badge.component'
|
||||
import { VideoAdminService } from './video-admin.service'
|
||||
|
||||
@Component({
|
||||
|
@ -56,7 +57,8 @@ import { VideoAdminService } from './video-admin.service'
|
|||
VideoBlockComponent,
|
||||
PTDatePipe,
|
||||
RouterLink,
|
||||
BytesPipe
|
||||
BytesPipe,
|
||||
VideoPrivacyBadgeComponent
|
||||
]
|
||||
})
|
||||
export class VideoListComponent extends RestTable<Video> implements OnInit {
|
||||
|
@ -180,12 +182,6 @@ export class VideoListComponent extends RestTable<Video> implements OnInit {
|
|||
return 'VideoListComponent'
|
||||
}
|
||||
|
||||
getPrivacyBadgeClass (video: Video) {
|
||||
if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green'
|
||||
|
||||
return 'badge-yellow'
|
||||
}
|
||||
|
||||
isUnpublished (video: Video) {
|
||||
return video.state.id !== VideoState.LIVE_ENDED && video.state.id !== VideoState.PUBLISHED
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</h2>
|
||||
|
||||
<form *ngIf="hasRegisteredSettings()" (ngSubmit)="formValidated()" [formGroup]="form">
|
||||
<div class="form-group" *ngFor="let setting of registeredSettings" [id]="getWrapperId(setting)">
|
||||
<div *ngFor="let setting of registeredSettings" [id]="getWrapperId(setting)">
|
||||
<my-dynamic-form-field [hidden]="isSettingHidden(setting)" [form]="form" [setting]="setting" [formErrors]="formErrors"></my-dynamic-form-field>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<my-help helpType="markdownText" supportRelMe="true"></my-help>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="description" formControlName="description" class="form-control"
|
||||
inputId="description" formControlName="description"
|
||||
markdownType="enhanced" [formError]="formErrors['description']" withEmoji="true" withHtml="true"
|
||||
></my-markdown-textarea>
|
||||
|
||||
|
|
12
client/src/app/+my-library/my-channel-space.component.html
Normal file
12
client/src/app/+my-library/my-channel-space.component.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<div class="root">
|
||||
|
||||
<div class="margin-content">
|
||||
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#my-channel-space-content" (click)="$event.preventDefault(); myChannelSpaceContent.focus()">Skip the sub menu</a>
|
||||
|
||||
<my-horizontal-menu h1Icon="channel" i18n-h1 h1="My channels" [menuEntries]="menuEntries"></my-horizontal-menu>
|
||||
</div>
|
||||
|
||||
<div #myChannelSpaceContent tabindex="-1" id="my-channel-space-content" class="margin-content pb-5 outline-0">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
48
client/src/app/+my-library/my-channel-space.component.ts
Normal file
48
client/src/app/+my-library/my-channel-space.component.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { ServerService } from '@app/core'
|
||||
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
|
||||
@Component({
|
||||
templateUrl: './my-channel-space.component.html',
|
||||
imports: [ RouterOutlet, HorizontalMenuComponent ]
|
||||
})
|
||||
export class MyChannelSpaceComponent implements OnInit {
|
||||
private serverService = inject(ServerService)
|
||||
|
||||
menuEntries: HorizontalMenuEntry[] = []
|
||||
|
||||
private serverConfig: HTMLServerConfig
|
||||
|
||||
ngOnInit (): void {
|
||||
this.serverConfig = this.serverService.getHTMLConfig()
|
||||
|
||||
this.buildMenu()
|
||||
}
|
||||
|
||||
private buildMenu () {
|
||||
this.menuEntries = [
|
||||
{
|
||||
label: $localize`Manage my channels`,
|
||||
routerLink: '/my-library/video-channels'
|
||||
},
|
||||
{
|
||||
label: $localize`Followers`,
|
||||
routerLink: '/my-library/followers'
|
||||
},
|
||||
{
|
||||
label: $localize`Synchronizations`,
|
||||
routerLink: '/my-library/video-channel-syncs',
|
||||
isDisplayed: () => this.isChannelSyncEnabled()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
private isChannelSyncEnabled () {
|
||||
const config = this.serverConfig.import
|
||||
|
||||
return this.serverConfig.import.videoChannelSynchronization.enabled &&
|
||||
(config.videos.http.enabled || config.videos.torrent.enabled)
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<my-video-cell [video]="videoChangeOwnership.video"></my-video-cell>
|
||||
<my-video-cell [video]="videoChangeOwnership.video" size="small"></my-video-cell>
|
||||
</td>
|
||||
|
||||
<td>{{ videoChangeOwnership.createdAt | ptDate: 'short' }}</td>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="margin-content">
|
||||
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#my-video-space-content" (click)="$event.preventDefault(); myVideoSpaceContent.focus()">Skip the sub menu</a>
|
||||
|
||||
<my-horizontal-menu i18n-h1 h1="My video space" [menuEntries]="menuEntries"></my-horizontal-menu>
|
||||
<my-horizontal-menu h1Icon="videos" i18n-h1 h1="My videos" [menuEntries]="menuEntries"></my-horizontal-menu>
|
||||
</div>
|
||||
|
||||
<div #myVideoSpaceContent tabindex="-1" id="my-video-space-content" class="margin-content pb-5 outline-0">
|
||||
|
|
|
@ -21,55 +21,19 @@ export class MyVideoSpaceComponent implements OnInit {
|
|||
this.buildMenu()
|
||||
}
|
||||
|
||||
isVideoImportEnabled () {
|
||||
const importConfig = this.serverConfig.import.videos
|
||||
|
||||
return importConfig.http.enabled || importConfig.torrent.enabled
|
||||
}
|
||||
|
||||
private buildMenu () {
|
||||
this.menuEntries = [
|
||||
{
|
||||
label: $localize`Channels`,
|
||||
routerLink: '/my-library/video-channels',
|
||||
children: [
|
||||
{
|
||||
label: $localize`Manage`,
|
||||
routerLink: '/my-library/video-channels'
|
||||
},
|
||||
{
|
||||
label: $localize`Followers`,
|
||||
routerLink: '/my-library/followers'
|
||||
},
|
||||
{
|
||||
label: $localize`Synchronizations`,
|
||||
routerLink: '/my-library/video-channel-syncs'
|
||||
}
|
||||
]
|
||||
label: $localize`Manage my videos`,
|
||||
routerLink: '/my-library/videos'
|
||||
},
|
||||
|
||||
{
|
||||
label: $localize`Videos`,
|
||||
routerLink: '/my-library/videos',
|
||||
label: $localize`Comments`,
|
||||
routerLink: '/my-library/comments-on-my-videos',
|
||||
children: [
|
||||
{
|
||||
label: $localize`Manage`,
|
||||
routerLink: '/my-library/videos'
|
||||
},
|
||||
|
||||
{
|
||||
label: $localize`Imports`,
|
||||
routerLink: '/my-library/video-imports',
|
||||
isDisplayed: () => this.isVideoImportEnabled()
|
||||
},
|
||||
|
||||
{
|
||||
label: $localize`Ownership changes`,
|
||||
routerLink: '/my-library/ownership'
|
||||
},
|
||||
|
||||
{
|
||||
label: $localize`Comments`,
|
||||
label: $localize`Comments on my videos`,
|
||||
routerLink: '/my-library/comments-on-my-videos'
|
||||
},
|
||||
{
|
||||
|
@ -81,7 +45,29 @@ export class MyVideoSpaceComponent implements OnInit {
|
|||
routerLink: '/my-library/auto-tag-policies'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: $localize`Settings`,
|
||||
routerLink: '/my-library/video-imports',
|
||||
children: [
|
||||
{
|
||||
label: $localize`Imports`,
|
||||
routerLink: '/my-library/video-imports',
|
||||
isDisplayed: () => this.isVideoImportEnabled()
|
||||
},
|
||||
{
|
||||
label: $localize`Ownership changes`,
|
||||
routerLink: '/my-library/ownership'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
private isVideoImportEnabled () {
|
||||
const config = this.serverConfig.import.videos
|
||||
|
||||
return config.http.enabled || config.torrent.enabled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,199 @@
|
|||
<div class="videos-header d-flex justify-content-between align-items-end gap-2 flex-wrap">
|
||||
<my-advanced-input-filter [emitOnInit]="false" [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
|
||||
<div class="channel-filters">
|
||||
<div class="channels-label" i18n>Per channel:</div>
|
||||
|
||||
<span class="total-items" *ngIf="pagination.totalItems"> {{ getTotalTitle() }}</span>
|
||||
|
||||
<div class="peertube-select-container">
|
||||
<select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control" i18n-ariaLabel aria-label="Sort by">
|
||||
<option value="-publishedAt" i18n>Last published first</option>
|
||||
<option value="-createdAt" i18n>Last created first</option>
|
||||
<option value="-views" i18n>Most viewed first</option>
|
||||
<option value="-likes" i18n>Most liked first</option>
|
||||
<option value="-duration" i18n>Longest first</option>
|
||||
</select>
|
||||
</div>
|
||||
@for (channel of channels; track channel) {
|
||||
<my-channel-toggle [channel]="channel" [(ngModel)]="channel.selected" (ngModelChange)="onFilter()"></my-channel-toggle>
|
||||
}
|
||||
</div>
|
||||
|
||||
<my-videos-selection
|
||||
[videosContainedInPlaylists]="videosContainedInPlaylists"
|
||||
[pagination]="pagination"
|
||||
[(selection)]="selection"
|
||||
[(videos)]="videos"
|
||||
[miniatureDisplayOptions]="miniatureDisplayOptions"
|
||||
[titlePage]="titlePage"
|
||||
[getVideosObservableFunction]="getVideosObservableFunction"
|
||||
[user]="user"
|
||||
[disabled]="disabled"
|
||||
#videosSelection
|
||||
<p-table
|
||||
[value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true"
|
||||
[(selection)]="selectedRows" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true"
|
||||
[showCurrentPageReport]="true" [currentPageReportTemplate]="getPaginationTemplate()"
|
||||
[expandedRowKeys]="expandedRows" [ngClass]="{ loading: loading }"
|
||||
class="new-p-table sticky-p-table cell-wrap"
|
||||
>
|
||||
<ng-template ptTemplate="globalButtons">
|
||||
<my-delete-button class="delete-selection" (click)="deleteSelectedVideos()"></my-delete-button>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="left-buttons">
|
||||
@if (isInSelectionMode()) {
|
||||
<my-action-dropdown
|
||||
i18n-label label="Batch actions" theme="primary"
|
||||
[actions]="bulkActions" [entry]="selectedRows"
|
||||
>
|
||||
</my-action-dropdown>
|
||||
} @else {
|
||||
<strong i18n [hidden]="loading">
|
||||
{ totalRecords, plural, =0 {No videos} =1 {1 video} other {{{ totalRecords | myNumberFormatter }} videos}}
|
||||
</strong>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-template ptTemplate="rowButtons" let-video>
|
||||
<div class="action-button">
|
||||
<my-edit-button label [ptRouterLink]="[ '/videos', 'update', video.shortUUID ]"></my-edit-button>
|
||||
<div class="ms-auto right-form">
|
||||
<div class="d-flex align-items-center">
|
||||
<label class="label-inline d-none d-inline-block-mw" for="table-search" i18n>Search</label>
|
||||
|
||||
<my-advanced-input-filter inputId="table-search" icon="true" i18n-placeholder placeholder="Search your videos" (search)="onSearch($event)"></my-advanced-input-filter>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<label class="label-inline" for="table-filter" i18n>Filter</label>
|
||||
|
||||
<my-select-checkbox
|
||||
inputId="table-filter"
|
||||
showToggleAll="false"
|
||||
showClear="true"
|
||||
|
||||
[availableItems]="filterItems"
|
||||
[selectableGroup]="false"
|
||||
[(ngModel)]="selectedFilterItems"
|
||||
i18n-placeholder placeholder="All videos"
|
||||
(ngModelChange)="onFilter()"
|
||||
>
|
||||
</my-select-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-video-actions-dropdown
|
||||
[video]="video" [displayOptions]="videoDropdownDisplayOptions" [moreActions]="moreVideoActions"
|
||||
buttonStyled="true" buttonDirection="horizontal" (videoRemoved)="onVideoRemoved(video)"
|
||||
></my-video-actions-dropdown>
|
||||
</div>
|
||||
</ng-template>
|
||||
</my-videos-selection>
|
||||
|
||||
<ng-template pTemplate="sorticon" let-sortOrder>
|
||||
@if (sortOrder === 1) {
|
||||
<my-button class="ms-2" small="true" active="true" rounded="true" theme="tertiary" icon="chevron-up"></my-button>
|
||||
} @else if (sortOrder === -1) {
|
||||
<my-button class="ms-2" small="true" active="true" rounded="true" theme="tertiary" icon="chevron-down"></my-button>
|
||||
} @else if (sortOrder === 0) {
|
||||
<my-button class="ms-2" small="true" rounded="true" theme="tertiary" icon="chevron-down"></my-button>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="paginatorright" let-item>
|
||||
<ng-container i18n>videos per page</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th scope="col" width="25px" class="checkbox-cell">
|
||||
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
||||
</th>
|
||||
|
||||
<th scope="col" *ngIf="isSelected('duration')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="duration" i18n>{{ getColumn('duration').label }} <p-sortIcon field="duration"></p-sortIcon></th>
|
||||
<th scope="col" *ngIf="isSelected('name')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="name" i18n>{{ getColumn('name').label }} <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th scope="col" *ngIf="isSelected('privacy')" i18n>{{ getColumn('privacy').label }}</th>
|
||||
<th scope="col" *ngIf="isSelected('sensitive')" i18n>{{ getColumn('sensitive').label }}</th>
|
||||
<th scope="col" *ngIf="isSelected('playlists')" i18n>{{ getColumn('playlists').label }}</th>
|
||||
<th scope="col" *ngIf="isSelected('insights')" [ngbTooltip]="sortTooltip" container="body" pSortableColumn="views" i18n>{{ getColumn('insights').label }} <p-sortIcon field="views"></p-sortIcon></th>
|
||||
<th scope="col" *ngIf="isSelected('published')" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="publishedAt">{{ getColumn('published').label }} <p-sortIcon field="publishedAt"></p-sortIcon></th>
|
||||
<th scope="col" *ngIf="isSelected('state')" i18n>{{ getColumn('state').label }}</th>
|
||||
|
||||
<th scope="col" width="250px" class="action-head">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span i18>Actions</span>
|
||||
|
||||
<span class="c-hand column-toggle" >
|
||||
<div ngbDropdown placement="bottom-left auto" container="body" autoClose="outside">
|
||||
<my-button theme="tertiary" icon="columns" ngbDropdownToggle i18n-title title="Open table configuration"></my-button>
|
||||
|
||||
<div ngbDropdownMenu class="p-3">
|
||||
<div class="form-group">
|
||||
<div class="muted small mb-2" i18n>Column displayed:</div>
|
||||
|
||||
@for (column of columns; track column) {
|
||||
<div class="ms-1 mb-1">
|
||||
<my-peertube-checkbox
|
||||
[inputName]="'column-' + column.id" [(ngModel)]="column.selected"
|
||||
i18n-labelText [labelText]="column.label"
|
||||
(ngModelChange)="saveSelectedColumns()"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-video>
|
||||
|
||||
<tr [pSelectableRow]="video">
|
||||
<td class="checkbox-cell">
|
||||
<p-tableCheckbox [value]="video" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
|
||||
</td>
|
||||
|
||||
<td *ngIf="isSelected('duration')">
|
||||
<my-video-cell [video]="video" thumbnail="true" title="false"></my-video-cell>
|
||||
</td>
|
||||
|
||||
<td *ngIf="isSelected('name')">
|
||||
<my-video-cell class="video-cell-name" [video]="video" thumbnail="false" title="true"></my-video-cell>
|
||||
</td>
|
||||
|
||||
<td *ngIf="isSelected('privacy')">
|
||||
<my-video-privacy-badge [video]="video"></my-video-privacy-badge>
|
||||
|
||||
<span *ngIf="video.blacklisted" class="pt-badge badge-red ms-1" i18n>Blocked</span>
|
||||
</td>
|
||||
|
||||
<td *ngIf="isSelected('sensitive')">
|
||||
<span *ngIf="video.nsfw" class="pt-badge badge-yellow" i18n>NSFW</span>
|
||||
</td>
|
||||
|
||||
<td *ngIf="isSelected('playlists')">
|
||||
<div class="badges">
|
||||
<a *ngFor="let playlist of (videosContainedInPlaylists[video.id] || [])" class="pt-badge badge-secondary" [routerLink]="['/w/p/', playlist.playlistShortUUID]">
|
||||
{{ playlist.playlistDisplayName }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td *ngIf="isSelected('insights')">
|
||||
<a class="pt" [routerLink]="[ '/videos/manage', video.shortUUID, 'stats' ]" i18n-title title="Navigate to the video stats page">
|
||||
<ng-container i18n>{video.views, plural, =0 {No views} =1 {1 view} other {{{ video.views | myNumberFormatter }} views}}</ng-container>
|
||||
|
||||
<br />
|
||||
|
||||
@if (video.isLive) {
|
||||
<ng-container i18n>{video.viewers, plural, =0 {No viewers} =1 {1 viewer} other {{{ video.views | myNumberFormatter }} viewers}}</ng-container>
|
||||
}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td *ngIf="isSelected('published')">
|
||||
{{ video.publishedAt | ptDate: 'short' }}
|
||||
</td>
|
||||
|
||||
<td *ngIf="isSelected('state')">
|
||||
<my-video-state-badge [video]="video"></my-video-state-badge>
|
||||
</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<div class="d-flex justify-content-end">
|
||||
<my-button class="me-3" i18n-label label="Manage" [ptRouterLink]="[ '/videos', 'manage', video.shortUUID ]" theme="secondary" icon="film" responsiveLabel="true"></my-button>
|
||||
|
||||
<my-video-actions-dropdown
|
||||
placement="bottom auto" buttonDirection="horizontal" buttonStyled="true" [video]="video" [displayOptions]="videoActionsOptions"
|
||||
(videoRemoved)="reloadData()" (videoFilesRemoved)="reloadData()" (transcodingCreated)="reloadData()"
|
||||
></my-video-actions-dropdown>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td myAutoColspan>
|
||||
<div class="no-results">
|
||||
<ng-container *ngIf="search" i18n>No video found matching current filters.</ng-container>
|
||||
<ng-container *ngIf="!search" i18n>You don't have any videos published yet.</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>
|
||||
|
|
|
@ -1,30 +1,71 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(300px);
|
||||
my-select-checkbox {
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.peertube-select-container {
|
||||
@include peertube-select-container(auto);
|
||||
.pt-badge {
|
||||
@include margin-right(5px);
|
||||
}
|
||||
|
||||
.action-button {
|
||||
.video-info > div {
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
min-width: 200px;
|
||||
justify-content: flex-end;
|
||||
|
||||
@include margin-left(10px);
|
||||
}
|
||||
my-global-icon {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
|
||||
my-edit-button {
|
||||
@include margin-right(10px);
|
||||
}
|
||||
|
||||
@include on-small-main-col {
|
||||
.action-button {
|
||||
margin: 1rem auto 0;
|
||||
@include global-icon-size(16px);
|
||||
@include margin-left(3px);
|
||||
}
|
||||
}
|
||||
|
||||
.right-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
td {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.channel-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid $separator-border-color;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.channels-label {
|
||||
color: pvar(--fg-200);
|
||||
font-weight: $font-bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
p-table {
|
||||
max-height: calc(100vh - #{pvar(--header-height)} - 300px);
|
||||
}
|
||||
|
||||
@media screen and (min-width: $small-view) {
|
||||
.video-cell-name {
|
||||
width: 300px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
my-video-cell {
|
||||
max-width: 120px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,91 +1,118 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { CommonModule, NgClass, NgIf } from '@angular/common'
|
||||
import { Component, inject, OnDestroy, OnInit, viewChild } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router'
|
||||
import {
|
||||
AuthService,
|
||||
ComponentPagination,
|
||||
AuthUser,
|
||||
ConfirmService,
|
||||
LocalStorageService,
|
||||
Notifier,
|
||||
ScreenService,
|
||||
ServerService,
|
||||
updatePaginationOnDelete,
|
||||
User
|
||||
PeerTubeRouterService,
|
||||
RestPagination,
|
||||
RestTable,
|
||||
ServerService
|
||||
} from '@app/core'
|
||||
import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
|
||||
import { formatICU, immutableAssign } from '@app/helpers'
|
||||
import { DropdownAction } from '@app/shared/shared-main/buttons/action-dropdown.component'
|
||||
import { DeleteButtonComponent } from '@app/shared/shared-main/buttons/delete-button.component'
|
||||
import { HeaderService } from '@app/header/header.service'
|
||||
import { formatICU } from '@app/helpers'
|
||||
import { AutoColspanDirective } from '@app/shared/shared-main/common/auto-colspan.directive'
|
||||
import { Video } from '@app/shared/shared-main/video/video.model'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { LiveStreamInformationComponent } from '@app/shared/shared-video-live/live-stream-information.component'
|
||||
import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature/video-miniature.component'
|
||||
import { SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature/videos-selection.component'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { VideoChannel, VideoExistInPlaylist, VideosExistInPlaylists, VideoSortField } from '@peertube/peertube-models'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import { concat, Observable } from 'rxjs'
|
||||
import { tap, toArray } from 'rxjs/operators'
|
||||
import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../shared/shared-forms/advanced-input-filter.component'
|
||||
|
||||
import { EditButtonComponent } from '../../shared/shared-main/buttons/edit-button.component'
|
||||
import { PeerTubeTemplateDirective } from '../../shared/shared-main/common/peertube-template.directive'
|
||||
import { ChannelToggleComponent } from '@app/shared/standalone-channels/channel-toggle.component'
|
||||
import { NgbDropdownModule, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
UserRight,
|
||||
VideoChannel,
|
||||
VideoExistInPlaylist,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
VideosExistInPlaylists
|
||||
} from '@peertube/peertube-models'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import uniqBy from 'lodash-es/uniqBy'
|
||||
import { SharedModule, SortMeta } from 'primeng/api'
|
||||
import { TableModule } from 'primeng/table'
|
||||
import { finalize } from 'rxjs/operators'
|
||||
import { SelectOptionsItem } from 'src/types'
|
||||
import { AdvancedInputFilterComponent } from '../../shared/shared-forms/advanced-input-filter.component'
|
||||
import { PeertubeCheckboxComponent } from '../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCheckboxComponent } from '../../shared/shared-forms/select/select-checkbox.component'
|
||||
import { ActionDropdownComponent, DropdownAction } from '../../shared/shared-main/buttons/action-dropdown.component'
|
||||
import { ButtonComponent } from '../../shared/shared-main/buttons/button.component'
|
||||
import { PTDatePipe } from '../../shared/shared-main/common/date.pipe'
|
||||
import { NumberFormatterPipe } from '../../shared/shared-main/common/number-formatter.pipe'
|
||||
import { VideoCellComponent } from '../../shared/shared-tables/video-cell.component'
|
||||
import {
|
||||
VideoActionsDisplayType,
|
||||
VideoActionsDropdownComponent
|
||||
} from '../../shared/shared-video-miniature/video-actions-dropdown.component'
|
||||
import { VideoPrivacyBadgeComponent } from '../../shared/shared-video/video-privacy-badge.component'
|
||||
import { VideoStateBadgeComponent } from '../../shared/shared-video/video-state-badge.component'
|
||||
import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
|
||||
|
||||
type Column = 'duration' | 'name' | 'privacy' | 'sensitive' | 'playlists' | 'insights' | 'published' | 'state'
|
||||
type CommonFilter = 'live' | 'vod' | 'private' | 'internal' | 'unlisted' | 'password-protected' | 'public'
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos',
|
||||
templateUrl: './my-videos.component.html',
|
||||
styleUrls: [ './my-videos.component.scss' ],
|
||||
imports: [
|
||||
DeleteButtonComponent,
|
||||
NgIf,
|
||||
AdvancedInputFilterComponent,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
VideosSelectionComponent,
|
||||
PeerTubeTemplateDirective,
|
||||
EditButtonComponent,
|
||||
TableModule,
|
||||
NgClass,
|
||||
SharedModule,
|
||||
NgIf,
|
||||
ActionDropdownComponent,
|
||||
AdvancedInputFilterComponent,
|
||||
ButtonComponent,
|
||||
NgbTooltip,
|
||||
VideoActionsDropdownComponent,
|
||||
VideoChangeOwnershipComponent
|
||||
VideoCellComponent,
|
||||
RouterLink,
|
||||
NumberFormatterPipe,
|
||||
VideoChangeOwnershipComponent,
|
||||
VideoPrivacyBadgeComponent,
|
||||
VideoStateBadgeComponent,
|
||||
NgbDropdownModule,
|
||||
PeertubeCheckboxComponent,
|
||||
ChannelToggleComponent,
|
||||
AutoColspanDirective,
|
||||
SelectCheckboxComponent,
|
||||
PTDatePipe
|
||||
]
|
||||
})
|
||||
export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
||||
protected router = inject(Router)
|
||||
protected serverService = inject(ServerService)
|
||||
export class MyVideosComponent extends RestTable<Video> implements OnInit, OnDestroy {
|
||||
protected route = inject(ActivatedRoute)
|
||||
protected authService = inject(AuthService)
|
||||
protected notifier = inject(Notifier)
|
||||
protected screenService = inject(ScreenService)
|
||||
protected router = inject(Router)
|
||||
private confirmService = inject(ConfirmService)
|
||||
private auth = inject(AuthService)
|
||||
private notifier = inject(Notifier)
|
||||
private videoService = inject(VideoService)
|
||||
private playlistService = inject(VideoPlaylistService)
|
||||
private server = inject(ServerService)
|
||||
private peertubeLocalStorage = inject(LocalStorageService)
|
||||
private peertubeRouter = inject(PeerTubeRouterService)
|
||||
private headerService = inject(HeaderService)
|
||||
|
||||
private static readonly LS_SELECTED_COLUMNS_KEY = 'user-my-videos-selected-columns'
|
||||
|
||||
readonly videosSelection = viewChild<VideosSelectionComponent>('videosSelection')
|
||||
readonly videoChangeOwnershipModal = viewChild<VideoChangeOwnershipComponent>('videoChangeOwnershipModal')
|
||||
readonly liveStreamInformationModal = viewChild<LiveStreamInformationComponent>('liveStreamInformationModal')
|
||||
|
||||
videos: Video[] = []
|
||||
|
||||
totalRecords = 0
|
||||
sort: SortMeta = { field: 'publishedAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
videosContainedInPlaylists: VideosExistInPlaylists = {}
|
||||
titlePage: string
|
||||
selection: SelectionType = {}
|
||||
pagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
totalItems: null
|
||||
}
|
||||
miniatureDisplayOptions: MiniatureDisplayOptions = {
|
||||
date: true,
|
||||
views: true,
|
||||
by: true,
|
||||
privacyLabel: false,
|
||||
privacyText: true,
|
||||
state: true,
|
||||
blacklistInfo: true,
|
||||
forceChannelInBy: true,
|
||||
nsfw: true
|
||||
}
|
||||
videoDropdownDisplayOptions: VideoActionsDisplayType = {
|
||||
|
||||
bulkActions: DropdownAction<Video[]>[][] = []
|
||||
|
||||
videoActionsOptions: VideoActionsDisplayType = {
|
||||
playlist: true,
|
||||
download: true,
|
||||
update: false,
|
||||
|
@ -102,97 +129,214 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
}
|
||||
|
||||
moreVideoActions: DropdownAction<{ video: Video }>[][] = []
|
||||
loading = true
|
||||
|
||||
videos: Video[] = []
|
||||
getVideosObservableFunction = this.getVideosObservable.bind(this)
|
||||
user: AuthUser
|
||||
channels: (VideoChannel & { selected: boolean })[] = []
|
||||
|
||||
sort: VideoSortField = '-publishedAt'
|
||||
filterItems: SelectOptionsItem<CommonFilter>[] = []
|
||||
selectedFilterItems: CommonFilter[] = []
|
||||
|
||||
user: User
|
||||
columns: { id: Column, label: string, selected: boolean }[] = []
|
||||
|
||||
inputFilters: AdvancedInputFilter[] = []
|
||||
|
||||
disabled = false
|
||||
|
||||
private search: string
|
||||
private userChannels: VideoChannel[] = []
|
||||
|
||||
constructor () {
|
||||
this.titlePage = $localize`My videos`
|
||||
get serverConfig () {
|
||||
return this.server.getHTMLConfig()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildActions()
|
||||
this.initialize()
|
||||
|
||||
this.user = this.authService.getUser()
|
||||
this.headerService.setSearchHidden(true)
|
||||
|
||||
if (this.route.snapshot.queryParams['search']) {
|
||||
this.search = this.route.snapshot.queryParams['search']
|
||||
}
|
||||
this.user = this.auth.getUser()
|
||||
|
||||
this.user = this.authService.getUser()
|
||||
this.userChannels = this.user.videoChannels
|
||||
this.columns = [
|
||||
{ id: 'duration', label: $localize`Duration`, selected: true },
|
||||
{ id: 'name', label: $localize`Name`, selected: true },
|
||||
{ id: 'privacy', label: $localize`Privacy`, selected: true },
|
||||
{ id: 'sensitive', label: $localize`Sensitive`, selected: true },
|
||||
{ id: 'playlists', label: $localize`Playlists`, selected: true },
|
||||
{ id: 'insights', label: $localize`Insights`, selected: true },
|
||||
{ id: 'published', label: $localize`Published`, selected: true },
|
||||
{ id: 'state', label: $localize`State`, selected: true }
|
||||
]
|
||||
|
||||
const channelFilters = [ ...this.userChannels ]
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
||||
.map(c => {
|
||||
return {
|
||||
value: 'channel:' + c.name,
|
||||
label: c.displayName
|
||||
}
|
||||
})
|
||||
|
||||
this.inputFilters = [
|
||||
this.filterItems = [
|
||||
{
|
||||
title: $localize`Advanced filters`,
|
||||
children: [
|
||||
{
|
||||
value: 'isLive:true',
|
||||
label: $localize`Only live videos`
|
||||
}
|
||||
]
|
||||
id: 'live',
|
||||
label: $localize`Lives`
|
||||
},
|
||||
|
||||
{
|
||||
title: $localize`Channel filters`,
|
||||
children: channelFilters
|
||||
id: 'vod',
|
||||
label: $localize`VOD`
|
||||
},
|
||||
{
|
||||
id: 'public',
|
||||
label: $localize`Public videos`
|
||||
},
|
||||
{
|
||||
id: 'internal',
|
||||
label: $localize`Internal videos`
|
||||
},
|
||||
{
|
||||
id: 'unlisted',
|
||||
label: $localize`Unlisted videos`
|
||||
},
|
||||
{
|
||||
id: 'password-protected',
|
||||
label: $localize`Password protected videos`
|
||||
},
|
||||
{
|
||||
id: 'private',
|
||||
label: $localize`Private videos`
|
||||
}
|
||||
]
|
||||
|
||||
this.parseQueryParams()
|
||||
this.buildActions()
|
||||
this.loadSelectedColumns()
|
||||
}
|
||||
|
||||
onSearch (search: string) {
|
||||
this.search = search
|
||||
this.reloadData()
|
||||
ngOnDestroy () {
|
||||
this.headerService.setSearchHidden(false)
|
||||
}
|
||||
|
||||
reloadData () {
|
||||
this.videosSelection().reloadVideos()
|
||||
private parseQueryParams () {
|
||||
const queryParams = this.route.snapshot.queryParams as {
|
||||
channelNameOneOf?: string[]
|
||||
privacyOneOf?: string[]
|
||||
isLive: string
|
||||
}
|
||||
|
||||
{
|
||||
const enabledChannels = queryParams.channelNameOneOf
|
||||
? new Set(arrayify(queryParams.channelNameOneOf))
|
||||
: new Set<string>()
|
||||
|
||||
this.user = this.auth.getUser()
|
||||
this.channels = this.user.videoChannels.map(c => ({
|
||||
...c,
|
||||
|
||||
selected: enabledChannels.has(c.name)
|
||||
}))
|
||||
}
|
||||
|
||||
{
|
||||
if (queryParams.isLive === 'true') this.selectedFilterItems.push('live')
|
||||
if (queryParams.isLive === 'false') this.selectedFilterItems.push('vod')
|
||||
|
||||
const enabledPrivacies = queryParams.privacyOneOf
|
||||
? new Set(arrayify(queryParams.privacyOneOf).map(t => parseInt(t) as VideoPrivacyType))
|
||||
: new Set<VideoPrivacyType>()
|
||||
|
||||
if (enabledPrivacies.has(VideoPrivacy.PUBLIC)) this.selectedFilterItems.push('public')
|
||||
if (enabledPrivacies.has(VideoPrivacy.INTERNAL)) this.selectedFilterItems.push('internal')
|
||||
if (enabledPrivacies.has(VideoPrivacy.UNLISTED)) this.selectedFilterItems.push('unlisted')
|
||||
if (enabledPrivacies.has(VideoPrivacy.PASSWORD_PROTECTED)) this.selectedFilterItems.push('password-protected')
|
||||
if (enabledPrivacies.has(VideoPrivacy.PRIVATE)) this.selectedFilterItems.push('private')
|
||||
}
|
||||
}
|
||||
|
||||
onChangeSortColumn () {
|
||||
this.videosSelection().reloadVideos()
|
||||
getIdentifier () {
|
||||
return 'MyVideosComponent'
|
||||
}
|
||||
|
||||
disableForReuse () {
|
||||
this.disabled = true
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isSelected (id: Column) {
|
||||
return this.columns.some(c => c.id === id && c.selected)
|
||||
}
|
||||
|
||||
enabledForReuse () {
|
||||
this.disabled = false
|
||||
getColumn (id: Column) {
|
||||
return this.columns.find(c => c.id === id)
|
||||
}
|
||||
|
||||
getVideosObservable (page: number) {
|
||||
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||
private loadSelectedColumns () {
|
||||
const enabledString = this.peertubeLocalStorage.getItem(MyVideosComponent.LS_SELECTED_COLUMNS_KEY)
|
||||
|
||||
return this.videoService.getMyVideos({
|
||||
videoPagination: newPagination,
|
||||
if (!enabledString) return
|
||||
try {
|
||||
const enabled = JSON.parse(enabledString)
|
||||
|
||||
for (const column of this.columns) {
|
||||
column.selected = enabled.includes(column.id)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Cannot load selected columns.', err)
|
||||
}
|
||||
}
|
||||
|
||||
saveSelectedColumns () {
|
||||
const enabled = this.columns.filter(c => c.selected).map(c => c.id)
|
||||
|
||||
this.peertubeLocalStorage.setItem(MyVideosComponent.LS_SELECTED_COLUMNS_KEY, JSON.stringify(enabled))
|
||||
}
|
||||
|
||||
onFilter () {
|
||||
const channelNameOneOf = this.channels.filter(c => c.selected).map(c => c.name)
|
||||
|
||||
const newParams = {
|
||||
...this.route.snapshot.queryParams,
|
||||
...this.buildCommonVideoFilters(),
|
||||
|
||||
channelNameOneOf
|
||||
}
|
||||
|
||||
this.peertubeRouter.silentNavigate([ '.' ], newParams, this.route)
|
||||
this.resetAndReload()
|
||||
}
|
||||
|
||||
private buildCommonVideoFilters () {
|
||||
const selectedFilterSet = new Set(this.selectedFilterItems)
|
||||
|
||||
let isLive: boolean
|
||||
if (selectedFilterSet.has('live') && !selectedFilterSet.has('vod')) {
|
||||
isLive = true
|
||||
} else if (!selectedFilterSet.has('live') && selectedFilterSet.has('vod')) {
|
||||
isLive = false
|
||||
}
|
||||
|
||||
const privacyOneOf: VideoPrivacyType[] = []
|
||||
if (selectedFilterSet.has('public')) privacyOneOf.push(VideoPrivacy.PUBLIC)
|
||||
if (selectedFilterSet.has('internal')) privacyOneOf.push(VideoPrivacy.INTERNAL)
|
||||
if (selectedFilterSet.has('unlisted')) privacyOneOf.push(VideoPrivacy.UNLISTED)
|
||||
if (selectedFilterSet.has('password-protected')) privacyOneOf.push(VideoPrivacy.PASSWORD_PROTECTED)
|
||||
if (selectedFilterSet.has('private')) privacyOneOf.push(VideoPrivacy.PRIVATE)
|
||||
|
||||
return {
|
||||
isLive,
|
||||
privacyOneOf
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
protected reloadDataInternal () {
|
||||
this.loading = true
|
||||
|
||||
const channelNameOneOf = this.channels.filter(c => c.selected).map(c => c.name)
|
||||
|
||||
return this.videoService.listMyVideos({
|
||||
restPagination: this.pagination,
|
||||
sort: this.sort,
|
||||
userChannels: this.userChannels,
|
||||
search: this.search
|
||||
}).pipe(
|
||||
tap(res => this.pagination.totalItems = res.total),
|
||||
tap(({ data }) => this.fetchVideosContainedInPlaylists(data))
|
||||
)
|
||||
search: this.search,
|
||||
|
||||
channelNameOneOf: channelNameOneOf.length !== 0
|
||||
? channelNameOneOf
|
||||
: undefined,
|
||||
|
||||
...this.buildCommonVideoFilters()
|
||||
}).pipe(finalize(() => this.loading = false))
|
||||
.subscribe({
|
||||
next: resultList => {
|
||||
this.videos = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
|
||||
this.fetchVideosContainedInPlaylists(resultList.data)
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
private fetchVideosContainedInPlaylists (videos: Video[]) {
|
||||
|
@ -205,67 +349,36 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
})
|
||||
}
|
||||
|
||||
async deleteSelectedVideos () {
|
||||
const toDeleteVideosIds = Object.entries(this.selection)
|
||||
.filter(([ _k, v ]) => v === true)
|
||||
.map(([ k, _v ]) => parseInt(k, 10))
|
||||
|
||||
const res = await this.confirmService.confirm(
|
||||
formatICU(
|
||||
$localize`Do you really want to delete {length, plural, =1 {this video} other {{length} videos}}?`,
|
||||
{ length: toDeleteVideosIds.length }
|
||||
),
|
||||
$localize`Delete`
|
||||
async removeVideos (videos: Video[]) {
|
||||
const message = formatICU(
|
||||
$localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`,
|
||||
{ count: videos.length }
|
||||
)
|
||||
|
||||
const res = await this.confirmService.confirm(message, $localize`Delete`)
|
||||
if (res === false) return
|
||||
|
||||
const observables: Observable<any>[] = []
|
||||
for (const videoId of toDeleteVideosIds) {
|
||||
const o = this.videoService.removeVideo(videoId)
|
||||
.pipe(tap(() => this.removeVideoFromArray(videoId)))
|
||||
|
||||
observables.push(o)
|
||||
}
|
||||
|
||||
concat(...observables)
|
||||
.pipe(toArray())
|
||||
this.videoService.removeVideo(videos.map(v => v.id))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success(
|
||||
formatICU(
|
||||
$localize`{length, plural, =1 {Video has been deleted} other {{length} videos have been deleted}}`,
|
||||
{ length: toDeleteVideosIds.length }
|
||||
$localize`Deleted {count, plural, =1 {1 video} other {{count} videos}}.`,
|
||||
{ count: videos.length }
|
||||
)
|
||||
)
|
||||
|
||||
this.selection = {}
|
||||
this.reloadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
onVideoRemoved (video: Video) {
|
||||
this.removeVideoFromArray(video.id)
|
||||
}
|
||||
|
||||
changeOwnership (video: Video) {
|
||||
this.videoChangeOwnershipModal().show(video)
|
||||
}
|
||||
|
||||
getTotalTitle () {
|
||||
return formatICU(
|
||||
$localize`${this.pagination.totalItems} {total, plural, =1 {video} other {videos}}`,
|
||||
{ total: this.pagination.totalItems }
|
||||
)
|
||||
}
|
||||
|
||||
private removeVideoFromArray (id: number) {
|
||||
this.videos = this.videos.filter(v => v.id !== id)
|
||||
|
||||
updatePaginationOnDelete(this.pagination)
|
||||
}
|
||||
|
||||
private buildActions () {
|
||||
this.moreVideoActions = [
|
||||
[
|
||||
|
@ -276,5 +389,16 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
|
|||
}
|
||||
]
|
||||
]
|
||||
|
||||
this.bulkActions = [
|
||||
[
|
||||
{
|
||||
label: $localize`Delete`,
|
||||
handler: videos => this.removeVideos(videos),
|
||||
isDisplayed: () => this.user.hasRight(UserRight.REMOVE_ANY_VIDEO),
|
||||
iconName: 'delete'
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Route, Routes, UrlSegment } from '@angular/router'
|
|||
import { userResolver } from '@app/core/routing/user.resolver'
|
||||
import { AbuseService } from '@app/shared/shared-moderation/abuse.service'
|
||||
import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service'
|
||||
import { BulkService } from '@app/shared/shared-moderation/bulk.service'
|
||||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
|
||||
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
|
||||
|
@ -27,7 +28,7 @@ import { MyVideoPlaylistsComponent } from './my-video-playlists/my-video-playlis
|
|||
import { MyVideoSpaceComponent } from './my-video-space.component'
|
||||
import { MyVideosComponent } from './my-videos/my-videos.component'
|
||||
import { MyWatchedWordsListComponent } from './my-watched-words-list/my-watched-words-list.component'
|
||||
import { BulkService } from '@app/shared/shared-moderation/bulk.service'
|
||||
import { MyChannelSpaceComponent } from './my-channel-space.component'
|
||||
|
||||
const commonConfig = {
|
||||
path: '',
|
||||
|
@ -50,55 +51,6 @@ const commonConfig = {
|
|||
}
|
||||
|
||||
const videoSpaceRoutes = [
|
||||
// ---------------------------------------------------------------------------
|
||||
// Channels
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'video-channels',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-channels',
|
||||
loadChildren: () => import('./+my-video-channels/routes')
|
||||
},
|
||||
|
||||
{
|
||||
path: 'followers',
|
||||
component: MyFollowersComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`My followers`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-channel-syncs',
|
||||
component: MyVideoChannelSyncsComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`My synchronizations`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-channel-syncs/create',
|
||||
component: VideoChannelSyncEditComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Create new synchronization`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Videos
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
{
|
||||
path: 'videos/comments',
|
||||
redirectTo: 'comments-on-my-videos',
|
||||
|
@ -168,8 +120,50 @@ const videoSpaceRoutes = [
|
|||
}
|
||||
] satisfies Routes
|
||||
|
||||
const libraryRoutes = [
|
||||
const channelSpaceRoutes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'video-channels',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-channels',
|
||||
loadChildren: () => import('./+my-video-channels/routes')
|
||||
},
|
||||
|
||||
{
|
||||
path: 'followers',
|
||||
component: MyFollowersComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`My followers`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-channel-syncs',
|
||||
component: MyVideoChannelSyncsComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`My synchronizations`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-channel-syncs/create',
|
||||
component: VideoChannelSyncEditComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Create new synchronization`
|
||||
}
|
||||
}
|
||||
}
|
||||
] satisfies Routes
|
||||
|
||||
const libraryRoutes = [
|
||||
// ---------------------------------------------------------------------------
|
||||
// Playlists
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -248,6 +242,14 @@ function isVideoSpaceRoute (segments: UrlSegment[]) {
|
|||
return videoSpaceRoutes.some(r => r.path === rootPath || r.path.startsWith(`${rootPath}/`))
|
||||
}
|
||||
|
||||
function isChannelSpaceRoute (segments: UrlSegment[]) {
|
||||
if (segments.length === 0) return false
|
||||
|
||||
const rootPath = segments[0].path
|
||||
|
||||
return channelSpaceRoutes.some(r => r.path === rootPath || r.path.startsWith(`${rootPath}/`))
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
...commonConfig,
|
||||
|
@ -264,10 +266,22 @@ export default [
|
|||
{
|
||||
...commonConfig,
|
||||
|
||||
component: MyChannelSpaceComponent,
|
||||
canMatch: [
|
||||
(_route: Route, segments: UrlSegment[]) => {
|
||||
return isChannelSpaceRoute(segments)
|
||||
}
|
||||
],
|
||||
|
||||
children: channelSpaceRoutes
|
||||
},
|
||||
{
|
||||
...commonConfig,
|
||||
|
||||
component: MyLibraryComponent,
|
||||
canMatch: [
|
||||
(_route: Route, segments: UrlSegment[]) => {
|
||||
return !isVideoSpaceRoute(segments)
|
||||
return !isVideoSpaceRoute(segments) && !isChannelSpaceRoute(segments)
|
||||
}
|
||||
],
|
||||
children: libraryRoutes
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { LoginGuard } from '@app/core'
|
||||
import { VideoStatsComponent, VideoStatsService } from './video'
|
||||
import { VideoResolver } from '@app/shared/shared-main/video/video.resolver'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
|
||||
export default [
|
||||
{
|
||||
path: 'videos/:videoId',
|
||||
canActivate: [ LoginGuard ],
|
||||
component: VideoStatsComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Video stats`
|
||||
}
|
||||
},
|
||||
providers: [
|
||||
LiveVideoService,
|
||||
VideoStatsService,
|
||||
VideoResolver
|
||||
],
|
||||
resolve: {
|
||||
video: VideoResolver
|
||||
}
|
||||
}
|
||||
] satisfies Routes
|
|
@ -1,2 +0,0 @@
|
|||
export * from './video-stats.component'
|
||||
export * from './video-stats.service'
|
|
@ -1,75 +0,0 @@
|
|||
<div class="margin-content">
|
||||
<h1 class="title-page">
|
||||
<my-global-icon iconName="stats"></my-global-icon>
|
||||
|
||||
<ng-container i18n>{{ video.name }} statistics</ng-container>
|
||||
</h1>
|
||||
|
||||
<div class="stats-embed">
|
||||
<div class="global-stats">
|
||||
<div *ngFor="let card of globalStatsCards" class="card stats-card">
|
||||
<div class="label">
|
||||
{{ card.label }}
|
||||
|
||||
<my-help *ngIf="card.help">
|
||||
<ng-template ptTemplate="customHtml">{{ card.help }}</ng-template>
|
||||
</my-help>
|
||||
</div>
|
||||
<div class="value">{{ card.value }}</div>
|
||||
<div *ngIf="card.moreInfo" class="more-info">{{ card.moreInfo }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-embed [video]="video"></my-embed>
|
||||
</div>
|
||||
|
||||
<div class="stats-with-date">
|
||||
|
||||
<div class="overall-stats">
|
||||
<h2>{{ getViewersStatsTitle() }}</h2>
|
||||
|
||||
<div class="date-filter-wrapper">
|
||||
<label class="visually-hidden" for="date-filter">Filter viewers stats by date</label>
|
||||
<my-select-options inputId="date-filter" [(ngModel)]="currentDateFilter" (ngModelChange)="onDateFilterChange()" [items]="dateFilters"></my-select-options>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div *ngFor="let card of overallStatCards" class="card stats-card">
|
||||
<div class="label">{{ card.label }}</div>
|
||||
<div class="value">{{ card.value }}</div>
|
||||
<div *ngIf="card.moreInfo" class="more-info">{{ card.moreInfo }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeserie">
|
||||
<div ngbNav #nav="ngbNav" [activeId]="activeGraphId" (activeIdChange)="onChartChange($event)" class="nav-tabs">
|
||||
|
||||
<ng-container *ngFor="let availableChart of availableCharts" [ngbNavItem]="availableChart.id">
|
||||
<a ngbNavLink>
|
||||
<span>{{ availableChart.label }}</span>
|
||||
</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="chart-container">
|
||||
<p-chart
|
||||
*ngIf="chartOptions[availableChart.id]"
|
||||
[height]="chartHeight" [width]="chartWidth"
|
||||
[type]="chartOptions[availableChart.id].type" [options]="chartOptions[availableChart.id].options" [data]="chartOptions[availableChart.id].data"
|
||||
[plugins]="chartPlugins"
|
||||
></p-chart>
|
||||
</div>
|
||||
|
||||
<div class="zoom-container">
|
||||
<span *ngIf="!hasZoom() && availableChart.zoomEnabled" i18n class="description">You can select a part of the graph to zoom in</span>
|
||||
|
||||
<my-button i18n *ngIf="hasZoom()" (click)="resetZoom()">Reset zoom</my-button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div [ngbNavOutlet]="nav"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,14 +1,14 @@
|
|||
import { forkJoin, Observable, of } from 'rxjs'
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { RestExtractor, ServerService } from '@app/core'
|
||||
import { immutableAssign } from '@app/helpers'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { objectKeysTyped, peertubeTranslate } from '@peertube/peertube-core-utils'
|
||||
import { VideosOverview as VideosOverviewServer } from '@peertube/peertube-models'
|
||||
import { environment } from '../../../../environments/environment'
|
||||
import { forkJoin, Observable, of } from 'rxjs'
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { VideosOverview } from './videos-overview.model'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
|
||||
@Injectable()
|
||||
export class OverviewService {
|
|
@ -4,10 +4,10 @@ import { RouterLink } from '@angular/router'
|
|||
import { DisableForReuseHook, Notifier, User, UserService } from '@app/core'
|
||||
import { ActorAvatarComponent, ActorAvatarInput } from '@app/shared/shared-actor-image/actor-avatar.component'
|
||||
import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
|
||||
import { InfiniteScrollerDirective } from '@app/shared/shared-main/common/infinite-scroller.directive'
|
||||
import { Video } from '@app/shared/shared-main/video/video.model'
|
||||
import { VideoMiniatureComponent } from '@app/shared/shared-video-miniature/video-miniature.component'
|
||||
import { Subject, Subscription, switchMap } from 'rxjs'
|
||||
import { InfiniteScrollerDirective } from '../../../shared/shared-main/common/infinite-scroller.directive'
|
||||
import { VideoMiniatureComponent } from '../../../shared/shared-video-miniature/video-miniature.component'
|
||||
import { OverviewService } from './overview.service'
|
||||
|
||||
@Component({
|
|
@ -6,9 +6,9 @@ import { BlocklistService } from '@app/shared/shared-moderation/blocklist.servic
|
|||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { OverviewService, VideosListAllComponent } from './video-list'
|
||||
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
|
||||
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
|
||||
import { OverviewService, VideosListAllComponent } from '.'
|
||||
import { VideoOverviewComponent } from './overview/video-overview.component'
|
||||
import { VideoUserSubscriptionsComponent } from './video-user-subscriptions.component'
|
||||
|
||||
export default [
|
||||
{
|
|
@ -1,13 +1,13 @@
|
|||
import { firstValueFrom } from 'rxjs'
|
||||
import { switchMap, tap } from 'rxjs/operators'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { AuthService, ComponentPaginationLight, DisableForReuseHook, ScopedTokensService } from '@app/core'
|
||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||
import { VideoSortField } from '@peertube/peertube-models'
|
||||
import { VideosListComponent } from '../../shared/shared-video-miniature/videos-list.component'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
|
||||
import { VideoFilters } from '@app/shared/shared-video-miniature/video-filters.model'
|
||||
import { VideosListComponent } from '@app/shared/shared-video-miniature/videos-list.component'
|
||||
import { VideoSortField } from '@peertube/peertube-models'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { switchMap, tap } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-user-subscriptions',
|
|
@ -4,9 +4,9 @@ import { ComponentPaginationLight, DisableForReuseHook, MetaService } from '@app
|
|||
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { VideoFilterScope, VideoFilters } from '@app/shared/shared-video-miniature/video-filters.model'
|
||||
import { VideosListComponent } from '@app/shared/shared-video-miniature/videos-list.component'
|
||||
import { VideoSortField } from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { VideosListComponent } from '../../shared/shared-video-miniature/videos-list.component'
|
||||
|
||||
@Component({
|
||||
templateUrl: './videos-list-all.component.html',
|
|
@ -1 +0,0 @@
|
|||
export * from './video-studio-edit.component'
|
|
@ -1,221 +0,0 @@
|
|||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { ConfirmService, Notifier, ServerService } from '@app/core'
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { secondsToTime } from '@peertube/peertube-core-utils'
|
||||
import { VideoStudioTask, VideoStudioTaskCut } from '@peertube/peertube-models'
|
||||
import { VideoStudioService } from '../shared'
|
||||
import { NgIf, NgFor } from '@angular/common'
|
||||
import { EmbedComponent } from '../../shared/shared-main/video/embed.component'
|
||||
import { ButtonComponent } from '../../shared/shared-main/buttons/button.component'
|
||||
import { ReactiveFileComponent } from '../../shared/shared-forms/reactive-file.component'
|
||||
import { TimestampInputComponent } from '../../shared/shared-forms/timestamp-input.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-studio-edit',
|
||||
templateUrl: './video-studio-edit.component.html',
|
||||
styleUrls: [ './video-studio-edit.component.scss' ],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
TimestampInputComponent,
|
||||
ReactiveFileComponent,
|
||||
ButtonComponent,
|
||||
EmbedComponent,
|
||||
NgIf,
|
||||
NgFor,
|
||||
GlobalIconComponent
|
||||
]
|
||||
})
|
||||
export class VideoStudioEditComponent extends FormReactive implements OnInit {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
private serverService = inject(ServerService)
|
||||
private notifier = inject(Notifier)
|
||||
private router = inject(Router)
|
||||
private route = inject(ActivatedRoute)
|
||||
private videoStudioService = inject(VideoStudioService)
|
||||
private loadingBar = inject(LoadingBarService)
|
||||
private confirmService = inject(ConfirmService)
|
||||
|
||||
isRunningEdit = false
|
||||
|
||||
video: VideoDetails
|
||||
|
||||
ngOnInit () {
|
||||
this.video = this.route.snapshot.data.video
|
||||
|
||||
const defaultValues = {
|
||||
cut: {
|
||||
start: 0,
|
||||
end: this.video.duration
|
||||
}
|
||||
}
|
||||
|
||||
this.buildForm({
|
||||
'cut': {
|
||||
start: null,
|
||||
end: null
|
||||
},
|
||||
'add-intro': {
|
||||
file: null
|
||||
},
|
||||
'add-outro': {
|
||||
file: null
|
||||
},
|
||||
'add-watermark': {
|
||||
file: null
|
||||
}
|
||||
}, defaultValues)
|
||||
}
|
||||
|
||||
get videoExtensions () {
|
||||
return this.serverService.getHTMLConfig().video.file.extensions
|
||||
}
|
||||
|
||||
get imageExtensions () {
|
||||
return this.serverService.getHTMLConfig().video.image.extensions
|
||||
}
|
||||
|
||||
async runEdit () {
|
||||
if (this.isRunningEdit) return
|
||||
if (!this.form.valid) return
|
||||
if (this.noEdit()) return
|
||||
|
||||
const title = $localize`Are you sure you want to edit "${this.video.name}"?`
|
||||
const listHTML = this.getTasksSummary().map(t => `<li>${t}</li>`).join('')
|
||||
|
||||
const confirmHTML =
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`The current video will be overwritten by this edited video and <strong>you won't be able to recover it</strong>.<br /><br />` +
|
||||
$localize`As a reminder, the following tasks will be executed: <ol>${listHTML}</ol>`
|
||||
|
||||
if (await this.confirmService.confirm(confirmHTML, title) !== true) return
|
||||
|
||||
this.isRunningEdit = true
|
||||
|
||||
const tasks = this.buildTasks()
|
||||
|
||||
this.loadingBar.useRef().start()
|
||||
|
||||
return this.videoStudioService.editVideo(this.video.uuid, tasks)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Editing tasks created.`)
|
||||
|
||||
// Don't redirect to old video version watch page that could be confusing for users
|
||||
this.router.navigateByUrl('/my-library/videos')
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.loadingBar.useRef().complete()
|
||||
this.isRunningEdit = false
|
||||
this.notifier.error(err.message)
|
||||
logger.error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getIntroOutroTooltip () {
|
||||
return $localize`(extensions: ${this.videoExtensions.join(', ')})`
|
||||
}
|
||||
|
||||
getWatermarkTooltip () {
|
||||
return $localize`(extensions: ${this.imageExtensions.join(', ')})`
|
||||
}
|
||||
|
||||
noEdit () {
|
||||
return this.buildTasks().length === 0
|
||||
}
|
||||
|
||||
getTasksSummary () {
|
||||
const tasks = this.buildTasks()
|
||||
|
||||
return tasks.map(t => {
|
||||
if (t.name === 'add-intro') {
|
||||
return $localize`"${this.getFilename(t.options.file)}" will be added at the beginning of the video`
|
||||
}
|
||||
|
||||
if (t.name === 'add-outro') {
|
||||
return $localize`"${this.getFilename(t.options.file)}" will be added at the end of the video`
|
||||
}
|
||||
|
||||
if (t.name === 'add-watermark') {
|
||||
return $localize`"${this.getFilename(t.options.file)}" image watermark will be added to the video`
|
||||
}
|
||||
|
||||
if (t.name === 'cut') {
|
||||
const { start, end } = t.options
|
||||
|
||||
if (start !== undefined && end !== undefined) {
|
||||
return $localize`Video will begin at ${secondsToTime(start)} and stop at ${secondsToTime(end)}`
|
||||
}
|
||||
|
||||
if (start !== undefined) {
|
||||
return $localize`Video will begin at ${secondsToTime(start)}`
|
||||
}
|
||||
|
||||
if (end !== undefined) {
|
||||
return $localize`Video will stop at ${secondsToTime(end)}`
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
}
|
||||
|
||||
private getFilename (obj: any) {
|
||||
return obj.name
|
||||
}
|
||||
|
||||
private buildTasks () {
|
||||
const tasks: VideoStudioTask[] = []
|
||||
const value = this.form.value
|
||||
|
||||
const cut = value['cut']
|
||||
if (cut['start'] !== 0 || cut['end'] !== this.video.duration) {
|
||||
const options: VideoStudioTaskCut['options'] = {}
|
||||
if (cut['start'] !== 0) options.start = cut['start']
|
||||
if (cut['end'] !== this.video.duration) options.end = cut['end']
|
||||
|
||||
tasks.push({
|
||||
name: 'cut',
|
||||
options
|
||||
})
|
||||
}
|
||||
|
||||
if (value['add-intro']?.['file']) {
|
||||
tasks.push({
|
||||
name: 'add-intro',
|
||||
options: {
|
||||
file: value['add-intro']['file']
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (value['add-outro']?.['file']) {
|
||||
tasks.push({
|
||||
name: 'add-outro',
|
||||
options: {
|
||||
file: value['add-outro']['file']
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (value['add-watermark']?.['file']) {
|
||||
tasks.push({
|
||||
name: 'add-watermark',
|
||||
options: {
|
||||
file: value['add-watermark']['file']
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { LoginGuard } from '@app/core'
|
||||
import { VideoStudioEditComponent } from './edit'
|
||||
import { VideoStudioService } from './shared'
|
||||
import { VideoResolver } from '@app/shared/shared-main/video/video.resolver'
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
canActivateChild: [ LoginGuard ],
|
||||
providers: [ VideoStudioService ],
|
||||
children: [
|
||||
{
|
||||
path: 'edit/:videoId',
|
||||
component: VideoStudioEditComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Studio`
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
video: VideoResolver
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
] satisfies Routes
|
|
@ -1 +0,0 @@
|
|||
export * from './video-studio.service'
|
1
client/src/app/+video-watch/player-styles.component.scss
Normal file
1
client/src/app/+video-watch/player-styles.component.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@use '../../standalone/player/build/peertube-player.css';
|
|
@ -8,7 +8,7 @@ import { UserAdminService } from '@app/shared/shared-users/user-admin.service'
|
|||
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { OverviewService } from '../video-list'
|
||||
import { OverviewService } from '../+video-list'
|
||||
import { VideoRecommendationService } from './shared'
|
||||
import { VideoWatchComponent } from './video-watch.component'
|
||||
import { BulkService } from '@app/shared/shared-moderation/bulk.service'
|
|
@ -1,19 +1,16 @@
|
|||
import { NgClass, NgIf, NgStyle } from '@angular/common'
|
||||
import { Component, OnChanges, inject, input, output, viewChild } from '@angular/core'
|
||||
import { RedirectService, ScreenService } from '@app/core'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
|
||||
import { VideoShareComponent } from '@app/shared/shared-share-modal/video-share.component'
|
||||
import { SupportModalComponent } from '@app/shared/shared-support-modal/support-modal.component'
|
||||
import { VideoDownloadComponent } from '@app/shared/shared-video-miniature/download/video-download.component'
|
||||
import { VideoActionsDisplayType, VideoActionsDropdownComponent } from '@app/shared/shared-video-miniature/video-actions-dropdown.component'
|
||||
import { VideoAddToPlaylistComponent } from '@app/shared/shared-video-playlist/video-add-to-playlist.component'
|
||||
import { VideoPlaylist } from '@app/shared/shared-video-playlist/video-playlist.model'
|
||||
import { NgbDropdown, NgbDropdownMenu, NgbDropdownToggle, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { UserVideoRateType, VideoCaption, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component'
|
||||
import {
|
||||
VideoActionsDisplayType,
|
||||
VideoActionsDropdownComponent
|
||||
} from '../../../../shared/shared-video-miniature/video-actions-dropdown.component'
|
||||
import { VideoAddToPlaylistComponent } from '../../../../shared/shared-video-playlist/video-add-to-playlist.component'
|
||||
import { VideoRateComponent } from './video-rate.component'
|
||||
|
||||
@Component({
|
|
@ -1,12 +1,12 @@
|
|||
import { Observable } from 'rxjs'
|
||||
import { Component, OnChanges, OnDestroy, OnInit, inject, input, output } from '@angular/core'
|
||||
import { Notifier, ScreenService, Hotkey, HotkeysService } from '@app/core'
|
||||
import { UserVideoRateType } from '@peertube/peertube-models'
|
||||
import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component'
|
||||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { NgbPopover, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { Component, OnChanges, OnDestroy, OnInit, inject, input, output } from '@angular/core'
|
||||
import { Hotkey, HotkeysService, Notifier, ScreenService } from '@app/core'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { NgbPopover, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { UserVideoRateType } from '@peertube/peertube-models'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-rate',
|
|
@ -1,23 +1,23 @@
|
|||
import { getLocaleDirection, NgClass, NgFor, NgIf } from '@angular/common'
|
||||
import { Component, ElementRef, LOCALE_ID, OnChanges, OnInit, SimpleChanges, inject, input, output, viewChild } from '@angular/core'
|
||||
import { Component, ElementRef, inject, input, LOCALE_ID, OnChanges, OnInit, output, SimpleChanges, viewChild } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Notifier, User } from '@app/core'
|
||||
import { VIDEO_COMMENT_TEXT_VALIDATOR } from '@app/shared/form-validators/video-comment-validators'
|
||||
import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.component'
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { TextareaAutoResizeDirective } from '@app/shared/shared-forms/textarea-autoresize.directive'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { HelpComponent } from '@app/shared/shared-main/buttons/help.component'
|
||||
import { PeerTubeTemplateDirective } from '@app/shared/shared-main/common/peertube-template.directive'
|
||||
import { LoginLinkComponent } from '@app/shared/shared-main/users/login-link.component'
|
||||
import { Video } from '@app/shared/shared-main/video/video.model'
|
||||
import { RemoteSubscribeComponent } from '@app/shared/shared-user-subscription/remote-subscribe.component'
|
||||
import { VideoComment } from '@app/shared/shared-video-comment/video-comment.model'
|
||||
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { VideoCommentCreate } from '@peertube/peertube-models'
|
||||
import { Observable } from 'rxjs'
|
||||
import { ActorAvatarComponent } from '../../../../shared/shared-actor-image/actor-avatar.component'
|
||||
import { TextareaAutoResizeDirective } from '../../../../shared/shared-forms/textarea-autoresize.directive'
|
||||
import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component'
|
||||
import { HelpComponent } from '../../../../shared/shared-main/buttons/help.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { RemoteSubscribeComponent } from '../../../../shared/shared-user-subscription/remote-subscribe.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-comment-add',
|
|
@ -3,16 +3,16 @@ import { Component, OnChanges, OnInit, inject, input, model, output, viewChild }
|
|||
import { RouterLink } from '@angular/router'
|
||||
import { MarkdownService, Notifier, UserService } from '@app/core'
|
||||
import { AuthService } from '@app/core/auth'
|
||||
import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.component'
|
||||
import { Account } from '@app/shared/shared-main/account/account.model'
|
||||
import { DropdownAction } from '@app/shared/shared-main/buttons/action-dropdown.component'
|
||||
import { FromNowPipe } from '@app/shared/shared-main/date/from-now.pipe'
|
||||
import { Video } from '@app/shared/shared-main/video/video.model'
|
||||
import { CommentReportComponent } from '@app/shared/shared-moderation/report-modals/comment-report.component'
|
||||
import { UserModerationDropdownComponent } from '@app/shared/shared-moderation/user-moderation-dropdown.component'
|
||||
import { VideoCommentThreadTree } from '@app/shared/shared-video-comment/video-comment-thread-tree.model'
|
||||
import { VideoComment } from '@app/shared/shared-video-comment/video-comment.model'
|
||||
import { User, UserRight } from '@peertube/peertube-models'
|
||||
import { ActorAvatarComponent } from '../../../../shared/shared-actor-image/actor-avatar.component'
|
||||
import { FromNowPipe } from '../../../../shared/shared-main/date/from-now.pipe'
|
||||
import { UserModerationDropdownComponent } from '../../../../shared/shared-moderation/user-moderation-dropdown.component'
|
||||
import { TimestampRouteTransformerDirective } from '../timestamp-route-transformer.directive'
|
||||
import { VideoCommentAddComponent } from './video-comment-add.component'
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
import { NgFor, NgIf } from '@angular/common'
|
||||
import { Component, ElementRef, OnChanges, OnDestroy, OnInit, SimpleChanges, inject, input, output, viewChild } from '@angular/core'
|
||||
import { Component, ElementRef, inject, input, OnChanges, OnDestroy, OnInit, output, SimpleChanges, viewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService, User } from '@app/core'
|
||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||
import { InfiniteScrollerDirective } from '@app/shared/shared-main/common/infinite-scroller.directive'
|
||||
import { LoaderComponent } from '@app/shared/shared-main/common/loader.component'
|
||||
import { FeedComponent } from '@app/shared/shared-main/feeds/feed.component'
|
||||
import { Syndication } from '@app/shared/shared-main/feeds/syndication.model'
|
||||
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
|
||||
import { VideoCommentThreadTree } from '@app/shared/shared-video-comment/video-comment-thread-tree.model'
|
||||
|
@ -11,9 +14,6 @@ import { VideoCommentService } from '@app/shared/shared-video-comment/video-comm
|
|||
import { NgbDropdown, NgbDropdownButtonItem, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PeerTubeProblemDocument, ServerErrorCode, VideoCommentPolicy } from '@peertube/peertube-models'
|
||||
import { lastValueFrom, Subject, Subscription } from 'rxjs'
|
||||
import { InfiniteScrollerDirective } from '../../../../shared/shared-main/common/infinite-scroller.directive'
|
||||
import { LoaderComponent } from '../../../../shared/shared-main/common/loader.component'
|
||||
import { FeedComponent } from '../../../../shared/shared-main/feeds/feed.component'
|
||||
import { VideoCommentAddComponent } from './video-comment-add.component'
|
||||
import { VideoCommentComponent } from './video-comment.component'
|
||||
|
|
@ -2,10 +2,10 @@ import { NgFor, NgIf } from '@angular/common'
|
|||
import { Component, OnChanges, inject, input } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { HooksService } from '@app/core'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { TimeDurationFormatterPipe } from '@app/shared/shared-main/date/time-duration-formatter.pipe'
|
||||
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
|
||||
import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component'
|
||||
|
||||
type PluginMetadata = {
|
||||
label: string
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, OnInit, input } from '@angular/core'
|
||||
import { ActorAvatarComponent } from '../../../../shared/shared-actor-image/actor-avatar.component'
|
||||
import { NgIf } from '@angular/common'
|
||||
import { Component, OnInit, input } from '@angular/core'
|
||||
import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.component'
|
||||
import { Video } from '@app/shared/shared-main/video/video.model'
|
||||
|
||||
@Component({
|
|
@ -1,9 +1,11 @@
|
|||
import { NgClass, NgFor } from '@angular/common'
|
||||
import { Component, ElementRef, HostListener, OnChanges, OnInit, SimpleChanges, inject, input, output, viewChild } from '@angular/core'
|
||||
import { Component, ElementRef, HostListener, inject, input, OnChanges, OnInit, output, SimpleChanges, viewChild } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Notifier } from '@app/core'
|
||||
import { durationToString, isInViewport } from '@app/helpers'
|
||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { Nl2BrPipe } from '@app/shared/shared-main/common/nl2br.pipe'
|
||||
import { VideoCaptionService } from '@app/shared/shared-main/video-caption/video-caption.service'
|
||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Video, VideoCaption } from '@peertube/peertube-models'
|
||||
|
@ -11,8 +13,6 @@ import { parse } from '@plussub/srt-vtt-parser'
|
|||
import debug from 'debug'
|
||||
import { debounceTime, distinctUntilChanged, Subject } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types'
|
||||
import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component'
|
||||
import { Nl2BrPipe } from '../../../../shared/shared-main/common/nl2br.pipe'
|
||||
|
||||
const debugLogger = debug('peertube:watch:VideoTranscriptionComponent')
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue