mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 01:39:37 +02:00
Compare commits
139 commits
fb18bcabca
...
e38c4ba0d1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e38c4ba0d1 | ||
![]() |
6a43a72fec | ||
![]() |
e902f40d3f | ||
![]() |
8384438a36 | ||
![]() |
b59dc46448 | ||
![]() |
994c53ea9a | ||
![]() |
ce8a6e402e | ||
![]() |
e170507000 | ||
![]() |
cf621b16cc | ||
![]() |
8c83592d89 | ||
![]() |
91afa1004e | ||
![]() |
0882d96624 | ||
![]() |
3ea32ba891 | ||
![]() |
12b4893239 | ||
![]() |
aea6983cc4 | ||
![]() |
65f5bd1c37 | ||
![]() |
c5ec42f587 | ||
![]() |
741c8f62e0 | ||
![]() |
5b1ac25794 | ||
![]() |
eedcca7879 | ||
![]() |
7f70d7f3f2 | ||
![]() |
8447769447 | ||
![]() |
4d45ca6f09 | ||
![]() |
77c3a279f7 | ||
![]() |
fde057171d | ||
![]() |
6c3d7501e2 | ||
![]() |
1b283c8694 | ||
![]() |
401e5c0b07 | ||
![]() |
7e1701d9d1 | ||
![]() |
06e5f534ba | ||
![]() |
3ee605b433 | ||
![]() |
bc0132239e | ||
![]() |
796229ac34 | ||
![]() |
3d75a7288f | ||
![]() |
42bf34c29a | ||
![]() |
d0e810d29a | ||
![]() |
b7aa685009 | ||
![]() |
cfe49b37ec | ||
![]() |
f383fe101a | ||
![]() |
7db2817877 | ||
![]() |
24dbbbad64 | ||
![]() |
5894c362d6 | ||
![]() |
57667734c6 | ||
![]() |
8f852e3153 | ||
![]() |
aaae2910e7 | ||
![]() |
507cedca1c | ||
![]() |
a94128a9ce | ||
![]() |
d477aa0df6 | ||
![]() |
198192aeb4 | ||
![]() |
d1b9a88297 | ||
![]() |
2ebd9a74f5 | ||
![]() |
c2fc1cbbd9 | ||
![]() |
423633bc2b | ||
![]() |
045409fa35 | ||
![]() |
74bb0e58c0 | ||
![]() |
bf1c1379a2 | ||
![]() |
c9eb2e2289 | ||
![]() |
d090795d0c | ||
![]() |
6822bcfcb3 | ||
![]() |
9ae1a0177c | ||
![]() |
c26c2c6007 | ||
![]() |
2955824366 | ||
![]() |
fac0f0bc1d | ||
![]() |
33aaa7f1d1 | ||
![]() |
2453a82856 | ||
![]() |
0967ee953c | ||
![]() |
a1a6515524 | ||
![]() |
0dd0198693 | ||
![]() |
6dcdc680e0 | ||
![]() |
8f0389ab8c | ||
![]() |
035846578f | ||
![]() |
c858dd9982 | ||
![]() |
789696d22f | ||
![]() |
0bb20d7e19 | ||
![]() |
e3f0beb6cb | ||
![]() |
a383baa812 | ||
![]() |
65f89db861 | ||
![]() |
b591f42914 | ||
![]() |
3ca2fec672 | ||
![]() |
263cd2e3d1 | ||
![]() |
d7ae2daed1 | ||
![]() |
667dc856ce | ||
![]() |
00b70b84ab | ||
![]() |
748b940dc4 | ||
![]() |
6126aed19e | ||
![]() |
82b92b497f | ||
![]() |
67f495bdac | ||
![]() |
51ecd4c2b0 | ||
![]() |
3561f25806 | ||
![]() |
05c4e1c43d | ||
![]() |
0c5f88a541 | ||
![]() |
e631b1d32e | ||
![]() |
f6c77d1383 | ||
![]() |
3656df036f | ||
![]() |
bc2b2a6c19 | ||
![]() |
54f572fbd0 | ||
![]() |
98ccaec295 | ||
![]() |
ce64ee9c5c | ||
![]() |
4222dd6686 | ||
![]() |
d7ffde7299 | ||
![]() |
bcb012977c | ||
![]() |
18d05d3a40 | ||
![]() |
5a87e008e5 | ||
![]() |
c5f854164c | ||
![]() |
d301a9f3ae | ||
![]() |
fd0fc9a5f9 | ||
![]() |
264558e2ca | ||
![]() |
92a4123f25 | ||
![]() |
5926787861 | ||
![]() |
8d262d01b2 | ||
![]() |
ae9a802403 | ||
![]() |
4fd2cdb390 | ||
![]() |
3edb4b75ee | ||
![]() |
59adaaad69 | ||
![]() |
82815624b4 | ||
![]() |
1496961c53 | ||
![]() |
18b22257a8 | ||
![]() |
bc1a7d0873 | ||
![]() |
bd6eb388f8 | ||
![]() |
71c832f6b2 | ||
![]() |
f3f77f596e | ||
![]() |
ef690bc792 | ||
![]() |
ad41b6c06e | ||
![]() |
e10ed4f5ed | ||
![]() |
15867684f5 | ||
![]() |
57a8e18022 | ||
![]() |
19ec133abb | ||
![]() |
f5d6097980 | ||
![]() |
93f5a7d789 | ||
![]() |
afc1f0e6b0 | ||
![]() |
adfa6b43ad | ||
![]() |
06fd09b93a | ||
![]() |
9d9607feff | ||
![]() |
d1a35e8421 | ||
![]() |
da23ad1d09 | ||
![]() |
b4df49b87f | ||
![]() |
6fce7c808c | ||
![]() |
89360a4ef0 | ||
![]() |
acaabaace1 |
410 changed files with 80217 additions and 32818 deletions
|
@ -88,6 +88,6 @@
|
|||
"https://plugins.dprint.dev/markdown-0.17.1.wasm",
|
||||
"https://plugins.dprint.dev/toml-0.6.2.wasm",
|
||||
"https://plugins.dprint.dev/g-plane/malva-v0.12.0.wasm",
|
||||
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.19.1.wasm"
|
||||
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.23.3.wasm"
|
||||
]
|
||||
}
|
||||
|
|
222
.github/copilot-instructions.md
vendored
Normal file
222
.github/copilot-instructions.md
vendored
Normal file
|
@ -0,0 +1,222 @@
|
|||
# PeerTube Copilot Instructions
|
||||
|
||||
## Repository Overview
|
||||
|
||||
PeerTube is an open-source, ActivityPub-federated video streaming platform using P2P technology directly in web browsers. It's developed by Framasoft and provides a decentralized alternative to centralized video platforms like YouTube.
|
||||
|
||||
**Repository Stats:**
|
||||
- **Size**: Large monorepo (~350MB, ~15k files)
|
||||
- **Type**: Full-stack web application
|
||||
- **Languages**: TypeScript (backend), Angular (frontend), Shell scripts
|
||||
- **Target Runtime**: Node.js >=20.x, PostgreSQL >=10.x, Redis >=6.x
|
||||
- **Package Manager**: Yarn 1.x (NOT >=2.x)
|
||||
- **Architecture**: Express.js API server + Angular SPA client + P2P video delivery
|
||||
|
||||
## Critical: Client Directory Exclusion
|
||||
|
||||
**🚫 ALWAYS IGNORE `client/` directory** - it contains a separate Angular frontend project with its own build system, dependencies, and development workflow. Focus only on the server-side backend code.
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
### Prerequisites (Required)
|
||||
1. **Dependencies**: Node.js >=20.x, Yarn 1.x, PostgreSQL >=10.x, Redis >=6.x, FFmpeg >=4.3, Python >=3.8
|
||||
2. **PostgreSQL Setup**:
|
||||
```bash
|
||||
sudo -u postgres createuser -P peertube
|
||||
sudo -u postgres createdb -O peertube peertube_dev
|
||||
sudo -u postgres psql -c "CREATE EXTENSION pg_trgm;" peertube_dev
|
||||
sudo -u postgres psql -c "CREATE EXTENSION unaccent;" peertube_dev
|
||||
```
|
||||
3. **Services**: Start PostgreSQL and Redis before development
|
||||
|
||||
### Installation & Build (Execute in Order)
|
||||
```bash
|
||||
# 1. ALWAYS install dependencies first (takes ~2-3 minutes)
|
||||
yarn install --frozen-lockfile
|
||||
|
||||
# 2. Build server (required for most operations, takes ~3-5 minutes)
|
||||
npm run build:server
|
||||
|
||||
# 3. Optional: Build full application (takes ~10-15 minutes)
|
||||
npm run build
|
||||
```
|
||||
|
||||
**⚠️ Critical Notes:**
|
||||
- Always run `yarn install --frozen-lockfile` before any build operation
|
||||
- Server build is prerequisite for testing and development
|
||||
- Never use `npm install` - always use `yarn`
|
||||
- Build failures often indicate missing PostgreSQL extensions or wrong Node.js version
|
||||
|
||||
### Development Commands
|
||||
```bash
|
||||
# Server-only development (recommended for backend work)
|
||||
npm run dev:server # Starts server on localhost:9000 with hot reload
|
||||
|
||||
# Full stack development (NOT recommended if only working on server)
|
||||
npm run dev # Starts both server (9000) and client (3000)
|
||||
|
||||
# Development credentials:
|
||||
# Username: root
|
||||
# Password: test
|
||||
```
|
||||
|
||||
### Testing Commands (Execute in Order)
|
||||
```bash
|
||||
# 1. Prepare test environment (required before first test run)
|
||||
sudo -u postgres createuser $(whoami) --createdb --superuser
|
||||
npm run clean:server:test
|
||||
|
||||
# 2. Build (required before testing)
|
||||
npm run build
|
||||
|
||||
# 3. Run specific test suites (recommended over full test)
|
||||
npm run ci -- api-1 # API tests part 1
|
||||
npm run ci -- api-2 # API tests part 2
|
||||
npm run ci -- lint # Linting only
|
||||
npm run ci -- client # Client tests
|
||||
|
||||
# 4. Run single test file
|
||||
npm run mocha -- --exit --bail packages/tests/src/api/videos/single-server.ts
|
||||
|
||||
# 5. Full test suite (takes ~45-60 minutes, avoid unless necessary)
|
||||
npm run test
|
||||
```
|
||||
|
||||
**⚠️ Test Environment Notes:**
|
||||
- Tests require PostgreSQL user with createdb/superuser privileges
|
||||
- Some tests need Docker containers for S3/LDAP simulation
|
||||
- Test failures often indicate missing system dependencies or DB permissions
|
||||
- Set `DISABLE_HTTP_IMPORT_TESTS=true` to skip flaky import tests
|
||||
|
||||
### Validation Commands
|
||||
```bash
|
||||
# Lint code (runs ESLint + OpenAPI validation)
|
||||
npm run lint
|
||||
|
||||
# Validate OpenAPI spec
|
||||
npm run swagger-cli -- validate support/doc/api/openapi.yaml
|
||||
|
||||
# Build server
|
||||
npm run build:server
|
||||
```
|
||||
|
||||
## Project Architecture & Layout
|
||||
|
||||
### Server-Side Structure (Primary Focus)
|
||||
```
|
||||
server/core/
|
||||
├── controllers/api/ # Express route handlers (add new endpoints here)
|
||||
│ ├── index.ts # Main API router registration
|
||||
│ ├── videos/ # Video-related endpoints
|
||||
│ └── users/ # User-related endpoints
|
||||
├── models/ # Sequelize database models
|
||||
│ ├── video/ # Video, channel, playlist models
|
||||
│ └── user/ # User, account models
|
||||
├── lib/ # Business logic services
|
||||
│ ├── job-queue/ # Background job processing
|
||||
│ └── emailer.ts # Email service
|
||||
├── middlewares/ # Express middleware
|
||||
│ ├── validators/ # Input validation (always required)
|
||||
│ └── auth.ts # Authentication middleware
|
||||
├── helpers/ # Utility functions
|
||||
└── initializers/ # App startup and constants
|
||||
```
|
||||
|
||||
### Key Configuration Files
|
||||
- `package.json` - Main dependencies and scripts
|
||||
- `server/package.json` - Server-specific config
|
||||
- `eslint.config.mjs` - Linting rules
|
||||
- `tsconfig.base.json` - TypeScript base config
|
||||
- `config/default.yaml` - Default app configuration
|
||||
- `.mocharc.cjs` - Test runner configuration
|
||||
|
||||
### Shared Packages (`packages/`)
|
||||
```
|
||||
packages/
|
||||
├── models/ # Shared TypeScript interfaces (modify for API changes)
|
||||
├── core-utils/ # Common utilities
|
||||
├── ffmpeg/ # Video processing
|
||||
├── server-commands/ # Test helpers
|
||||
└── tests/ # Test files
|
||||
```
|
||||
|
||||
### Scripts Directory (`scripts/`)
|
||||
- `scripts/build/` - Build automation
|
||||
- `scripts/dev/` - Development helpers
|
||||
- `scripts/ci.sh` - Continuous integration runner
|
||||
- `scripts/test.sh` - Test runner
|
||||
|
||||
## Continuous Integration Pipeline
|
||||
|
||||
**GitHub Actions** (`.github/workflows/test.yml`):
|
||||
1. **Matrix Strategy**: Tests run in parallel across different suites
|
||||
2. **Required Services**: PostgreSQL, Redis, LDAP, S3, Keycloak containers
|
||||
3. **Test Suites**: `types-package`, `client`, `api-1` through `api-5`, `transcription`, `cli-plugin`, `lint`, `external-plugins`
|
||||
4. **Environment**: Ubuntu 22.04, Node.js 20.x
|
||||
5. **Typical Runtime**: 15-30 minutes per suite
|
||||
|
||||
**Pre-commit Checks**: ESLint, TypeScript compilation, OpenAPI validation
|
||||
|
||||
## Making Code Changes
|
||||
|
||||
### Adding New API Endpoint
|
||||
1. Create controller in `server/core/controllers/api/`
|
||||
2. Add validation middleware in `server/core/middlewares/validators/`
|
||||
3. Register route in `server/core/controllers/api/index.ts`
|
||||
4. Update shared types in `packages/models/`
|
||||
5. Add OpenAPI documentation tags
|
||||
6. Write tests in `packages/tests/src/api/`
|
||||
|
||||
### Common Patterns to Follow
|
||||
```typescript
|
||||
// Controller pattern
|
||||
import express from 'express'
|
||||
import { apiRateLimiter, asyncMiddleware } from '../../middlewares/index.js'
|
||||
|
||||
const router = express.Router()
|
||||
router.use(apiRateLimiter) // ALWAYS include rate limiting
|
||||
|
||||
router.get('/:id',
|
||||
validationMiddleware, // ALWAYS validate inputs
|
||||
asyncMiddleware(handler) // ALWAYS wrap async handlers
|
||||
)
|
||||
```
|
||||
|
||||
### Database Changes
|
||||
1. Create/modify Sequelize model in `server/core/models/`
|
||||
2. Generate migration in `server/core/initializers/migrations/`
|
||||
3. Update shared types in `packages/models/`
|
||||
4. Run `npm run build:server` to compile
|
||||
|
||||
## Validation Steps Before PR
|
||||
|
||||
1. **Build**: `npm run build` (must succeed)
|
||||
2. **Lint**: `npm run lint` (must pass without errors)
|
||||
5. **OpenAPI**: Validate if API changes made
|
||||
|
||||
## Common Error Solutions
|
||||
|
||||
**Build Errors:**
|
||||
- "Cannot find module": Run `yarn install --frozen-lockfile`
|
||||
- "PostgreSQL connection": Check PostgreSQL is running and extensions installed
|
||||
- TypeScript errors: Check Node.js version (must be >=20.x)
|
||||
|
||||
**Test Errors:**
|
||||
- Permission denied: Ensure PostgreSQL user has createdb/superuser rights
|
||||
- Port conflicts: Stop other PeerTube instances
|
||||
- Import test failures: Set `DISABLE_HTTP_IMPORT_TESTS=true`
|
||||
|
||||
**Development Issues:**
|
||||
- "Client dist not found": Run `npm run build:client` (only if working on client features)
|
||||
- Redis connection: Ensure Redis server is running
|
||||
- Hot reload not working: Kill all Node processes and restart
|
||||
|
||||
## Trust These Instructions
|
||||
|
||||
These instructions have been validated against the current codebase. Only search for additional information if:
|
||||
- Commands fail with updated error messages
|
||||
- New dependencies are added to package.json
|
||||
- Build system changes are detected
|
||||
- You need specific implementation details not covered here
|
||||
|
||||
Focus on server-side TypeScript development in `server/core/` and ignore the `client/` directory unless explicitly working on frontend integration.
|
63
CHANGELOG.md
63
CHANGELOG.md
|
@ -1,5 +1,68 @@
|
|||
# Changelog
|
||||
|
||||
## v7.3.0-rc.1
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* Minimum supported NodeJS version is `20.17`
|
||||
|
||||
### NGINX
|
||||
|
||||
* Disable request buffering on upload endpoints to fix HTTP request timeouts: https://github.com/Chocobozzz/PeerTube/commit/d1a35e8421195088e2754b787c4af1e765b9eaa9
|
||||
|
||||
### Plugins/Themes/Embed API
|
||||
|
||||
* Add server API (https://docs.joinpeertube.org/api/plugins):
|
||||
* Support `externalRedirectUri` for `registerExternalAuth` so PeerTube redirects users on another URL set by the plugin
|
||||
* If your plugin uses `filter:email.template-path.result` server hook: emails now use Handlebars template engine instead of Pug template engine
|
||||
|
||||
### Features
|
||||
|
||||
* :tada: Emails can now be translated :tada: Check the [translation documentation](https://docs.joinpeertube.org/support/doc/translation) to help us translate emails in your language!
|
||||
* :tada: Introduce a web configuration wizard to help administrators to configure their instance automatically :tada:
|
||||
* The wizard appears once the administrators have logged in following the installation of the PeerTube instance
|
||||
* Admins can also run the wizard via a button in the web admin config
|
||||
* The main instance information (e.g. name, short description, logo, primary colour) can be entered using the wizard.
|
||||
* It also helps the admin to apply a configuration depending on the instance type (community-based, institutional, private)
|
||||
* :tada: Redesign the admin config to use a lateral menu for navigating between subsections :tada:
|
||||
* Add a new *Customization* page to easily change the main colors and shape of the client interface
|
||||
* Add a new *Logo* page where admins can upload logos/favicon and social media images for their instances
|
||||
* Add an option to set the default licence, privacy and comments policy when publishing videos
|
||||
* The email prefix and body can now be changed in the web admin config. These configurations also support the `{{instanceName}}` template variable, which is replaced by the instance name
|
||||
* Improve admin federation control:
|
||||
* Add the ability for admins to completely disable remote subscriptions to local channels
|
||||
* Admins can also set up automatic rejection of video comments from remote instances
|
||||
* Add 2FA column information in admin users overview table
|
||||
* Display remote runner version in admin
|
||||
* Add ability for users to set the planned date of a live. These lives are displayed when browsing videos [#7144](https://github.com/Chocobozzz/PeerTube/pull/7144)
|
||||
* Improve data tables UX/UI
|
||||
* Improve account/channel playlists management:
|
||||
* Use a data table to manage account and channel playlists
|
||||
* Allow to manually set the order of the public playlists displayed in a channel
|
||||
* Improve sensitive content warning in embed player
|
||||
* Improve audio transcoding quality, especially with FLAC input
|
||||
* Support Creole French languages in video language metadata
|
||||
* Add ability for users to list and revoke token sessions
|
||||
* Support *Free of known copyright restrictions* and *Copyrighted - All Rights Reserved* video licence metadata
|
||||
* Play/pause the video player using `k` key
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix ActivityPub audience for unlisted videos
|
||||
* Use an array of URL in `attributedTo` ActivityPub field
|
||||
* Prefer `og:image` instead of `og:image:url`
|
||||
* Better thumbnail blur for sensitive content [#7105](https://github.com/Chocobozzz/PeerTube/pull/7105)
|
||||
* Prefer `allow="fullscreen"` for video embed `iframe` [#7043](https://github.com/Chocobozzz/PeerTube/pull/7043)
|
||||
* Respect the sensitive content policy, even for videos owned by the user
|
||||
* Fix the issue of the scroll position not being restored when pages load slowly [#7143](https://github.com/Chocobozzz/PeerTube/pull/7143)
|
||||
* Fix remote actor follow counter after a local subscription
|
||||
* Fix reloading videos in *Browser videos* when the link only changes query parameters
|
||||
* Add stall job check for remote studio and transcription runner jobs
|
||||
* Prevent metric warning for redundancy gauge
|
||||
* Fix disabling *Wait transcoding* checkbox
|
||||
* Correctly import new elements of a playlist in channel synchronization
|
||||
|
||||
|
||||
## v7.2.3
|
||||
|
||||
### SECURITY
|
||||
|
|
104
client/.github/instructions/angular.instructions.md
vendored
Normal file
104
client/.github/instructions/angular.instructions.md
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
description: 'Angular-specific coding standards and best practices'
|
||||
applyTo: 'src/app/**/*.ts, src/app/**/*.html, src/app/**/*.scss, src/app/**/*.css'
|
||||
---
|
||||
|
||||
# Angular Development Instructions
|
||||
|
||||
Instructions for generating high-quality Angular applications with TypeScript, using Angular Signals for state management, adhering to Angular best practices as outlined at https://angular.dev.
|
||||
|
||||
## Project Context
|
||||
- Latest Angular version (use standalone components by default)
|
||||
- TypeScript for type safety
|
||||
- Angular CLI for project setup and scaffolding
|
||||
- Follow Angular Style Guide (https://angular.dev/style-guide)
|
||||
- Use Angular Material or other modern UI libraries for consistent styling (if specified)
|
||||
|
||||
## Development Standards
|
||||
|
||||
### Architecture
|
||||
- Use standalone components unless modules are explicitly required
|
||||
- Organize code by feature modules or domains for scalability
|
||||
- Implement lazy loading for feature modules to optimize performance
|
||||
- Use Angular's built-in dependency injection system effectively
|
||||
- Structure components with a clear separation of concerns (smart vs. presentational components)
|
||||
|
||||
### TypeScript
|
||||
- Enable strict mode in `tsconfig.json` for type safety
|
||||
- Define clear interfaces and types for components, services, and models
|
||||
- Use type guards and union types for robust type checking
|
||||
- Implement proper error handling with RxJS operators (e.g., `catchError`)
|
||||
- Use typed forms (e.g., `FormGroup`, `FormControl`) for reactive forms
|
||||
|
||||
### Component Design
|
||||
- Follow Angular's component lifecycle hooks best practices
|
||||
- When using Angular >= 19, Use `input()` `output()`, `viewChild()`, `viewChildren()`, `contentChild()` and `viewChildren()` functions instead of decorators; otherwise use decorators
|
||||
- Leverage Angular's change detection strategy (default or `OnPush` for performance)
|
||||
- Keep templates clean and logic in component classes or services
|
||||
- Use Angular directives and pipes for reusable functionality
|
||||
|
||||
### Styling
|
||||
- Use Angular's component-level CSS encapsulation (default: ViewEncapsulation.Emulated)
|
||||
- Prefer SCSS for styling with consistent theming
|
||||
- Implement responsive design using CSS Grid, Flexbox, or Angular CDK Layout utilities
|
||||
- Follow Angular Material's theming guidelines if used
|
||||
- Maintain accessibility (a11y) with ARIA attributes and semantic HTML
|
||||
|
||||
### State Management
|
||||
- Use Angular Signals for reactive state management in components and services
|
||||
- Leverage `signal()`, `computed()`, and `effect()` for reactive state updates
|
||||
- Use writable signals for mutable state and computed signals for derived state
|
||||
- Handle loading and error states with signals and proper UI feedback
|
||||
- Use Angular's `AsyncPipe` to handle observables in templates when combining signals with RxJS
|
||||
|
||||
### Data Fetching
|
||||
- Use Angular's `HttpClient` for API calls with proper typing
|
||||
- Implement RxJS operators for data transformation and error handling
|
||||
- Use Angular's `inject()` function for dependency injection in standalone components
|
||||
- Implement caching strategies (e.g., `shareReplay` for observables)
|
||||
- Store API response data in signals for reactive updates
|
||||
- Handle API errors with global interceptors for consistent error handling
|
||||
|
||||
### Security
|
||||
- Sanitize user inputs using Angular's built-in sanitization
|
||||
- Implement route guards for authentication and authorization
|
||||
- Use Angular's `HttpInterceptor` for CSRF protection and API authentication headers
|
||||
- Validate form inputs with Angular's reactive forms and custom validators
|
||||
- Follow Angular's security best practices (e.g., avoid direct DOM manipulation)
|
||||
|
||||
### Performance
|
||||
- Enable production builds with `ng build --prod` for optimization
|
||||
- Use lazy loading for routes to reduce initial bundle size
|
||||
- Optimize change detection with `OnPush` strategy and signals for fine-grained reactivity
|
||||
- Use trackBy in `ngFor` loops to improve rendering performance
|
||||
- Implement server-side rendering (SSR) or static site generation (SSG) with Angular Universal (if specified)
|
||||
|
||||
### Testing
|
||||
- Write unit tests for components, services, and pipes using Jasmine and Karma
|
||||
- Use Angular's `TestBed` for component testing with mocked dependencies
|
||||
- Test signal-based state updates using Angular's testing utilities
|
||||
- Write end-to-end tests with Cypress or Playwright (if specified)
|
||||
- Mock HTTP requests using `HttpClientTestingModule`
|
||||
- Ensure high test coverage for critical functionality
|
||||
|
||||
## Implementation Process
|
||||
1. Plan project structure and feature modules
|
||||
2. Define TypeScript interfaces and models
|
||||
3. Scaffold components, services, and pipes using Angular CLI
|
||||
4. Implement data services and API integrations with signal-based state
|
||||
5. Build reusable components with clear inputs and outputs
|
||||
6. Add reactive forms and validation
|
||||
7. Apply styling with SCSS and responsive design
|
||||
8. Implement lazy-loaded routes and guards
|
||||
9. Add error handling and loading states using signals
|
||||
10. Write unit and end-to-end tests
|
||||
11. Optimize performance and bundle size
|
||||
|
||||
## Additional Guidelines
|
||||
- Follow Angular's naming conventions (e.g., `feature.component.ts`, `feature.service.ts`)
|
||||
- Use Angular CLI commands for generating boilerplate code
|
||||
- Document components and services with clear JSDoc comments
|
||||
- Ensure accessibility compliance (WCAG 2.1) where applicable
|
||||
- Use Angular's built-in i18n for internationalization (if specified)
|
||||
- Keep code DRY by creating reusable utilities and shared modules
|
||||
- Use signals consistently for state management to ensure reactive updates
|
112
client/.github/instructions/copilot-instructions.md
vendored
Normal file
112
client/.github/instructions/copilot-instructions.md
vendored
Normal file
|
@ -0,0 +1,112 @@
|
|||
# PeerTube Client Development Instructions for Coding Agents
|
||||
|
||||
## Client Overview
|
||||
|
||||
This is the Angular frontend for PeerTube, a decentralized video hosting platform. The client is built with Angular 20+, TypeScript, and SCSS. It communicates with the PeerTube server API and provides the web interface for users, administrators, and content creators.
|
||||
|
||||
**Key Technologies:**
|
||||
- Angular 20+ with standalone components
|
||||
- TypeScript 5+
|
||||
- SCSS for styling
|
||||
- RxJS for reactive programming
|
||||
- PrimeNg and Bootstrap for UI components
|
||||
- WebdriverIO for E2E testing
|
||||
- Angular CLI
|
||||
|
||||
## Client Build and Development Commands
|
||||
|
||||
### Prerequisites (for client development)
|
||||
- Node.js 20+
|
||||
- yarn 1
|
||||
- Running PeerTube server (see ../server instructions)
|
||||
|
||||
### Essential Client Commands
|
||||
|
||||
```bash
|
||||
# From the client directory:
|
||||
cd /client
|
||||
|
||||
# 1. Install dependencies (ALWAYS first)
|
||||
yarn install --pure-lockfile
|
||||
|
||||
# 2. Development server with hot reload
|
||||
npm run dev
|
||||
|
||||
# 3. Build for production
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Client Testing Commands
|
||||
```bash
|
||||
# From client directory:
|
||||
npm run lint # ESLint for client code
|
||||
```
|
||||
|
||||
### Common Client Issues and Solutions
|
||||
|
||||
**Angular Build Failures:**
|
||||
- Always run `yarn install --pure-lockfile` after pulling changes
|
||||
- Clear `node_modules` and reinstall if dependency errors occur
|
||||
- Build may fail on memory issues: `NODE_OPTIONS="--max-old-space-size=4096" npm run build`
|
||||
- Check TypeScript errors carefully - Angular is strict about types
|
||||
|
||||
**Development Server Issues:**
|
||||
- Default port is 3000, ensure it's not in use
|
||||
- Hot reload may fail on file permission issues
|
||||
- Clear browser cache if changes don't appear
|
||||
|
||||
## Client Architecture and File Structure
|
||||
|
||||
### Client Directory Structure
|
||||
```
|
||||
/src/
|
||||
/app/
|
||||
+admin/ # Admin interface components
|
||||
+my-account/ # User account management pages
|
||||
+my-library/ # User's videos, playlists, subscriptions
|
||||
+search/ # Search functionality and results
|
||||
+shared/ # Shared Angular components, services, pipes
|
||||
+standalone/ # Standalone Angular components
|
||||
+videos/ # Video-related components (watch, upload, etc.)
|
||||
/core/ # Core services (auth, server, notifications)
|
||||
/helpers/ # Utility functions and helpers
|
||||
/menu/ # Navigation menu components
|
||||
/assets/ # Static assets (images, icons, etc.)
|
||||
/environments/ # Environment configurations
|
||||
/locale/ # Internationalization files
|
||||
/sass/ # Global SCSS styles
|
||||
```
|
||||
|
||||
### Key Client Configuration Files
|
||||
|
||||
- `angular.json` - Angular CLI workspace configuration
|
||||
- `tsconfig.json` - TypeScript configuration for client
|
||||
- `e2e/wdio*.conf.js` - WebdriverIO E2E test configurations
|
||||
- `src/environments/` - Environment-specific configurations
|
||||
|
||||
### Shared Code with Server (`../shared/`)
|
||||
|
||||
The client imports TypeScript models and utilities from the shared directory:
|
||||
- `../shared/models/` - Data models (Video, User, Channel, etc.). Import these in client code: `import { Video } from '@peertube/peertube-models'`
|
||||
- `../shared/core-utils/` - Utility functions shared between client/server. Import these in client code: `import { ... } from '@peertube/peertube-core-utils'`
|
||||
-
|
||||
|
||||
## Client Development Workflow
|
||||
|
||||
### Making Client Changes
|
||||
|
||||
1. **Angular Components:** Create/modify in `/src/app/` following existing patterns
|
||||
2. **Shared Components:** Reusable components go in `/src/app/shared/`
|
||||
3. **Services:** Core services in `/src/app/core/`, feature services with components
|
||||
4. **Styles:** Component styles in `.scss` files, global styles in `/src/sass/`
|
||||
5. **Assets:** Images, icons in `/src/assets/`
|
||||
6. **Routing:** Routes defined in feature modules or `app-routing.module.ts`
|
||||
|
||||
## Trust These Instructions
|
||||
|
||||
These instructions are comprehensive and tested specifically for client development. Only search for additional information if:
|
||||
1. Commands fail despite following instructions exactly
|
||||
2. New error messages appear that aren't documented here
|
||||
3. You need specific Angular implementation details not covered above
|
||||
|
||||
For server-side questions, refer to the server instructions in `../.github/copilot-instructions.md`.
|
|
@ -244,7 +244,7 @@
|
|||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "120kb"
|
||||
"maximumError": "140kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
|
|
|
@ -2,25 +2,19 @@ import { NSFWPolicyType } from '@peertube/peertube-models'
|
|||
import { browserSleep, go, setCheckboxEnabled } from '../utils'
|
||||
|
||||
export class AdminConfigPage {
|
||||
async navigateTo (tab: 'instance-homepage' | 'basic-configuration' | 'instance-information' | 'live') {
|
||||
const waitTitles = {
|
||||
'instance-homepage': 'INSTANCE HOMEPAGE',
|
||||
'basic-configuration': 'APPEARANCE',
|
||||
'instance-information': 'INSTANCE',
|
||||
'live': 'LIVE'
|
||||
async navigateTo (page: 'information' | 'live' | 'general' | 'homepage') {
|
||||
const url = '/admin/settings/config/' + page
|
||||
|
||||
const currentUrl = await browser.getUrl()
|
||||
if (!currentUrl.endsWith(url)) {
|
||||
await go(url)
|
||||
}
|
||||
|
||||
const url = '/admin/settings/config/edit-custom#' + tab
|
||||
|
||||
if (await browser.getUrl() !== url) {
|
||||
await go('/admin/settings/config/edit-custom#' + tab)
|
||||
}
|
||||
|
||||
await $('h2=' + waitTitles[tab]).waitForDisplayed()
|
||||
await $('a.active[href="' + url + '"]').waitForDisplayed()
|
||||
}
|
||||
|
||||
async updateNSFWSetting (newValue: NSFWPolicyType) {
|
||||
await this.navigateTo('instance-information')
|
||||
await this.navigateTo('information')
|
||||
|
||||
const elem = $(`#instanceDefaultNSFWPolicy-${newValue} + label`)
|
||||
|
||||
|
@ -32,25 +26,25 @@ export class AdminConfigPage {
|
|||
}
|
||||
|
||||
async updateHomepage (newValue: string) {
|
||||
await this.navigateTo('instance-homepage')
|
||||
await this.navigateTo('homepage')
|
||||
|
||||
return $('#instanceCustomHomepageContent').setValue(newValue)
|
||||
return $('#homepageContent').setValue(newValue)
|
||||
}
|
||||
|
||||
async toggleSignup (enabled: boolean) {
|
||||
await this.navigateTo('basic-configuration')
|
||||
await this.navigateTo('general')
|
||||
|
||||
return setCheckboxEnabled('signupEnabled', enabled)
|
||||
}
|
||||
|
||||
async toggleSignupApproval (required: boolean) {
|
||||
await this.navigateTo('basic-configuration')
|
||||
await this.navigateTo('general')
|
||||
|
||||
return setCheckboxEnabled('signupRequiresApproval', required)
|
||||
}
|
||||
|
||||
async toggleSignupEmailVerification (required: boolean) {
|
||||
await this.navigateTo('basic-configuration')
|
||||
await this.navigateTo('general')
|
||||
|
||||
return setCheckboxEnabled('signupRequiresEmailVerification', required)
|
||||
}
|
||||
|
@ -62,11 +56,18 @@ export class AdminConfigPage {
|
|||
}
|
||||
|
||||
async save () {
|
||||
const button = $('input[type=submit]')
|
||||
const button = $('my-admin-save-bar .save-button')
|
||||
|
||||
try {
|
||||
await button.waitForClickable()
|
||||
} catch {
|
||||
// The config may have not been changed
|
||||
return
|
||||
} finally {
|
||||
await browserSleep(1000) // Wait for the button to be clickable
|
||||
}
|
||||
|
||||
await button.waitForClickable()
|
||||
await button.click()
|
||||
|
||||
await browserSleep(1000)
|
||||
await button.waitForClickable({ reverse: true })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ export class LoginPage {
|
|||
}
|
||||
|
||||
async logout () {
|
||||
const loggedInDropdown = $('.logged-in-container .logged-in-info')
|
||||
const loggedInDropdown = $('.logged-in-container .dropdown-toggle')
|
||||
|
||||
await loggedInDropdown.waitForClickable()
|
||||
await loggedInDropdown.click()
|
||||
|
|
|
@ -185,7 +185,7 @@ export class MyAccountPage {
|
|||
const playlist = () => {
|
||||
return $$('my-video-playlist-miniature')
|
||||
.filter(async e => {
|
||||
const t = await e.$('.miniature-name').getText()
|
||||
const t = await e.$('img').getAttribute('aria-label')
|
||||
|
||||
return t.includes(name)
|
||||
})
|
||||
|
|
|
@ -72,15 +72,15 @@ export class PlayerPage {
|
|||
}
|
||||
|
||||
getNSFWContentText () {
|
||||
return $('.video-js .nsfw-content').getText()
|
||||
return $('.video-js .nsfw-info').getText()
|
||||
}
|
||||
|
||||
getNSFWMoreContent () {
|
||||
return $('.video-js .nsfw-more-content')
|
||||
getNSFWDetailsContent () {
|
||||
return $('.video-js .nsfw-details-content')
|
||||
}
|
||||
|
||||
getMoreNSFWInfoButton () {
|
||||
return $('.video-js .nsfw-container button')
|
||||
return $('.video-js .nsfw-info button')
|
||||
}
|
||||
|
||||
async hasPoster () {
|
||||
|
|
|
@ -76,7 +76,7 @@ export abstract class VideoManage {
|
|||
await input.waitForClickable()
|
||||
await input.click()
|
||||
|
||||
const nextMonth = $('.p-datepicker-next')
|
||||
const nextMonth = $('.p-datepicker-next-button')
|
||||
await nextMonth.click()
|
||||
|
||||
await $('.p-datepicker-calendar td[aria-label="1"] > span').click()
|
||||
|
@ -135,7 +135,13 @@ export abstract class VideoManage {
|
|||
}
|
||||
|
||||
protected async goOnPage (page: 'Main information' | 'Moderation' | 'Live settings') {
|
||||
const el = $('my-video-manage-container .menu').$('*=' + page)
|
||||
const urls = {
|
||||
'Main information': '',
|
||||
'Moderation': 'moderation',
|
||||
'Live settings': 'live'
|
||||
}
|
||||
|
||||
const el = $(`my-video-manage-container .menu a[href*="/${urls[page]}"]`)
|
||||
await el.waitForClickable()
|
||||
await el.click()
|
||||
}
|
||||
|
|
|
@ -167,6 +167,7 @@ export class VideoWatchPage {
|
|||
|
||||
async clickOnMoreDropdownIcon () {
|
||||
const dropdown = $('my-video-actions-dropdown .action-button')
|
||||
await dropdown.scrollIntoView({ block: 'center' })
|
||||
await dropdown.click()
|
||||
|
||||
await $('.dropdown-menu.show .dropdown-item').waitForDisplayed()
|
||||
|
@ -176,8 +177,12 @@ export class VideoWatchPage {
|
|||
// Playlists
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
clickOnSave () {
|
||||
return $('.action-button-save').click()
|
||||
async clickOnSave () {
|
||||
const button = $('.action-button-save')
|
||||
|
||||
await button.scrollIntoView({ block: 'center' })
|
||||
|
||||
return button.click()
|
||||
}
|
||||
|
||||
async createPlaylist (name: string) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { PlayerPage } from '../po/player.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari } from '../utils'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari, prepareWebBrowser } from '../utils'
|
||||
|
||||
describe('Live all workflow', () => {
|
||||
let videoWatchPage: VideoWatchPage
|
||||
|
@ -10,9 +10,7 @@ describe('Live all workflow', () => {
|
|||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
playerPage = new PlayerPage()
|
||||
|
||||
if (!isMobileDevice()) {
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should go to the live page', async () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { PlayerPage } from '../po/player.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari } from '../utils'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari, prepareWebBrowser } from '../utils'
|
||||
|
||||
async function checkCorrectlyPlay (playerPage: PlayerPage) {
|
||||
await playerPage.playAndPauseVideo(false, 2)
|
||||
|
@ -22,9 +22,7 @@ describe('Private videos all workflow', () => {
|
|||
loginPage = new LoginPage(isMobileDevice())
|
||||
playerPage = new PlayerPage()
|
||||
|
||||
if (!isMobileDevice()) {
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should log in', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { VideoListPage } from '../po/video-list.po'
|
|||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoUpdatePage } from '../po/video-update.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isIOS, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { FIXTURE_URLS, go, isIOS, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
function isUploadUnsupported () {
|
||||
if (isMobileDevice() || isSafari()) {
|
||||
|
@ -53,9 +53,7 @@ describe('Videos all workflow', () => {
|
|||
playerPage = new PlayerPage()
|
||||
videoListPage = new VideoListPage(isMobileDevice(), isSafari())
|
||||
|
||||
if (!isMobileDevice()) {
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should log in', async () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Custom server defaults', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -15,7 +15,7 @@ describe('Custom server defaults', () => {
|
|||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Publish default values', function () {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { VideoListPage } from '../po/video-list.po'
|
|||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoSearchPage } from '../po/video-search.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('NSFW', () => {
|
||||
let videoListPage: VideoListPage
|
||||
|
@ -102,6 +102,8 @@ describe('NSFW', () => {
|
|||
|
||||
for (const video of videos) {
|
||||
await videoSearchPage.search(video)
|
||||
|
||||
await browser.saveScreenshot(getScreenshotPath('before-test.png'))
|
||||
await checkVideo({ policy, videoName: video, nsfwTooltip })
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +155,7 @@ describe('NSFW', () => {
|
|||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
anonymousSettingsPage = new AnonymousSettingsPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Preparation', function () {
|
||||
|
@ -265,10 +267,10 @@ describe('NSFW', () => {
|
|||
expect(await moreButton.isDisplayed()).toBeTruthy()
|
||||
|
||||
await moreButton.click()
|
||||
await playerPage.getNSFWMoreContent().waitForDisplayed()
|
||||
await playerPage.getNSFWDetailsContent().waitForDisplayed()
|
||||
|
||||
const moreContent = await playerPage.getNSFWMoreContent().getText()
|
||||
expect(moreContent).toContain('Violence')
|
||||
const moreContent = await playerPage.getNSFWDetailsContent().getText()
|
||||
expect(moreContent).toContain('Potentially violent content')
|
||||
expect(moreContent).toContain('bibi is violent')
|
||||
}
|
||||
|
||||
|
|
119
client/e2e/src/suites-local/page-crash.e2e-spec.ts
Normal file
119
client/e2e/src/suites-local/page-crash.e2e-spec.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
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, go, isMobileDevice, isSafari, prepareWebBrowser, selectCustomSelect, waitServerUp } from '../utils'
|
||||
|
||||
// These tests help to notice crash with invalid translated strings
|
||||
describe('Page crash', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let adminConfigPage: AdminConfigPage
|
||||
|
||||
const languages = [
|
||||
'العربية',
|
||||
'Català',
|
||||
'Čeština',
|
||||
'Deutsch',
|
||||
'ελληνικά',
|
||||
'Esperanto',
|
||||
'Español',
|
||||
'Euskara',
|
||||
'فارسی',
|
||||
'Suomi',
|
||||
'Français',
|
||||
'Gàidhlig',
|
||||
'Galego',
|
||||
'Hrvatski',
|
||||
'Magyar',
|
||||
'Íslenska',
|
||||
'Italiano',
|
||||
'日本語',
|
||||
'Taqbaylit',
|
||||
'Norsk bokmål',
|
||||
'Nederlands',
|
||||
'Norsk nynorsk',
|
||||
'Occitan',
|
||||
'Polski',
|
||||
'Português (Brasil)',
|
||||
'Português (Portugal)',
|
||||
'Pусский',
|
||||
'Slovenčina',
|
||||
'Shqip',
|
||||
'Svenska',
|
||||
'ไทย',
|
||||
'Toki Pona',
|
||||
'Türkçe',
|
||||
'украї́нська мо́ва',
|
||||
'Tiếng Việt',
|
||||
'简体中文(中国)',
|
||||
'繁體中文(台灣)'
|
||||
]
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
|
||||
adminConfigPage = new AdminConfigPage()
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await prepareWebBrowser()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
||||
for (const language of languages) {
|
||||
describe('For language: ' + language, () => {
|
||||
it('Should change the language', async function () {
|
||||
await go('/')
|
||||
|
||||
await $('.settings-button').waitForClickable()
|
||||
await $('.settings-button').click()
|
||||
|
||||
await selectCustomSelect('language', language)
|
||||
|
||||
await $('my-user-interface-settings .primary-button').waitForClickable()
|
||||
await $('my-user-interface-settings .primary-button').click()
|
||||
})
|
||||
|
||||
it('Should upload and watch a video', async function () {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video3.mp4')
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
})
|
||||
|
||||
it('Should set a homepage', async function () {
|
||||
await adminConfigPage.updateHomepage('My custom homepage content')
|
||||
await adminConfigPage.save()
|
||||
|
||||
// All tests
|
||||
await go('/home')
|
||||
|
||||
await $('*=My custom homepage content').waitForDisplayed()
|
||||
})
|
||||
|
||||
it('Should go on client pages and not crash', async function () {
|
||||
await $('a[href="/videos/overview"]').waitForClickable()
|
||||
await $('a[href="/videos/overview"]').click()
|
||||
|
||||
await $('my-video-overview').waitForExist()
|
||||
})
|
||||
|
||||
it('Should go on videos from subscriptions pages', async function () {
|
||||
await $('a[href="/videos/subscriptions"]').waitForClickable()
|
||||
await $('a[href="/videos/subscriptions"]').click()
|
||||
|
||||
await $('my-videos-user-subscriptions').waitForExist()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath(`after-page-crash-test-${language}.png`))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
|
@ -3,7 +3,7 @@ import { LoginPage } from '../po/login.po'
|
|||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Player settings', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -21,7 +21,7 @@ describe('Player settings', () => {
|
|||
myAccountPage = new MyAccountPage()
|
||||
anonymousSettingsPage = new AnonymousSettingsPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('P2P', function () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AdminPluginPage } from '../po/admin-plugin.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { getCheckbox, getScreenshotPath, isMobileDevice, waitServerUp } from '../utils'
|
||||
import { getCheckbox, getScreenshotPath, isMobileDevice, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Plugins', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -28,7 +28,7 @@ describe('Plugins', () => {
|
|||
videoPublishPage = new VideoPublishPage()
|
||||
adminPluginPage = new AdminPluginPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should install hello world plugin', async () => {
|
||||
|
|
|
@ -2,7 +2,7 @@ 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'
|
||||
import { getScreenshotPath, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Publish live', function () {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -18,7 +18,7 @@ describe('Publish live', function () {
|
|||
adminConfigPage = new AdminConfigPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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'
|
||||
import { getScreenshotPath, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Publish video', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -15,7 +15,7 @@ describe('Publish video', () => {
|
|||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
go,
|
||||
isMobileDevice,
|
||||
MockSMTPServer,
|
||||
prepareWebBrowser,
|
||||
waitServerUp
|
||||
} from '../utils'
|
||||
|
||||
|
@ -76,6 +77,7 @@ describe('Signup', () => {
|
|||
}) {
|
||||
await loginPage.loginAsRootUser()
|
||||
|
||||
// Ensure we change the state of the form to "dirty" so we can save the form
|
||||
await adminConfigPage.toggleSignup(options.enabled)
|
||||
|
||||
if (options.enabled) {
|
||||
|
@ -104,7 +106,7 @@ describe('Signup', () => {
|
|||
signupPage = new SignupPage()
|
||||
adminRegistrationPage = new AdminRegistrationPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Signup disabled', function () {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
go,
|
||||
isMobileDevice,
|
||||
MockSMTPServer,
|
||||
prepareWebBrowser,
|
||||
waitServerUp
|
||||
} from '../utils'
|
||||
|
||||
|
@ -29,7 +30,7 @@ describe('User settings', () => {
|
|||
|
||||
await MockSMTPServer.Instance.collectEmails(await getEmailPort(), emails)
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Email', function () {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { PlayerPage } from '../po/player.po'
|
|||
import { SignupPage } from '../po/signup.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Password protected videos', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -50,7 +50,7 @@ describe('Password protected videos', () => {
|
|||
playerPage = new PlayerPage()
|
||||
myAccountPage = new MyAccountPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Owner', function () {
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
async function browserSleep (amount: number) {
|
||||
export async function browserSleep (amount: number) {
|
||||
await browser.pause(amount)
|
||||
}
|
||||
|
||||
function isMobileDevice () {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function isMobileDevice () {
|
||||
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
|
||||
|
||||
return platformName === 'android' || platformName === 'ios'
|
||||
}
|
||||
|
||||
function isAndroid () {
|
||||
export function isAndroid () {
|
||||
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
|
||||
|
||||
return platformName === 'android'
|
||||
}
|
||||
|
||||
function isSafari () {
|
||||
export function isSafari () {
|
||||
return browser.capabilities['browserName'] &&
|
||||
browser.capabilities['browserName'].toLowerCase() === 'safari'
|
||||
browser.capabilities['browserName'].toLowerCase() === 'safari'
|
||||
}
|
||||
|
||||
function isIOS () {
|
||||
export function isIOS () {
|
||||
return isMobileDevice() && isSafari()
|
||||
}
|
||||
|
||||
async function go (url: string) {
|
||||
export async function go (url: string) {
|
||||
await browser.url(url)
|
||||
|
||||
await browser.execute(() => {
|
||||
|
@ -33,7 +35,20 @@ async function go (url: string) {
|
|||
})
|
||||
}
|
||||
|
||||
async function waitServerUp () {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function prepareWebBrowser () {
|
||||
if (isMobileDevice()) return
|
||||
|
||||
// Window size on chromium doesn't seem to work in "new" headless mode
|
||||
if (process.env.MOZ_HEADLESS_WIDTH) {
|
||||
await browser.setWindowSize(+process.env.MOZ_HEADLESS_WIDTH, +process.env.MOZ_HEADLESS_HEIGHT)
|
||||
}
|
||||
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
|
||||
export async function waitServerUp () {
|
||||
await browser.waitUntil(async () => {
|
||||
await go('/')
|
||||
await browserSleep(500)
|
||||
|
@ -41,13 +56,3 @@ async function waitServerUp () {
|
|||
return $('<my-app>').isDisplayed()
|
||||
}, { timeout: 20 * 1000 })
|
||||
}
|
||||
|
||||
export {
|
||||
isMobileDevice,
|
||||
isSafari,
|
||||
isIOS,
|
||||
isAndroid,
|
||||
waitServerUp,
|
||||
go,
|
||||
browserSleep
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ export async function clickOnRadio (name: string) {
|
|||
export async function selectCustomSelect (id: string, valueLabel: string) {
|
||||
const wrapper = $(`[formcontrolname=${id}] span[role=combobox]`)
|
||||
|
||||
await wrapper.waitForExist()
|
||||
await wrapper.scrollIntoView({ block: 'center' })
|
||||
await wrapper.waitForClickable()
|
||||
await wrapper.click()
|
||||
|
||||
|
|
|
@ -95,18 +95,18 @@ module.exports = {
|
|||
{
|
||||
browserName: 'Chrome',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Latest Chrome Android', deviceName: 'Samsung Galaxy S8', osVersion: '7.0' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Latest Chrome Android', deviceName: 'Samsung Galaxy S10', osVersion: '9.0' })
|
||||
},
|
||||
{
|
||||
browserName: 'Safari',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 11', osVersion: '14' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 12', osVersion: '14' })
|
||||
},
|
||||
|
||||
{
|
||||
browserName: 'Safari',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad Pro 11 2020', osVersion: '14' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad Pro 12.9 2021', osVersion: '14' })
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -121,7 +121,8 @@ module.exports = {
|
|||
|
||||
services: [
|
||||
[
|
||||
'browserstack', { browserstackLocal: true }
|
||||
'browserstack',
|
||||
{ browserstackLocal: true }
|
||||
]
|
||||
],
|
||||
|
||||
|
@ -174,6 +175,5 @@ module.exports = {
|
|||
|
||||
onPrepare: onBrowserStackPrepare,
|
||||
onComplete: onBrowserStackComplete
|
||||
|
||||
} as WebdriverIO.Config
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ module.exports = {
|
|||
'browserName': 'chrome',
|
||||
'acceptInsecureCerts': true,
|
||||
'goog:chromeOptions': {
|
||||
args: [ '--disable-gpu', windowSizeArg ],
|
||||
args: [ '--headless', '--disable-gpu', windowSizeArg ],
|
||||
prefs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "7.2.3",
|
||||
"version": "7.3.0-rc.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
|
|
|
@ -38,6 +38,7 @@ input[type="checkbox"] {
|
|||
|
||||
my-select-checkbox,
|
||||
my-select-options,
|
||||
my-select-player-theme,
|
||||
my-select-custom-value {
|
||||
display: block;
|
||||
|
||||
|
|
|
@ -7,26 +7,32 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="theme">
|
||||
<div class="form-group">
|
||||
<label i18n for="themeDefault">Theme</label>
|
||||
<div class="form-group" formGroupName="theme">
|
||||
<label i18n for="themeDefault">Theme</label>
|
||||
|
||||
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
||||
</div>
|
||||
</ng-container>
|
||||
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
||||
</div>
|
||||
|
||||
<ng-container formGroupName="client">
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="miniature">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientVideosMiniaturePreferAuthorDisplayName"
|
||||
formControlName="preferAuthorDisplayName"
|
||||
i18n-labelText
|
||||
labelText="Prefer author display name in video miniature"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="form-group" formGroupName="miniature">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientVideosMiniaturePreferAuthorDisplayName"
|
||||
formControlName="preferAuthorDisplayName"
|
||||
i18n-labelText
|
||||
labelText="Prefer author display name in video miniature"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="defaults">
|
||||
<ng-container formGroupName="player">
|
||||
<div class="form-group">
|
||||
<label i18n for="defaultsPlayerTheme">Player Theme</label>
|
||||
|
||||
<my-select-player-theme mode="instance" formControlName="theme" inputId="defaultsPlayerTheme"></my-select-player-theme>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { PeertubeCheckboxComponent } from '@app/shared/shared-forms/peertube-che
|
|||
import { SelectCustomValueComponent } from '@app/shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { CustomConfig, PlayerTheme } from '@peertube/peertube-models'
|
||||
import { capitalizeFirstLetter } from '@root-helpers/string'
|
||||
import { ColorPaletteThemeConfig, ThemeCustomizationKey } from '@root-helpers/theme-manager'
|
||||
import debug from 'debug'
|
||||
|
@ -20,6 +20,7 @@ import { AdminConfigService } from '../../../shared/shared-admin/admin-config.se
|
|||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { AlertComponent } from '../../../shared/shared-main/common/alert.component'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
import { SelectPlayerThemeComponent } from '@app/shared/shared-forms/select/select-player-theme.component'
|
||||
|
||||
const debugLogger = debug('peertube:config')
|
||||
|
||||
|
@ -65,6 +66,12 @@ type Form = {
|
|||
inputBorderRadius: FormControl<string>
|
||||
}>
|
||||
}>
|
||||
|
||||
defaults: FormGroup<{
|
||||
player: FormGroup<{
|
||||
theme: FormControl<PlayerTheme>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
type FieldType = 'color' | 'radius'
|
||||
|
@ -84,7 +91,8 @@ type FieldType = 'color' | 'radius'
|
|||
SelectOptionsComponent,
|
||||
HelpComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
SelectCustomValueComponent
|
||||
SelectCustomValueComponent,
|
||||
SelectPlayerThemeComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
|
@ -108,6 +116,7 @@ export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, Can
|
|||
}[] = []
|
||||
|
||||
availableThemes: SelectOptionsItem[]
|
||||
availablePlayerThemes: SelectOptionsItem<PlayerTheme>[] = []
|
||||
|
||||
private customizationResetFields = new Set<ThemeCustomizationKey>()
|
||||
private customConfig: CustomConfig
|
||||
|
@ -164,6 +173,11 @@ export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, Can
|
|||
...this.themeService.buildAvailableThemes()
|
||||
]
|
||||
|
||||
this.availablePlayerThemes = [
|
||||
{ id: 'galaxy', label: $localize`Galaxy`, description: $localize`Original theme` },
|
||||
{ id: 'lucide', label: $localize`Lucide`, description: $localize`A clean and modern theme` }
|
||||
]
|
||||
|
||||
this.buildForm()
|
||||
this.subscribeToCustomizationChanges()
|
||||
|
||||
|
@ -265,6 +279,11 @@ export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, Can
|
|||
headerBackgroundColor: null,
|
||||
inputBorderRadius: null
|
||||
}
|
||||
},
|
||||
defaults: {
|
||||
player: {
|
||||
theme: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<div class="form-group" formGroupName="instance">
|
||||
<label i18n id="instanceDefaultClientRouteLabel" for="instanceDefaultClientRoute">Landing page</label>
|
||||
|
||||
|
@ -43,13 +42,14 @@
|
|||
</div>
|
||||
|
||||
<ng-container formGroupName="client">
|
||||
|
||||
<ng-container formGroupName="menu">
|
||||
<ng-container formGroupName="login">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientMenuLoginRedirectOnSingleExternalAuth" formControlName="redirectOnSingleExternalAuth"
|
||||
i18n-labelText labelText="Redirect users on single external auth when users click on the login button in menu"
|
||||
inputName="clientMenuLoginRedirectOnSingleExternalAuth"
|
||||
formControlName="redirectOnSingleExternalAuth"
|
||||
i18n-labelText
|
||||
labelText="Redirect users on single external auth when users click on the login button in menu"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
@if (countExternalAuth() === 0) {
|
||||
|
@ -58,12 +58,11 @@
|
|||
<span i18n>⚠️ You have multiple external auth plugins enabled</span>
|
||||
}
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -76,20 +75,22 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="broadcastMessage">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="broadcastMessageEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable broadcast message"
|
||||
inputName="broadcastMessageEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Enable broadcast message"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="broadcastMessageDismissable" formControlName="dismissable"
|
||||
i18n-labelText labelText="Allow users to dismiss the broadcast message "
|
||||
inputName="broadcastMessageDismissable"
|
||||
formControlName="dismissable"
|
||||
i18n-labelText
|
||||
labelText="Allow users to dismiss the broadcast message "
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
|
@ -111,31 +112,28 @@
|
|||
<label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="broadcastMessageMessage" formControlName="message"
|
||||
[formError]="formErrors.broadcastMessage.message" markdownType="to-unsafe-html"
|
||||
inputId="broadcastMessageMessage"
|
||||
formControlName="message"
|
||||
[formError]="formErrors.broadcastMessage.message"
|
||||
markdownType="to-unsafe-html"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors.broadcastMessage.message" class="form-error" role="alert">{{ formErrors.broadcastMessage.message }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- new users grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<!-- new users grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>NEW USERS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="signup">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="signupEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable Signup"
|
||||
>
|
||||
<my-peertube-checkbox inputName="signupEnabled" formControlName="enabled" i18n-labelText labelText="Enable Signup">
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation</span>
|
||||
|
||||
|
@ -144,27 +142,37 @@
|
|||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresApproval" formControlName="requiresApproval"
|
||||
i18n-labelText labelText="Signup requires approval by moderators"
|
||||
<my-peertube-checkbox
|
||||
[ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresApproval"
|
||||
formControlName="requiresApproval"
|
||||
i18n-labelText
|
||||
labelText="Signup requires approval by moderators"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
|
||||
i18n-labelText labelText="Signup requires email verification"
|
||||
<my-peertube-checkbox
|
||||
[ngClass]="getDisabledSignupClass()"
|
||||
inputName="signupRequiresEmailVerification"
|
||||
formControlName="requiresEmailVerification"
|
||||
i18n-labelText
|
||||
labelText="Signup requires email verification"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div [ngClass]="getDisabledSignupClass()">
|
||||
<label i18n for="signupLimit">Signup limit</label>
|
||||
<span i18n class="small muted ms-1">When the total number of users in your platform reaches this limit, registrations are disabled. -1 == unlimited</span>
|
||||
<span i18n class="small muted ms-1">When the total number of users in your platform reaches this limit, registrations are disabled. -1 = unlimited</span>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="-1" id="signupLimit" class="form-control"
|
||||
formControlName="limit" [ngClass]="{ 'input-error': formErrors.signup.limit }"
|
||||
type="number"
|
||||
min="-1"
|
||||
id="signupLimit"
|
||||
class="form-control"
|
||||
formControlName="limit"
|
||||
[ngClass]="{ 'input-error': formErrors.signup.limit }"
|
||||
>
|
||||
<span i18n>{form.value.signup.limit, plural, =1 {user} other {users}}</span>
|
||||
</div>
|
||||
|
@ -179,8 +187,12 @@
|
|||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="signupMinimumAge" class="form-control"
|
||||
formControlName="minimumAge" [ngClass]="{ 'input-error': formErrors.signup.minimumAge }"
|
||||
type="number"
|
||||
min="1"
|
||||
id="signupMinimumAge"
|
||||
class="form-control"
|
||||
formControlName="minimumAge"
|
||||
[ngClass]="{ 'input-error': formErrors.signup.minimumAge }"
|
||||
>
|
||||
<span i18n>{form.value.signup.minimumAge, plural, =1 {year old} other {years old}}</span>
|
||||
</div>
|
||||
|
@ -201,7 +213,9 @@
|
|||
inputId="userVideoQuota"
|
||||
[items]="getVideoQuotaOptions()"
|
||||
formControlName="videoQuota"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
i18n-inputSuffix
|
||||
inputSuffix="bytes"
|
||||
inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
|
@ -218,7 +232,9 @@
|
|||
inputId="userVideoQuotaDaily"
|
||||
[items]="getVideoQuotaDailyOptions()"
|
||||
formControlName="videoQuotaDaily"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
i18n-inputSuffix
|
||||
inputSuffix="bytes"
|
||||
inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
|
@ -228,15 +244,16 @@
|
|||
<ng-container formGroupName="history">
|
||||
<ng-container formGroupName="videos">
|
||||
<my-peertube-checkbox
|
||||
inputName="videosHistoryEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically enable video history for a new user"
|
||||
inputName="videosHistoryEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Automatically enable video history for a new user"
|
||||
>
|
||||
</my-peertube-checkbox>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -246,11 +263,8 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="import">
|
||||
|
||||
<ng-container formGroupName="videos">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="importConcurrency">Import jobs concurrency</label>
|
||||
<span i18n class="small muted ms-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart</span>
|
||||
|
@ -265,39 +279,46 @@
|
|||
|
||||
<div class="form-group" formGroupName="http">
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosHttpEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
|
||||
inputName="importVideosHttpEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Allow import with HTTP URL (e.g. YouTube)"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ If enabled, we recommend to use <a class="link-primary" href="https://docs.joinpeertube.org/maintain/configuration#security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
||||
<span i18n
|
||||
>⚠️ If enabled, we recommend to use <a class="link-primary" href="https://docs.joinpeertube.org/maintain/configuration#security"
|
||||
>a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="torrent">
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosTorrentEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow import with a torrent file or a magnet URI"
|
||||
inputName="importVideosTorrentEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Allow import with a torrent file or a magnet URI"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ We don't recommend to enable this feature if you don't trust your users</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ We don't recommend to enable this feature if you don't trust your users</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="videoChannelSynchronization">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="importSynchronizationEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow channel synchronization with channel of other platforms like YouTube"
|
||||
inputName="importSynchronizationEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Allow channel synchronization with channel of other platforms like YouTube"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n [hidden]="isImportVideosHttpEnabled()">
|
||||
⛔ You need to allow import with HTTP URL to be able to activate this feature.
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n [hidden]="isImportVideosHttpEnabled()">
|
||||
⛔ You need to allow import with HTTP URL to be able to activate this feature.
|
||||
</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
|
@ -306,16 +327,21 @@
|
|||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="videoChannelSynchronizationMaxPerUser" class="form-control"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors['import']['videoChannelSynchronization']['maxPerUser'] }"
|
||||
type="number"
|
||||
min="1"
|
||||
id="videoChannelSynchronizationMaxPerUser"
|
||||
class="form-control"
|
||||
formControlName="maxPerUser"
|
||||
[ngClass]="{ 'input-error': formErrors['import']['videoChannelSynchronization']['maxPerUser'] }"
|
||||
>
|
||||
<span i18n>{form.value.import.videoChannelSynchronization.maxPerUser, plural, =1 {sync} other {syncs}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">{{ formErrors.import.videoChannelSynchronization.maxPerUser }}</div>
|
||||
<div *ngIf="formErrors.import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">
|
||||
{{ formErrors.import.videoChannelSynchronization.maxPerUser }}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -326,22 +352,21 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="autoBlacklist">
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="ofUsers">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="autoBlacklistVideosOfUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Block new videos automatically"
|
||||
inputName="autoBlacklistVideosOfUsersEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Block new videos automatically"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -350,8 +375,10 @@
|
|||
<ng-container formGroupName="update">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoFileUpdateEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow users to upload a new version of their video"
|
||||
inputName="videoFileUpdateEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Allow users to upload a new version of their video"
|
||||
>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
@ -360,10 +387,7 @@
|
|||
|
||||
<ng-container formGroupName="storyboards">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="storyboardsEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable video storyboards"
|
||||
>
|
||||
<my-peertube-checkbox inputName="storyboardsEnabled" formControlName="enabled" i18n-labelText labelText="Enable video storyboards">
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video</span>
|
||||
</ng-container>
|
||||
|
@ -373,24 +397,24 @@
|
|||
|
||||
<ng-container formGroupName="videoTranscription">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoTranscriptionEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable video transcription"
|
||||
>
|
||||
<my-peertube-checkbox inputName="videoTranscriptionEnabled" formControlName="enabled" i18n-labelText labelText="Enable video transcription">
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n><a href="https://docs.joinpeertube.org/admin/configuration#automatic-transcription" target="_blank">Automatically create subtitles</a> for uploaded/imported VOD videos</span>
|
||||
<span i18n><a href="https://docs.joinpeertube.org/admin/configuration#automatic-transcription" target="_blank">Automatically create subtitles</a>
|
||||
for uploaded/imported VOD videos</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div class="form-group" formGroupName="remoteRunners" [ngClass]="getTranscriptionRunnerDisabledClass()">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoTranscriptionRemoteRunnersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable remote runners for transcription"
|
||||
inputName="videoTranscriptionRemoteRunnersEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Enable remote runners for transcription"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>
|
||||
Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process transcription tasks.
|
||||
Remote runners has to register on your instance first.
|
||||
Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process transcription tasks. Remote runners has to
|
||||
register on your instance first.
|
||||
</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
|
@ -402,7 +426,6 @@
|
|||
|
||||
<ng-container formGroupName="defaults">
|
||||
<ng-container formGroupName="publish">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="defaultsPublishPrivacy">Default video privacy</label>
|
||||
|
||||
|
@ -442,8 +465,12 @@
|
|||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="videoChannelsMaxPerUser" class="form-control"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors.videoChannels.maxPerUser }"
|
||||
type="number"
|
||||
min="1"
|
||||
id="videoChannelsMaxPerUser"
|
||||
class="form-control"
|
||||
formControlName="maxPerUser"
|
||||
[ngClass]="{ 'input-error': formErrors.videoChannels.maxPerUser }"
|
||||
>
|
||||
<span i18n>{form.value.videoChannels.maxPerUser, plural, =1 {channel} other {channels}}</span>
|
||||
</div>
|
||||
|
@ -462,15 +489,16 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="videoComments">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoCommentsAcceptRemoteComments" formControlName="acceptRemoteComments"
|
||||
i18n-labelText labelText="Accept comments made on remote platforms"
|
||||
inputName="videoCommentsAcceptRemoteComments"
|
||||
formControlName="acceptRemoteComments"
|
||||
i18n-labelText
|
||||
labelText="Accept comments made on remote platforms"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>This setting is not retroactive: current remote comments platform will not be deleted</span>
|
||||
<span i18n>This setting is not retroactive: current comments from remote platforms will not be deleted</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
@ -480,22 +508,25 @@
|
|||
<ng-container formGroupName="channels">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersChannelsEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Remote actors can follow channels of your platform"
|
||||
inputName="followersChannelsEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Remote actors can follow channels of your platform"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>This setting is not retroactive: current followers of channels of your platform will not be affected</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Remote actors can follow your platform"
|
||||
inputName="followersInstanceEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Remote actors can follow your platform"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>This setting is not retroactive: current followers of your platform will not be affected</span>
|
||||
|
@ -505,8 +536,10 @@
|
|||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceManualApproval" formControlName="manualApproval"
|
||||
i18n-labelText labelText="Manually approve new followers that follow your platform"
|
||||
inputName="followersInstanceManualApproval"
|
||||
formControlName="manualApproval"
|
||||
i18n-labelText
|
||||
labelText="Manually approve new followers that follow your platform"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -514,12 +547,13 @@
|
|||
|
||||
<ng-container formGroupName="followings">
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<ng-container formGroupName="autoFollowBack">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow back followers that follow your platform"
|
||||
inputName="followingsInstanceAutoFollowBackEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Automatically follow back followers that follow your platform"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation</span>
|
||||
|
@ -531,14 +565,21 @@
|
|||
<ng-container formGroupName="autoFollowIndex">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow platforms of a public index"
|
||||
inputName="followingsInstanceAutoFollowIndexEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Automatically follow platforms of a public index"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
|
||||
|
||||
<span i18n>
|
||||
See <a class="link-primary" href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL
|
||||
See <a
|
||||
class="link-primary"
|
||||
href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>the documentation</a> for more information about the expected URL
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
|
@ -546,19 +587,22 @@
|
|||
<div [ngClass]="{ 'disabled-checkbox-extra': !isAutoFollowIndexEnabled() }">
|
||||
<label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
|
||||
<input
|
||||
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
||||
formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors.followings.instance.autoFollowIndex.indexUrl }"
|
||||
type="text"
|
||||
id="followingsInstanceAutoFollowIndexUrl"
|
||||
class="form-control"
|
||||
formControlName="indexUrl"
|
||||
[ngClass]="{ 'input-error': formErrors.followings.instance.autoFollowIndex.indexUrl }"
|
||||
>
|
||||
<div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
|
||||
<div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">
|
||||
{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -569,27 +613,34 @@
|
|||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="defaults">
|
||||
<ng-container formGroupName="player">
|
||||
|
||||
<div class="form-group" formGroupName="player">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsPlayerAutoplay" formControlName="autoPlay"
|
||||
i18n-labelText labelText="Automatically play videos in the player"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsPlayerAutoplay"
|
||||
formControlName="autoPlay"
|
||||
i18n-labelText
|
||||
labelText="Automatically play videos in the player"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="p2p">
|
||||
|
||||
<div class="form-group" formGroupName="webapp">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsP2PWebappEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable P2P streaming by default on your platform"
|
||||
inputName="defaultsP2PWebappEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Enable P2P streaming by default on your platform"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="embed">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsP2PEmbedEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable P2P streaming by default for videos embedded on external websites"
|
||||
inputName="defaultsP2PEmbedEnabled"
|
||||
formControlName="enabled"
|
||||
i18n-labelText
|
||||
labelText="Enable P2P streaming by default for videos embedded on external websites"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -603,14 +654,14 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="search">
|
||||
<ng-container formGroupName="remoteUri">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="searchRemoteUriUsers" formControlName="users"
|
||||
i18n-labelText labelText="Allow users to do remote URI/handle search"
|
||||
inputName="searchRemoteUriUsers"
|
||||
formControlName="users"
|
||||
i18n-labelText
|
||||
labelText="Allow users to do remote URI/handle search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your platform</span>
|
||||
|
@ -620,23 +671,21 @@
|
|||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="searchRemoteUriAnonymous" formControlName="anonymous"
|
||||
i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
|
||||
inputName="searchRemoteUriAnonymous"
|
||||
formControlName="anonymous"
|
||||
i18n-labelText
|
||||
labelText="Allow anonymous to do remote URI/handle search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your platform</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="searchIndex">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="searchIndexEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable global search"
|
||||
>
|
||||
<my-peertube-checkbox inputName="searchIndexEnabled" formControlName="enabled" i18n-labelText labelText="Enable global search">
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>⚠️ This functionality depends heavily on the moderation of platforms followed by the search index you select</div>
|
||||
</ng-container>
|
||||
|
@ -646,43 +695,48 @@
|
|||
<label i18n for="searchIndexUrl">Search index URL</label>
|
||||
|
||||
<div i18n class="form-group-description">
|
||||
Use your <a class="link-primary" target="_blank" href="https://framagit.org/framasoft/peertube/search-index">your own search index</a> or choose the official one, <a class="link-primary" target="_blank" href="https://sepiasearch.org">https://sepiasearch.org</a>, that is not moderated.
|
||||
Use <a class="link-primary" target="_blank" href="https://framagit.org/framasoft/peertube/search-index">your own search index</a> or choose the official one, <a class="link-primary" target="_blank" href="https://sepiasearch.org">https://sepiasearch.org</a>, that is not moderated.
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text" id="searchIndexUrl" class="form-control"
|
||||
formControlName="url" [ngClass]="{ 'input-error': formErrors.search.searchIndex.url }"
|
||||
type="text"
|
||||
id="searchIndexUrl"
|
||||
class="form-control"
|
||||
formControlName="url"
|
||||
[ngClass]="{ 'input-error': formErrors.search.searchIndex.url }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.search.searchIndex.url" class="form-error" role="alert">{{ formErrors.search.searchIndex.url }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexDisableLocalSearch" formControlName="disableLocalSearch"
|
||||
i18n-labelText labelText="Disable local search in search bar"
|
||||
<my-peertube-checkbox
|
||||
[ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexDisableLocalSearch"
|
||||
formControlName="disableLocalSearch"
|
||||
i18n-labelText
|
||||
labelText="Disable local search in search bar"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<my-peertube-checkbox [ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexIsDefaultSearch" formControlName="isDefaultSearch"
|
||||
i18n-labelText labelText="Search bar uses the global search index by default"
|
||||
<my-peertube-checkbox
|
||||
[ngClass]="getDisabledSearchIndexClass()"
|
||||
inputName="searchIndexIsDefaultSearch"
|
||||
formControlName="isDefaultSearch"
|
||||
i18n-labelText
|
||||
labelText="Search bar uses the global search index by default"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Otherwise, the local search will be used by default</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -692,14 +746,10 @@
|
|||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="import">
|
||||
<ng-container formGroupName="users">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="importUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow your users to import a data archive"
|
||||
>
|
||||
<my-peertube-checkbox inputName="importUsersEnabled" formControlName="enabled" i18n-labelText labelText="Allow your users to import a data archive">
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>Video quota is checked on import so the user doesn't upload a too big archive file</div>
|
||||
<div i18n>Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import</div>
|
||||
|
@ -710,20 +760,14 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="export">
|
||||
|
||||
<ng-container formGroupName="users">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="exportUsersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow your users to export their data"
|
||||
>
|
||||
<my-peertube-checkbox inputName="exportUsersEnabled" formControlName="enabled" i18n-labelText labelText="Allow your users to export their data">
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Users can export their PeerTube data in a .zip for backup or re-import. Only one export at a time is allowed per user</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||
<label i18n id="exportUsersMaxUserVideoQuota" for="exportUsersMaxUserVideoQuota">Max user video quota allowed to generate the export</label>
|
||||
|
||||
|
@ -734,7 +778,9 @@
|
|||
inputId="exportUsersMaxUserVideoQuota"
|
||||
[items]="exportMaxUserVideoQuotaOptions"
|
||||
formControlName="maxUserVideoQuota"
|
||||
i18n-inputSuffix inputSuffix="bytes" inputType="number"
|
||||
i18n-inputSuffix
|
||||
inputSuffix="bytes"
|
||||
inputType="number"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
|
@ -744,20 +790,21 @@
|
|||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||
<label i18n for="exportUsersExportExpiration">User export expiration</label>
|
||||
|
||||
<my-select-options inputId="exportUsersExportExpiration" [items]="exportExpirationOptions" formControlName="exportExpiration"></my-select-options>
|
||||
<my-select-options
|
||||
inputId="exportUsersExportExpiration"
|
||||
[items]="exportExpirationOptions"
|
||||
formControlName="exportExpiration"
|
||||
></my-select-options>
|
||||
|
||||
<div i18n class="mt-1 small muted">The archive file is deleted after this period</div>
|
||||
|
||||
<div *ngIf="formErrors.export.users.exportExpiration" class="form-error" role="alert">{{ formErrors.export.users.exportExpiration }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
|
|
@ -24,7 +24,14 @@ import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@a
|
|||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { BroadcastMessageLevel, CustomConfig, VideoCommentPolicyType, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import {
|
||||
BroadcastMessageLevel,
|
||||
CustomConfig,
|
||||
PlayerTheme,
|
||||
VideoCommentPolicyType,
|
||||
VideoConstant,
|
||||
VideoPrivacyType
|
||||
} from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { pairwise } from 'rxjs/operators'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
|
|
|
@ -107,7 +107,7 @@ export class AdminConfigLiveComponent implements OnInit, OnDestroy, CanComponent
|
|||
{ id: 1000 * 3600 * 10, label: $localize`10 hours` }
|
||||
]
|
||||
|
||||
this.liveResolutions = this.adminConfigService.transcodingResolutionOptions
|
||||
this.liveResolutions = this.adminConfigService.getTranscodingOptions('live')
|
||||
this.transcodingProfiles = this.adminConfigService.buildTranscodingProfiles(
|
||||
this.server.getHTMLConfig().live.transcoding.availableProfiles
|
||||
)
|
||||
|
@ -143,7 +143,7 @@ export class AdminConfigLiveComponent implements OnInit, OnDestroy, CanComponent
|
|||
enabled: null,
|
||||
threads: TRANSCODING_THREADS_VALIDATOR,
|
||||
profile: null,
|
||||
resolutions: this.adminConfigService.buildFormResolutions(),
|
||||
resolutions: this.adminConfigService.buildFormResolutions('live'),
|
||||
alwaysTranscodeOriginalResolution: null,
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<my-admin-save-bar i18n-title title="Platform information" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
<my-admin-save-bar i18n-title title="Upload logos" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="pt-two-cols mt-4">
|
||||
|
|
|
@ -112,7 +112,7 @@ export class AdminConfigVODComponent implements OnInit, OnDestroy, CanComponentD
|
|||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
this.transcodingThreadOptions = this.configService.transcodingThreadOptions
|
||||
this.resolutions = this.adminConfigService.transcodingResolutionOptions
|
||||
this.resolutions = this.adminConfigService.getTranscodingOptions('vod')
|
||||
this.additionalVideoExtensions = serverConfig.video.file.extensions.join(' ')
|
||||
this.transcodingProfiles = this.adminConfigService.buildTranscodingProfiles(serverConfig.transcoding.availableProfiles)
|
||||
|
||||
|
@ -156,7 +156,7 @@ export class AdminConfigVODComponent implements OnInit, OnDestroy, CanComponentD
|
|||
max: TRANSCODING_MAX_FPS_VALIDATOR
|
||||
},
|
||||
|
||||
resolutions: this.adminConfigService.buildFormResolutions(),
|
||||
resolutions: this.adminConfigService.buildFormResolutions('vod'),
|
||||
alwaysTranscodeOriginalResolution: null,
|
||||
|
||||
remoteRunners: {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
.root {
|
||||
position: sticky;
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
input[type=text],
|
||||
input[type=password] {
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
@include peertube-input-text(340px);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: pvar(--input-bg);
|
||||
border-left: 1px solid pvar(--bg);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
[customUpdateUrl]="customUpdateUrl"
|
||||
>
|
||||
<ng-template #totalTitle let-totalRecords>
|
||||
<ng-container i18n>{ totalRecords, plural, =0 {No jobs} =1 {1 job} other {{{ totalRecords | myNumberFormatter }} jobs}}</ng-container>
|
||||
<ng-container i18n>{ totalRecords, plural, =0 {No job} =1 {1 job} other {{{ totalRecords | myNumberFormatter }} jobs}}</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #captionRight>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
<div class="playlist-info">
|
||||
<my-video-playlist-miniature
|
||||
*ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true"
|
||||
[displayDescription]="true" [displayPrivacy]="true"
|
||||
*ngIf="playlist" [playlist]="playlist" toManage="false" displayChannel="true"
|
||||
displayDescription="true" displayPrivacy="true"
|
||||
></my-video-playlist-miniature>
|
||||
|
||||
<div class="playlist-buttons">
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
@if (user.videoChannels.length > 1) {
|
||||
<div class="form-group">
|
||||
<div class="label" i18n>Filter by a channel</div>
|
||||
<div class="form-group-description" i18n>This allows you to reorder playlists assigned to it</div>
|
||||
<div class="form-group">
|
||||
<div class="label" i18n>Filter by a channel</div>
|
||||
<div class="form-group-description" i18n>This allows you to reorder playlists assigned to it</div>
|
||||
|
||||
<div class="channel-filters">
|
||||
@for (channel of channels; track channel.id) {
|
||||
<my-channel-toggle [channel]="channel" [(ngModel)]="channel.selected" (ngModelChange)="onChannelFilter(channel)"></my-channel-toggle>
|
||||
}
|
||||
</div>
|
||||
<div class="channel-filters">
|
||||
@for (channel of channels; track channel.id) {
|
||||
<my-channel-toggle [channel]="channel" [(ngModel)]="channel.selected" (ngModelChange)="onChannelFilter(channel)"></my-channel-toggle>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<my-table
|
||||
#table
|
||||
|
@ -56,7 +54,7 @@
|
|||
</td>
|
||||
|
||||
<td *ngIf="table.isColumnDisplayed('videos')">
|
||||
<my-video-playlist-miniature [playlist]="playlist" thumbnailOnly="true"></my-video-playlist-miniature>
|
||||
<my-video-playlist-miniature [playlist]="playlist" thumbnailOnly="true" toManage="true"></my-video-playlist-miniature>
|
||||
</td>
|
||||
|
||||
<td *ngIf="table.isColumnDisplayed('name')">
|
||||
|
|
|
@ -212,7 +212,7 @@ export class MyVideoPlaylistsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
hasReorderableRows () {
|
||||
return !!this.getFilteredChannel() || this.user.videoChannels.length === 1
|
||||
return !!this.getFilteredChannel()
|
||||
}
|
||||
|
||||
private _dataLoader (options: {
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
|
||||
<div *ngIf="isPlaylist(result)" class="entry video-playlist">
|
||||
<my-video-playlist-miniature
|
||||
[playlist]="result" [displayAsRow]="true" [displayChannel]="true"
|
||||
[playlist]="result" displayAsRow="true" displayChannel="true" toManage="false"
|
||||
[linkType]="getLinkType()"
|
||||
></my-video-playlist-miniature>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<div class="playlists" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
|
||||
<div *ngFor="let playlist of videoPlaylists" class="playlist-wrapper">
|
||||
<my-video-playlist-miniature [playlist]="playlist" [toManage]="false" [displayAsRow]="displayAsRow()"></my-video-playlist-miniature>
|
||||
<my-video-playlist-miniature [playlist]="playlist" toManage="false" [displayAsRow]="displayAsRow()"></my-video-playlist-miniature>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Routes } from '@angular/router'
|
||||
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 { SearchService } from '@app/shared/shared-search/search.service'
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
|
||||
|
@ -8,11 +9,11 @@ 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 { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
import { OverviewService } from '../+video-list'
|
||||
import { VideoRecommendationService } from './shared'
|
||||
import { VideoWatchComponent } from './video-watch.component'
|
||||
import { BulkService } from '@app/shared/shared-moderation/bulk.service'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -30,7 +31,8 @@ export default [
|
|||
AbuseService,
|
||||
UserAdminService,
|
||||
BulkService,
|
||||
VideoStateMessageService
|
||||
VideoStateMessageService,
|
||||
PlayerSettingsService
|
||||
],
|
||||
children: [
|
||||
{
|
||||
|
|
|
@ -29,12 +29,16 @@ import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription/s
|
|||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { VideoPlaylist } from '@app/shared/shared-video-playlist/video-playlist.model'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { getVideoWatchRSSFeeds, timeToInt } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HTMLServerConfig,
|
||||
HttpStatusCode,
|
||||
LiveVideo,
|
||||
PeerTubeProblemDocument,
|
||||
PlayerMode,
|
||||
PlayerTheme,
|
||||
PlayerVideoSettings,
|
||||
ServerErrorCode,
|
||||
Storyboard,
|
||||
VideoCaption,
|
||||
|
@ -51,7 +55,6 @@ import {
|
|||
PeerTubePlayer,
|
||||
PeerTubePlayerConstructorOptions,
|
||||
PeerTubePlayerLoadOptions,
|
||||
PlayerMode,
|
||||
videojs,
|
||||
VideojsPlayer
|
||||
} from '@peertube/player'
|
||||
|
@ -79,6 +82,7 @@ const debugLogger = debug('peertube:watch:VideoWatchComponent')
|
|||
|
||||
type URLOptions = {
|
||||
playerMode: PlayerMode
|
||||
playerTheme?: PlayerTheme
|
||||
|
||||
startTime: number | string
|
||||
stopTime: number | string
|
||||
|
@ -138,6 +142,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
private zone = inject(NgZone)
|
||||
private videoCaptionService = inject(VideoCaptionService)
|
||||
private videoChapterService = inject(VideoChapterService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
private hotkeysService = inject(HotkeysService)
|
||||
private hooks = inject(HooksService)
|
||||
private pluginService = inject(PluginService)
|
||||
|
@ -161,6 +166,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
liveVideo: LiveVideo
|
||||
videoPassword: string
|
||||
storyboards: Storyboard[] = []
|
||||
playerSettings: PlayerVideoSettings
|
||||
|
||||
playlistPosition: number
|
||||
playlist: VideoPlaylist = null
|
||||
|
@ -372,9 +378,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.videoCaptionService.listCaptions(videoId, videoPassword),
|
||||
this.videoChapterService.getChapters({ videoId, videoPassword }),
|
||||
this.videoService.getStoryboards(videoId, videoPassword),
|
||||
this.playerSettingsService.getVideoSettings({ videoId, videoPassword, raw: false }),
|
||||
this.userService.getAnonymousOrLoggedUser()
|
||||
]).subscribe({
|
||||
next: ([ { video, live, videoFileToken }, captionsResult, chaptersResult, storyboards, loggedInOrAnonymousUser ]) => {
|
||||
next: ([ { video, live, videoFileToken }, captionsResult, chaptersResult, storyboards, playerSettings, loggedInOrAnonymousUser ]) => {
|
||||
this.onVideoFetched({
|
||||
video,
|
||||
live,
|
||||
|
@ -383,6 +390,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
storyboards,
|
||||
videoFileToken,
|
||||
videoPassword,
|
||||
playerSettings,
|
||||
loggedInOrAnonymousUser,
|
||||
forceAutoplay
|
||||
}).catch(err => {
|
||||
|
@ -489,6 +497,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
storyboards: Storyboard[]
|
||||
videoFileToken: string
|
||||
videoPassword: string
|
||||
playerSettings: PlayerVideoSettings
|
||||
|
||||
loggedInOrAnonymousUser: User
|
||||
forceAutoplay: boolean
|
||||
|
@ -501,6 +510,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
storyboards,
|
||||
videoFileToken,
|
||||
videoPassword,
|
||||
playerSettings,
|
||||
loggedInOrAnonymousUser,
|
||||
forceAutoplay
|
||||
} = options
|
||||
|
@ -514,6 +524,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.videoFileToken = videoFileToken
|
||||
this.videoPassword = videoPassword
|
||||
this.storyboards = storyboards
|
||||
this.playerSettings = playerSettings
|
||||
|
||||
// Re init attributes
|
||||
this.remoteServerDown = false
|
||||
|
@ -577,6 +588,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
liveVideo: this.liveVideo,
|
||||
videoFileToken: this.videoFileToken,
|
||||
videoPassword: this.videoPassword,
|
||||
playerSettings: this.playerSettings,
|
||||
urlOptions: this.getUrlOptions(),
|
||||
loggedInOrAnonymousUser,
|
||||
forceAutoplay,
|
||||
|
@ -725,6 +737,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
videoCaptions: VideoCaption[]
|
||||
videoChapters: VideoChapter[]
|
||||
storyboards: Storyboard[]
|
||||
playerSettings: PlayerVideoSettings
|
||||
|
||||
videoFileToken: string
|
||||
videoPassword: string
|
||||
|
@ -745,7 +758,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
videoPassword,
|
||||
urlOptions,
|
||||
loggedInOrAnonymousUser,
|
||||
forceAutoplay
|
||||
forceAutoplay,
|
||||
playerSettings
|
||||
} = options
|
||||
|
||||
let mode: PlayerMode
|
||||
|
@ -814,6 +828,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
|
||||
return {
|
||||
mode,
|
||||
theme: urlOptions.playerTheme || playerSettings.theme as PlayerTheme,
|
||||
|
||||
autoplay: this.isAutoplay(video, loggedInOrAnonymousUser),
|
||||
forceAutoplay,
|
||||
|
@ -1032,6 +1047,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
subtitle: queryParams.subtitle,
|
||||
|
||||
playerMode: queryParams.mode,
|
||||
playerTheme: queryParams.playerTheme,
|
||||
playbackRate: queryParams.playbackRate,
|
||||
|
||||
controlBar: toBoolean(queryParams.controlBar),
|
||||
|
|
|
@ -8,6 +8,8 @@ import { manageRoutes } from '../shared-manage/routes'
|
|||
import { VideoStudioService } from '../shared-manage/studio/video-studio.service'
|
||||
import { VideoManageComponent } from './video-manage.component'
|
||||
import { VideoManageResolver } from './video-manage.resolver'
|
||||
import { VideoManageController } from '../shared-manage/video-manage-controller.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -16,12 +18,14 @@ export default [
|
|||
canActivate: [ LoginGuard ],
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
providers: [
|
||||
VideoManageController,
|
||||
VideoManageResolver,
|
||||
LiveVideoService,
|
||||
I18nPrimengCalendarService,
|
||||
VideoUploadService,
|
||||
VideoStudioService,
|
||||
VideoStateMessageService
|
||||
VideoStateMessageService,
|
||||
PlayerSettingsService
|
||||
],
|
||||
resolve: {
|
||||
resolverData: VideoManageResolver
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<div class="margin-content">
|
||||
<my-video-manage-container
|
||||
*ngIf="loaded"
|
||||
canUpdate="true" canWatch="true" cancelLink="/my-library/videos" (videoUpdated)="onVideoUpdated()"
|
||||
></my-video-manage-container>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Component, HostListener, inject, OnDestroy, OnInit } from '@angular/cor
|
|||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
||||
import { VideoEdit } from '../shared-manage/common/video-edit.model'
|
||||
import { VideoManageContainerComponent } from '../shared-manage/video-manage-container.component'
|
||||
import { VideoManageController } from '../shared-manage/video-manage-controller.service'
|
||||
import { VideoManageResolverData } from './video-manage.resolver'
|
||||
|
@ -16,8 +15,7 @@ import { VideoManageResolverData } from './video-manage.resolver'
|
|||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
VideoManageContainerComponent
|
||||
],
|
||||
providers: [ VideoManageController ]
|
||||
]
|
||||
})
|
||||
export class VideoManageComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private route = inject(ActivatedRoute)
|
||||
|
@ -29,18 +27,9 @@ export class VideoManageComponent implements OnInit, OnDestroy, CanComponentDeac
|
|||
isUpdatingVideo = false
|
||||
loaded = false
|
||||
|
||||
async ngOnInit () {
|
||||
ngOnInit () {
|
||||
const data = this.route.snapshot.data.resolverData as VideoManageResolverData
|
||||
const { video, userChannels, captions, chapters, videoSource, live, videoPasswords, userQuota, privacies } = data
|
||||
|
||||
const videoEdit = await VideoEdit.createFromAPI(this.serverService.getHTMLConfig(), {
|
||||
video,
|
||||
captions,
|
||||
chapters,
|
||||
live,
|
||||
videoSource,
|
||||
videoPasswords: videoPasswords.map(p => p.password)
|
||||
})
|
||||
const { userChannels, userQuota, privacies, videoEdit } = data
|
||||
|
||||
this.manageController.setStore({
|
||||
videoEdit,
|
||||
|
@ -50,8 +39,6 @@ export class VideoManageComponent implements OnInit, OnDestroy, CanComponentDeac
|
|||
})
|
||||
|
||||
this.manageController.setConfig({ manageType: 'update', serverConfig: this.serverService.getHTMLConfig() })
|
||||
|
||||
this.loaded = true
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { VideoService } from '@app/shared/shared-main/video/video.service'
|
|||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import {
|
||||
LiveVideo,
|
||||
PlayerVideoSettings,
|
||||
UserVideoQuota,
|
||||
VideoCaption,
|
||||
VideoChapter,
|
||||
|
@ -22,6 +23,8 @@ import {
|
|||
import { forkJoin, of } from 'rxjs'
|
||||
import { map, switchMap } from 'rxjs/operators'
|
||||
import { SelectChannelItem } from '../../../types'
|
||||
import { VideoEdit } from '../shared-manage/common/video-edit.model'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
|
||||
export type VideoManageResolverData = {
|
||||
video: VideoDetails
|
||||
|
@ -33,6 +36,8 @@ export type VideoManageResolverData = {
|
|||
videoPasswords: VideoPassword[]
|
||||
userQuota: UserVideoQuota
|
||||
privacies: VideoConstant<VideoPrivacyType>[]
|
||||
videoEdit: VideoEdit
|
||||
playerSettings: PlayerVideoSettings
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
@ -45,6 +50,7 @@ export class VideoManageResolver {
|
|||
private videoPasswordService = inject(VideoPasswordService)
|
||||
private userService = inject(UserService)
|
||||
private serverService = inject(ServerService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
|
||||
resolve (route: ActivatedRouteSnapshot) {
|
||||
const uuid: string = route.params['uuid']
|
||||
|
@ -52,18 +58,32 @@ export class VideoManageResolver {
|
|||
return this.videoService.getVideo({ videoId: uuid })
|
||||
.pipe(
|
||||
switchMap(video => forkJoin(this.buildObservables(video))),
|
||||
map(([ video, videoSource, userChannels, captions, chapters, live, videoPasswords, userQuota, privacies ]) =>
|
||||
({
|
||||
video,
|
||||
userChannels,
|
||||
captions,
|
||||
chapters,
|
||||
videoSource,
|
||||
live,
|
||||
videoPasswords,
|
||||
userQuota,
|
||||
privacies
|
||||
}) as VideoManageResolverData
|
||||
switchMap(
|
||||
async ([ video, videoSource, userChannels, captions, chapters, live, videoPasswords, userQuota, privacies, playerSettings ]) => {
|
||||
const videoEdit = await VideoEdit.createFromAPI(this.serverService.getHTMLConfig(), {
|
||||
video,
|
||||
captions,
|
||||
chapters,
|
||||
live,
|
||||
videoSource,
|
||||
playerSettings,
|
||||
videoPasswords: videoPasswords.map(p => p.password)
|
||||
})
|
||||
|
||||
return {
|
||||
video,
|
||||
userChannels,
|
||||
captions,
|
||||
chapters,
|
||||
videoSource,
|
||||
live,
|
||||
videoPasswords,
|
||||
userQuota,
|
||||
privacies,
|
||||
videoEdit,
|
||||
playerSettings
|
||||
} satisfies VideoManageResolverData
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -94,11 +114,13 @@ export class VideoManageResolver {
|
|||
|
||||
video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED
|
||||
? this.videoPasswordService.getVideoPasswords({ videoUUID: video.uuid })
|
||||
: of([]),
|
||||
: of([] as VideoPassword[]),
|
||||
|
||||
this.userService.getMyVideoQuotaUsed(),
|
||||
|
||||
this.serverService.getVideoPrivacies()
|
||||
]
|
||||
this.serverService.getVideoPrivacies(),
|
||||
|
||||
this.playerSettingsService.getVideoSettings({ videoId: video.uuid, raw: true })
|
||||
] as const
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ export class VideoImportTorrentComponent implements OnInit, AfterViewInit, CanCo
|
|||
.pipe(switchMap(({ video }) => this.videoService.getVideo({ videoId: video.uuid })))
|
||||
.subscribe({
|
||||
next: async video => {
|
||||
await videoEdit.loadFromAPI({ video })
|
||||
await videoEdit.loadFromAPI({ video, loadPrivacy: false })
|
||||
|
||||
this.loadingBar.useRef().complete()
|
||||
|
||||
|
|
|
@ -130,7 +130,7 @@ export class VideoImportUrlComponent implements OnInit, AfterViewInit, CanCompon
|
|||
)
|
||||
.subscribe({
|
||||
next: async ({ video, captions, chapters }) => {
|
||||
await videoEdit.loadFromAPI({ video, captions, chapters })
|
||||
await videoEdit.loadFromAPI({ video, captions, chapters, loadPrivacy: false })
|
||||
|
||||
this.loadingBar.useRef().complete()
|
||||
|
||||
|
|
|
@ -105,7 +105,7 @@ export class VideoGoLiveComponent implements OnInit, AfterViewInit, CanComponent
|
|||
.subscribe({
|
||||
next: async ({ video: { id, uuid, shortUUID }, live }) => {
|
||||
videoEdit.loadAfterPublish({ video: { id, uuid, shortUUID } })
|
||||
await videoEdit.loadFromAPI({ live })
|
||||
await videoEdit.loadFromAPI({ live, loadPrivacy: false })
|
||||
|
||||
debugLogger(`Live published`)
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { inject } from '@angular/core'
|
||||
import { RedirectCommand, Router, Routes } from '@angular/router'
|
||||
import { CanDeactivateGuard, LoginGuard } from '@app/core'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
import debug from 'debug'
|
||||
import { I18nPrimengCalendarService } from '../shared-manage/common/i18n-primeng-calendar.service'
|
||||
import { VideoUploadService } from '../shared-manage/common/video-upload.service'
|
||||
import { manageRoutes } from '../shared-manage/routes'
|
||||
|
@ -8,9 +12,6 @@ import { VideoStudioService } from '../shared-manage/studio/video-studio.service
|
|||
import { VideoManageController } from '../shared-manage/video-manage-controller.service'
|
||||
import { VideoPublishComponent } from './video-publish.component'
|
||||
import { VideoPublishResolver } from './video-publish.resolver'
|
||||
import { inject } from '@angular/core'
|
||||
import debug from 'debug'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
|
||||
const debugLogger = debug('peertube:video-publish')
|
||||
|
||||
|
@ -43,6 +44,7 @@ export default [
|
|||
providers: [
|
||||
VideoPublishResolver,
|
||||
VideoManageController,
|
||||
PlayerSettingsService,
|
||||
VideoStateMessageService,
|
||||
LiveVideoService,
|
||||
I18nPrimengCalendarService,
|
||||
|
|
|
@ -41,8 +41,7 @@ import { VideoPublishResolverData } from './video-publish.resolver'
|
|||
VideoImportUrlComponent,
|
||||
VideoUploadComponent,
|
||||
HelpComponent
|
||||
],
|
||||
providers: [ VideoManageController ]
|
||||
]
|
||||
})
|
||||
export class VideoPublishComponent implements OnInit, CanComponentDeactivate {
|
||||
private auth = inject(AuthService)
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
LiveVideoCreate,
|
||||
LiveVideoUpdate,
|
||||
NSFWFlag,
|
||||
PlayerVideoSettings,
|
||||
PlayerVideoSettingsUpdate,
|
||||
VideoCaption,
|
||||
VideoChapter,
|
||||
VideoCreate,
|
||||
|
@ -65,6 +67,8 @@ type StudioForm = {
|
|||
'add-watermark'?: { file?: File }
|
||||
}
|
||||
|
||||
type PlayerSettingsForm = PlayerVideoSettingsUpdate
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type LoadFromPublishOptions = Required<Pick<VideoCreate, 'channelId' | 'support'>> & Partial<Pick<VideoCreate, 'name'>>
|
||||
|
@ -115,6 +119,7 @@ type UpdateFromAPIOptions = {
|
|||
captions?: VideoCaption[]
|
||||
videoPasswords?: string[]
|
||||
videoSource?: VideoSource
|
||||
playerSettings?: PlayerVideoSettings
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -143,6 +148,7 @@ export class VideoEdit {
|
|||
private live: LiveUpdate
|
||||
private replaceFile: File
|
||||
private studioTasks: VideoStudioTask[] = []
|
||||
private playerSettings: PlayerVideoSettings
|
||||
|
||||
private videoImport: Pick<VideoImportCreate, 'magnetUri' | 'torrentfile' | 'targetUrl'>
|
||||
|
||||
|
@ -185,6 +191,7 @@ export class VideoEdit {
|
|||
previewfile?: { size: number }
|
||||
|
||||
live?: LiveUpdate
|
||||
playerSettings?: PlayerVideoSettings
|
||||
|
||||
pluginData?: any
|
||||
pluginDefaults?: Record<string, string | boolean>
|
||||
|
@ -293,13 +300,14 @@ export class VideoEdit {
|
|||
return videoEdit
|
||||
}
|
||||
|
||||
async loadFromAPI (options: UpdateFromAPIOptions) {
|
||||
const { video, videoPasswords, live, chapters, captions, videoSource } = options
|
||||
async loadFromAPI (options: UpdateFromAPIOptions & { loadPrivacy?: boolean }) {
|
||||
const { video, videoPasswords, live, chapters, captions, videoSource, playerSettings, loadPrivacy = true } = options
|
||||
|
||||
debugLogger('Load from API', options)
|
||||
|
||||
this.loadVideo({ video, videoPasswords, saveInStore: true })
|
||||
this.loadVideo({ video, videoPasswords, saveInStore: true, loadPrivacy })
|
||||
this.loadLive(live)
|
||||
this.loadPlayerSettings(playerSettings)
|
||||
|
||||
if (captions !== undefined) {
|
||||
this.captions = captions
|
||||
|
@ -322,18 +330,21 @@ export class VideoEdit {
|
|||
private loadVideo (options: {
|
||||
video: UpdateFromAPIOptions['video']
|
||||
videoPasswords?: string[]
|
||||
loadPrivacy?: boolean // default true
|
||||
saveInStore: boolean
|
||||
}) {
|
||||
const { video, saveInStore, videoPasswords = [] } = options
|
||||
const { video, saveInStore, loadPrivacy = true, videoPasswords = [] } = options
|
||||
|
||||
if (video === undefined) return
|
||||
|
||||
const buildObj: () => CommonUpdate = () => {
|
||||
return {
|
||||
const buildObj: (options: { loadPrivacy: boolean }) => CommonUpdate = () => {
|
||||
const { loadPrivacy } = options
|
||||
|
||||
const base = {
|
||||
...this.common,
|
||||
|
||||
name: video.name || '',
|
||||
privacy: video.privacy?.id ?? null,
|
||||
|
||||
channelId: video.channel?.id ?? null,
|
||||
category: video.category?.id ?? null,
|
||||
licence: video.licence?.id ?? null,
|
||||
|
@ -361,12 +372,18 @@ export class VideoEdit {
|
|||
|
||||
videoPasswords: videoPasswords ?? []
|
||||
}
|
||||
|
||||
if (loadPrivacy) {
|
||||
return { ...base, privacy: video.privacy?.id ?? null }
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
this.common = buildObj()
|
||||
this.common = buildObj({ loadPrivacy })
|
||||
|
||||
if (saveInStore) {
|
||||
const obj = buildObj()
|
||||
const obj = buildObj({ loadPrivacy: true })
|
||||
this.saveStore.common = omit(obj, [ 'pluginData', 'previewfile' ])
|
||||
|
||||
// Apply plugin defaults so we correctly detect changes
|
||||
|
@ -440,6 +457,17 @@ export class VideoEdit {
|
|||
this.metadata.live = pick(live, [ 'rtmpUrl', 'rtmpsUrl', 'streamKey' ])
|
||||
}
|
||||
|
||||
private loadPlayerSettings (playerSettings: UpdateFromAPIOptions['playerSettings']) {
|
||||
const buildObj = () => {
|
||||
return {
|
||||
theme: playerSettings.theme
|
||||
}
|
||||
}
|
||||
|
||||
this.playerSettings = buildObj()
|
||||
this.saveStore.playerSettings = buildObj()
|
||||
}
|
||||
|
||||
loadAfterPublish (options: {
|
||||
video: Pick<VideoDetails, 'id' | 'uuid' | 'shortUUID'>
|
||||
}) {
|
||||
|
@ -788,6 +816,26 @@ export class VideoEdit {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
loadFromPlayerSettingsForm (values: PlayerSettingsForm) {
|
||||
this.playerSettings = values
|
||||
}
|
||||
|
||||
toPlayerSettingsFormPatch (): Required<PlayerSettingsForm> {
|
||||
return {
|
||||
theme: this.playerSettings?.theme ?? 'channel-default'
|
||||
}
|
||||
}
|
||||
|
||||
toPlayerSettingsUpdate (): PlayerVideoSettingsUpdate {
|
||||
if (!this.playerSettings) return undefined
|
||||
|
||||
return {
|
||||
theme: this.playerSettings.theme
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getVideoSource () {
|
||||
return this.metadata.videoSource
|
||||
}
|
||||
|
@ -816,6 +864,10 @@ export class VideoEdit {
|
|||
return this.studioTasks
|
||||
}
|
||||
|
||||
getPlayerSettings () {
|
||||
return this.playerSettings
|
||||
}
|
||||
|
||||
getStudioTasksSummary () {
|
||||
return this.getStudioTasks().map(t => {
|
||||
if (t.name === 'add-intro') {
|
||||
|
@ -932,6 +984,21 @@ export class VideoEdit {
|
|||
return changes
|
||||
}
|
||||
|
||||
hasPlayerSettingsChanges () {
|
||||
if (!this.playerSettings) return false
|
||||
if (!this.saveStore.playerSettings) return true
|
||||
|
||||
const changes = !this.areSameObjects(this.playerSettings, this.saveStore.playerSettings)
|
||||
|
||||
debugLogger('Check if player settings has changes', {
|
||||
playerSettings: this.playerSettings,
|
||||
savePlayerSettings: this.saveStore.playerSettings,
|
||||
changes
|
||||
})
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
hasPendingChanges () {
|
||||
|
@ -941,7 +1008,8 @@ export class VideoEdit {
|
|||
this.hasStudioTasks() ||
|
||||
this.hasChaptersChanges() ||
|
||||
this.hasCommonChanges() ||
|
||||
this.hasPluginDataChanges()
|
||||
this.hasPluginDataChanges() ||
|
||||
this.hasPlayerSettingsChanges()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -31,8 +31,15 @@
|
|||
</div>
|
||||
|
||||
<p-datepicker
|
||||
inputId="originallyPublishedAt" formControlName="originallyPublishedAt" [dateFormat]="calendarDateFormat" [firstDayOfWeek]="0"
|
||||
[showTime]="true" [hideOnDateTimeSelect]="true" [monthNavigator]="true" [yearNavigator]="true" [yearRange]="myYearRange"
|
||||
inputId="originallyPublishedAt"
|
||||
formControlName="originallyPublishedAt"
|
||||
[dateFormat]="calendarDateFormat"
|
||||
[firstDayOfWeek]="0"
|
||||
[showTime]="true"
|
||||
[hideOnDateTimeSelect]="true"
|
||||
[monthNavigator]="true"
|
||||
[yearNavigator]="true"
|
||||
[yearRange]="myYearRange"
|
||||
baseZIndex="20000"
|
||||
>
|
||||
</p-datepicker>
|
||||
|
@ -42,10 +49,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<my-peertube-checkbox
|
||||
inputName="downloadEnabled" formControlName="downloadEnabled"
|
||||
i18n-labelText labelText="Enable download"
|
||||
></my-peertube-checkbox>
|
||||
<my-peertube-checkbox inputName="downloadEnabled" formControlName="downloadEnabled" i18n-labelText labelText="Enable download"></my-peertube-checkbox>
|
||||
|
||||
<div class="form-group" formGroupName="playerSettings">
|
||||
<label i18n for="playerSettingsTheme">Player Theme</label>
|
||||
|
||||
<my-select-player-theme formControlName="theme" inputId="playerSettingsTheme" mode="video" [channel]="videoChannel">
|
||||
</my-select-player-theme>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ServerService } from '@app/core'
|
||||
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
||||
import { BuildFormArgumentTyped } from '@app/shared/form-validators/form-validator.model'
|
||||
import { VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR } from '@app/shared/form-validators/video-validators'
|
||||
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { FormReactiveErrors, FormReactiveMessages, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { SelectPlayerThemeComponent } from '@app/shared/shared-forms/select/select-player-theme.component'
|
||||
import { HTMLServerConfig, PlayerVideoSettings, VideoChannel } from '@peertube/peertube-models'
|
||||
import debug from 'debug'
|
||||
import { DatePickerModule } from 'primeng/datepicker'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
@ -19,6 +20,10 @@ const debugLogger = debug('peertube:video-manage')
|
|||
type Form = {
|
||||
downloadEnabled: FormControl<boolean>
|
||||
originallyPublishedAt: FormControl<Date>
|
||||
|
||||
playerSettings: FormGroup<{
|
||||
theme: FormControl<PlayerVideoSettings['theme']>
|
||||
}>
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
@ -28,12 +33,13 @@ type Form = {
|
|||
],
|
||||
templateUrl: './video-customization.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgIf,
|
||||
DatePickerModule,
|
||||
PeertubeCheckboxComponent,
|
||||
GlobalIconComponent
|
||||
GlobalIconComponent,
|
||||
SelectPlayerThemeComponent
|
||||
]
|
||||
})
|
||||
export class VideoCustomizationComponent implements OnInit, OnDestroy {
|
||||
|
@ -47,6 +53,7 @@ export class VideoCustomizationComponent implements OnInit, OnDestroy {
|
|||
validationMessages: FormReactiveMessages = {}
|
||||
|
||||
videoEdit: VideoEdit
|
||||
videoChannel: Pick<VideoChannel, 'name' | 'displayName'>
|
||||
|
||||
calendarDateFormat: string
|
||||
myYearRange: string
|
||||
|
@ -63,17 +70,24 @@ export class VideoCustomizationComponent implements OnInit, OnDestroy {
|
|||
ngOnInit () {
|
||||
this.serverConfig = this.serverService.getHTMLConfig()
|
||||
|
||||
const { videoEdit } = this.manageController.getStore()
|
||||
const { videoEdit, userChannels } = this.manageController.getStore()
|
||||
this.videoEdit = videoEdit
|
||||
|
||||
const channelItem = userChannels.find(c => c.id === videoEdit.toCommonFormPatch().channelId)
|
||||
this.videoChannel = { name: channelItem.name, displayName: channelItem.label }
|
||||
|
||||
this.buildForm()
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const defaultValues = this.videoEdit.toCommonFormPatch()
|
||||
const obj: BuildFormArgument = {
|
||||
const defaultValues = { ...this.videoEdit.toCommonFormPatch(), playerSettings: this.videoEdit.toPlayerSettingsFormPatch() }
|
||||
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
downloadEnabled: null,
|
||||
originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR
|
||||
originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
|
||||
playerSettings: {
|
||||
theme: null
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
|
@ -93,12 +107,18 @@ export class VideoCustomizationComponent implements OnInit, OnDestroy {
|
|||
debugLogger('Updating form values', formValues)
|
||||
|
||||
this.videoEdit.loadFromCommonForm(formValues)
|
||||
this.videoEdit.loadFromPlayerSettingsForm({
|
||||
theme: formValues.playerSettings.theme
|
||||
})
|
||||
})
|
||||
|
||||
this.formReactiveService.markAllAsDirty(this.form.controls)
|
||||
|
||||
this.updatedSub = this.manageController.getUpdatedObs().subscribe(() => {
|
||||
this.form.patchValue(this.videoEdit.toCommonFormPatch())
|
||||
this.form.patchValue({
|
||||
...this.videoEdit.toCommonFormPatch(),
|
||||
...this.videoEdit.toPlayerSettingsFormPatch()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -184,7 +184,7 @@ export class VideoMainInfoComponent implements OnInit, OnDestroy {
|
|||
.subscribe(res => this.videoCategories = res)
|
||||
|
||||
this.serverService.getVideoLicences()
|
||||
.subscribe(res => this.videoLicences = res)
|
||||
.subscribe(res => this.videoLicences = this.videoService.explainedLicenceLabels(res))
|
||||
|
||||
this.buildLanguages()
|
||||
this.buildPrivacies()
|
||||
|
@ -410,7 +410,7 @@ export class VideoMainInfoComponent implements OnInit, OnDestroy {
|
|||
|
||||
waitTranscodingControl.disable()
|
||||
if (!isInitialPatch) waitTranscodingControl.setValue(false)
|
||||
} else {
|
||||
} else if (waitTranscodingControl.disabled) {
|
||||
scheduleControl.clearValidators()
|
||||
waitTranscodingControl.enable()
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
|
|
|
@ -9,6 +9,7 @@ import { VideoChapterService } from '@app/shared/shared-main/video/video-chapter
|
|||
import { VideoPasswordService } from '@app/shared/shared-main/video/video-password.service'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import {
|
||||
HTMLServerConfig,
|
||||
|
@ -47,6 +48,7 @@ export class VideoManageController implements OnDestroy {
|
|||
private formReactiveService = inject(FormReactiveService)
|
||||
private videoStudio = inject(VideoStudioService)
|
||||
private peertubeRouter = inject(PeerTubeRouterService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
|
||||
private videoEdit: VideoEdit
|
||||
private userChannels: SelectChannelItem[]
|
||||
|
@ -245,6 +247,16 @@ export class VideoManageController implements OnDestroy {
|
|||
|
||||
return this.videoChapterService.updateChapters(videoAttributes.uuid, this.videoEdit.getChaptersEdit())
|
||||
}),
|
||||
switchMap(() => {
|
||||
if (!this.videoEdit.hasPlayerSettingsChanges()) return of(true)
|
||||
|
||||
debugLogger('Update player settings')
|
||||
|
||||
return this.playerSettingsService.updateVideoSettings({
|
||||
videoId: videoAttributes.uuid,
|
||||
settings: this.videoEdit.getPlayerSettings()
|
||||
})
|
||||
}),
|
||||
switchMap(() => {
|
||||
if (!isLive || !this.videoEdit.hasLiveChanges()) return of(true)
|
||||
|
||||
|
@ -283,16 +295,19 @@ export class VideoManageController implements OnDestroy {
|
|||
|
||||
!isLive
|
||||
? this.videoCaptionService.listCaptions(videoAttributes.uuid)
|
||||
: of(undefined)
|
||||
: of(undefined),
|
||||
|
||||
this.playerSettingsService.getVideoSettings({ videoId: videoAttributes.uuid, raw: true })
|
||||
])
|
||||
}),
|
||||
switchMap(([ video, videoPasswords, live, chaptersRes, captionsRes ]) => {
|
||||
switchMap(([ video, videoPasswords, live, chaptersRes, captionsRes, playerSettings ]) => {
|
||||
return this.videoEdit.loadFromAPI({
|
||||
video,
|
||||
videoPasswords: videoPasswords.map(p => p.password),
|
||||
live,
|
||||
chapters: chaptersRes?.chapters,
|
||||
captions: captionsRes?.data
|
||||
captions: captionsRes?.data,
|
||||
playerSettings
|
||||
})
|
||||
}),
|
||||
first(), // To complete
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
ServerStats,
|
||||
VideoCommentPolicy,
|
||||
VideoConstant,
|
||||
VideoLicenceType,
|
||||
VideoPlaylistPrivacyType,
|
||||
VideoPrivacyType
|
||||
} from '@peertube/peertube-models'
|
||||
|
@ -30,7 +31,7 @@ export class ServerService {
|
|||
configReloaded = new Subject<ServerConfig>()
|
||||
|
||||
private localeObservable: Observable<any>
|
||||
private videoLicensesObservable: Observable<VideoConstant<number>[]>
|
||||
private videoLicensesObservable: Observable<VideoConstant<VideoLicenceType>[]>
|
||||
private videoCategoriesObservable: Observable<VideoConstant<number>[]>
|
||||
private videoPrivaciesObservable: Observable<VideoConstant<VideoPrivacyType>[]>
|
||||
private videoPlaylistPrivaciesObservable: Observable<VideoConstant<VideoPlaylistPrivacyType>[]>
|
||||
|
@ -129,7 +130,7 @@ export class ServerService {
|
|||
|
||||
getVideoLicences () {
|
||||
if (!this.videoLicensesObservable) {
|
||||
this.videoLicensesObservable = this.loadAttributeEnum<number>(ServerService.BASE_VIDEO_URL, 'licences')
|
||||
this.videoLicensesObservable = this.loadAttributeEnum<VideoLicenceType>(ServerService.BASE_VIDEO_URL, 'licences')
|
||||
}
|
||||
|
||||
return this.videoLicensesObservable.pipe(first())
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<my-notification-dropdown class="margin-button"></my-notification-dropdown>
|
||||
|
||||
<my-button
|
||||
i18n-title title="Go to the manage your account page"
|
||||
i18n-title title="Go to the page where you can manage your account"
|
||||
theme="tertiary" rounded="true" class="margin-button settings-button" icon="cog" ptRouterLink="/my-account"
|
||||
></my-button>
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<h4 i18n class="title">General information</h4>
|
||||
|
||||
<div class="text-content">You can edit this information later</div>
|
||||
<div i18n class="text-content">You can edit this information later</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="form-group">
|
||||
|
|
|
@ -47,59 +47,59 @@ export class CommunityBasedConfigComponent implements OnInit {
|
|||
registrationOptions: SelectOptionsItem<RegistrationType>[] = [
|
||||
{
|
||||
id: 'open',
|
||||
label: 'Open',
|
||||
description: 'Anyone can register and use the platform'
|
||||
label: $localize`Open`,
|
||||
description: $localize`Anyone can register and use the platform`
|
||||
},
|
||||
|
||||
{
|
||||
id: 'approval',
|
||||
label: 'Requires approval',
|
||||
description: 'Anyone can register, but a moderator must approve their account before they can use the platform'
|
||||
label: $localize`Requires approval`,
|
||||
description: $localize`Anyone can register, but a moderator must approve their account before they can use the platform`
|
||||
},
|
||||
{
|
||||
id: 'closed',
|
||||
label: 'Closed',
|
||||
description: 'Only an administrator can create users on the platform'
|
||||
label: $localize`Closed`,
|
||||
description: $localize`Only an administrator can create users on the platform`
|
||||
}
|
||||
]
|
||||
|
||||
importOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: 'Enabled',
|
||||
label: $localize`Enabled`,
|
||||
description:
|
||||
'Your community can import videos from remote platforms (YouTube, Vimeo...) and automatically synchronize remote channels'
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: 'Disabled',
|
||||
description: 'Your community cannot import or synchronize content from remote platforms'
|
||||
label: $localize`Disabled`,
|
||||
description: $localize`Your community cannot import or synchronize content from remote platforms`
|
||||
}
|
||||
]
|
||||
|
||||
liveOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: 'Yes',
|
||||
description: 'Your community can live stream on the platform (this requires extra moderation work)'
|
||||
label: $localize`Yes`,
|
||||
description: $localize`Your community can live stream on the platform (this requires extra moderation work)`
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: 'No',
|
||||
description: 'Your community is not permitted to run live streams on the platform'
|
||||
label: $localize`No`,
|
||||
description: $localize`Your community is not permitted to run live streams on the platform`
|
||||
}
|
||||
]
|
||||
|
||||
globalSearchOptions: SelectOptionsItem<string>[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: 'Enable global search',
|
||||
description: 'Use https://sepiasearch.org as default search engine to search for content across all known peertube platforms'
|
||||
label: $localize`Enable global search`,
|
||||
description: $localize`Use https://sepiasearch.org as default search engine to search for content across all known PeerTube platforms`
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: 'Disable global search',
|
||||
description: 'Use your platform search engine which only displays local content'
|
||||
label: $localize`Disable global search`,
|
||||
description: $localize`Use your platform search engine which only displays local content`
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -45,62 +45,62 @@ export class InstitutionalConfigComponent implements OnInit {
|
|||
p2pOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: 'Enabled',
|
||||
description: 'Enable P2P streaming by default for anonymous and new users'
|
||||
label: $localize`Enabled`,
|
||||
description: $localize`Enable P2P streaming by default for anonymous and new users`
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: 'Disabled',
|
||||
description: 'Disable P2P streaming'
|
||||
label: $localize`Disabled`,
|
||||
description: $localize`Disable P2P streaming`
|
||||
}
|
||||
]
|
||||
|
||||
transcriptionOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: 'Enabled',
|
||||
description: 'Enable automatic transcription of videos to automatically generate subtitles'
|
||||
label: $localize`Enabled`,
|
||||
description: $localize`Enable automatic transcription of videos to automatically generate subtitles`
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: 'Disabled',
|
||||
description: 'Disable automatic transcription of videos'
|
||||
label: $localize`Disabled`,
|
||||
description: $localize`Disable automatic transcription of videos`
|
||||
}
|
||||
]
|
||||
|
||||
keepOriginalVideoOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: 'Yes',
|
||||
description: 'Keep the original video file on the server'
|
||||
label: $localize`Yes`,
|
||||
description: $localize`Keep the original video file on the server`
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: 'No',
|
||||
description: 'Delete the original video file after processing'
|
||||
label: $localize`No`,
|
||||
description: $localize`Delete the original video file after processing`
|
||||
}
|
||||
]
|
||||
|
||||
authenticationOptions: SelectOptionsItem<AuthType>[] = [
|
||||
{
|
||||
id: 'local',
|
||||
label: 'Disabled',
|
||||
description: 'Your platform will manage user registration and login internally'
|
||||
label: $localize`Disabled`,
|
||||
description: $localize`Your platform will manage user registration and login internally`
|
||||
},
|
||||
{
|
||||
id: 'ldap',
|
||||
label: 'LDAP',
|
||||
description: 'Use LDAP for user authentication'
|
||||
label: $localize`LDAP`,
|
||||
description: $localize`Use LDAP for user authentication`
|
||||
},
|
||||
{
|
||||
id: 'oidc',
|
||||
label: 'OIDC',
|
||||
description: 'Use OpenID Connect for user authentication'
|
||||
label: $localize`OIDC`,
|
||||
description: $localize`Use OpenID Connect for user authentication`
|
||||
},
|
||||
{
|
||||
id: 'saml',
|
||||
label: 'SAML',
|
||||
description: 'Use SAML 2.0 for user authentication'
|
||||
label: $localize`SAML`,
|
||||
description: $localize`Use SAML 2.0 for user authentication`
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -44,37 +44,38 @@ export class PrivateInstanceConfigComponent implements OnInit {
|
|||
importOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: 'Enabled',
|
||||
description: 'Users can import videos from remote platforms (YouTube, Vimeo...) and automatically synchronize remote channels'
|
||||
label: $localize`Enabled`,
|
||||
description:
|
||||
$localize`Users can import videos from remote platforms (YouTube, Vimeo...) and automatically synchronize remote channels`
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: 'Disabled',
|
||||
description: 'Disable video import and channel synchronization'
|
||||
label: $localize`Disabled`,
|
||||
description: $localize`Disable video import and channel synchronization`
|
||||
}
|
||||
]
|
||||
|
||||
liveOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: 'Yes'
|
||||
label: $localize`Yes`
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: 'No'
|
||||
label: $localize`No`
|
||||
}
|
||||
]
|
||||
|
||||
keepOriginalVideoOptions: SelectOptionsItem<EnabledDisabled>[] = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: 'Yes',
|
||||
description: 'Keep the original video file on the server'
|
||||
label: $localize`Yes`,
|
||||
description: $localize`Keep the original video file on the server`
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: 'No',
|
||||
description: 'Delete the original video file after processing'
|
||||
label: $localize`No`,
|
||||
description: $localize`Delete the original video file after processing`
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ export class UsageType {
|
|||
if (!exists(this.registration)) return
|
||||
|
||||
if (this.registration === 'open') {
|
||||
this.addExplanation($localize`<strong>Allow</strong> any user <strong>to register</strong>`)
|
||||
this.addExplanation($localize`:bullet point of "PeerTube will\:":<strong>Allow</strong> any user <strong>to register</strong>`)
|
||||
|
||||
this.addConfig({
|
||||
signup: {
|
||||
|
@ -169,7 +169,9 @@ export class UsageType {
|
|||
}
|
||||
})
|
||||
} else if (this.registration === 'approval') {
|
||||
this.addExplanation($localize`Allow users to <strong>apply for registration</strong> on your platform`)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":Allow users to <strong>apply for registration</strong> on your platform`
|
||||
)
|
||||
|
||||
this.addConfig({
|
||||
signup: {
|
||||
|
@ -178,7 +180,7 @@ export class UsageType {
|
|||
}
|
||||
})
|
||||
} else if (this.registration === 'closed') {
|
||||
this.addExplanation($localize`<strong>Disable</strong> user <strong>registration</strong>`)
|
||||
this.addExplanation($localize`:bullet point of "PeerTube will\:":<strong>Disable</strong> user <strong>registration</strong>`)
|
||||
|
||||
this.addConfig({
|
||||
signup: {
|
||||
|
@ -188,7 +190,9 @@ export class UsageType {
|
|||
}
|
||||
|
||||
if (this.registration === 'approval' || this.registration === 'open') {
|
||||
this.addExplanation($localize`Require <strong>moderator approval</strong> for videos published by your community`)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":Require <strong>moderator approval</strong> for videos published by your community`
|
||||
)
|
||||
|
||||
this.addConfig({
|
||||
autoBlacklist: {
|
||||
|
@ -213,13 +217,18 @@ export class UsageType {
|
|||
|
||||
if (this.videoQuota === 0) {
|
||||
this.addExplanation(
|
||||
$localize`<strong>Prevent</strong> new users <strong>from uploading videos</strong> (can be changed by moderators)`
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Prevent</strong> new users <strong>from uploading videos</strong> (can be changed by moderators)`
|
||||
)
|
||||
} else if (this.videoQuota === -1) {
|
||||
this.addExplanation($localize`Will <strong>not limit the amount of videos</strong> new users can upload`)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Not limit the amount of videos</strong> new users can upload`
|
||||
)
|
||||
} else {
|
||||
this.addExplanation(
|
||||
$localize`Set <strong>video quota to ${getBytes(this.videoQuota, 0)}</strong> for new users (can be changed by moderators)`
|
||||
$localize`:bullet point of "PeerTube will\:":Set <strong>video quota to ${
|
||||
getBytes(this.videoQuota, 0)
|
||||
}</strong> for new users (can be changed by moderators)`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -243,10 +252,13 @@ export class UsageType {
|
|||
if (this.remoteImport === 'enabled') {
|
||||
this.addExplanation(
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`<strong>Allow</strong> your users <strong>to import and synchronize</strong> videos from remote platforms (YouTube, Vimeo...)`
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Allow</strong> your users <strong>to import and synchronize</strong> videos from remote platforms (YouTube, Vimeo...)`
|
||||
)
|
||||
} else {
|
||||
this.addExplanation($localize`<strong>Prevent</strong> your users <strong>from importing videos</strong> from remote platforms`)
|
||||
this.addExplanation(
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Prevent</strong> your users <strong>from importing videos</strong> from remote platforms`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,10 +276,12 @@ export class UsageType {
|
|||
|
||||
this.addExplanation(
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`<strong>Allow</strong> your users <strong>to stream lives</strong> and chat with their viewers using the <strong>Livechat</strong> plugin`
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Allow</strong> your users <strong>to stream lives</strong> and chat with their viewers using the <strong>Livechat</strong> plugin`
|
||||
)
|
||||
} else {
|
||||
this.addExplanation($localize`<strong>Prevent</strong> your users from running <strong>live streams</strong>`)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Prevent</strong> your users from running <strong>live streams</strong>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -283,9 +297,13 @@ export class UsageType {
|
|||
})
|
||||
|
||||
if (this.defaultPrivacy === VideoPrivacy.INTERNAL) {
|
||||
this.addExplanation($localize`Set the <strong>default video privacy</strong> to <strong>Internal</strong>`)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":Set the <strong>default video privacy</strong> to <strong>Internal</strong>`
|
||||
)
|
||||
} else if (this.defaultPrivacy === VideoPrivacy.PUBLIC) {
|
||||
this.addExplanation($localize`Set the <strong>default video privacy</strong> to <strong>Public</strong>`)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":Set the <strong>default video privacy</strong> to <strong>Public</strong>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,7 +318,7 @@ export class UsageType {
|
|||
}
|
||||
})
|
||||
|
||||
this.addExplanation($localize`<strong>Require approval</strong> by default of new video comment`)
|
||||
this.addExplanation($localize`:bullet point of "PeerTube will\:":<strong>Require approval</strong> by default of new video comment`)
|
||||
}
|
||||
|
||||
private computeP2P () {
|
||||
|
@ -320,9 +338,13 @@ export class UsageType {
|
|||
})
|
||||
|
||||
if (this.p2p === 'enabled') {
|
||||
this.addExplanation($localize`<strong>Enable P2P streaming</strong> by default for anonymous and new users`)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Enable P2P streaming</strong> by default for anonymous and new users`
|
||||
)
|
||||
} else {
|
||||
this.addExplanation($localize`<strong>Disable P2P streaming</strong> by default for anonymous and new users`)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Disable P2P streaming</strong> by default for anonymous and new users`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,9 +363,15 @@ export class UsageType {
|
|||
})
|
||||
|
||||
if (this.federation === 'enabled') {
|
||||
this.addExplanation($localize`<strong>Allow</strong> external platforms/users to <strong>subscribe</strong> to your content`)
|
||||
this.addExplanation(
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Allow</strong> external platforms/users to <strong>subscribe</strong> to your content`
|
||||
)
|
||||
} else {
|
||||
this.addExplanation($localize`<strong>Prevent</strong> external platforms/users to <strong>subscribe to your content</strong>`)
|
||||
this.addExplanation(
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Prevent</strong> external platforms/users to <strong>subscribe to your content</strong>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -359,7 +387,7 @@ export class UsageType {
|
|||
})
|
||||
|
||||
if (this.keepOriginalVideo === 'enabled') {
|
||||
this.addExplanation($localize`Will <strong>save a copy</strong> of the uploaded video file`)
|
||||
this.addExplanation($localize`:bullet point of "PeerTube will\:":<strong>Save a copy</strong> of the uploaded video file`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -376,7 +404,8 @@ export class UsageType {
|
|||
|
||||
if (this.allowReplaceFile === 'enabled') {
|
||||
this.addExplanation(
|
||||
$localize`Will <strong>allow</strong> your users <strong>to replace a video</strong> that has already been published`
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Allow</strong> your users <strong>to replace a video</strong> that has already been published`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -410,7 +439,8 @@ export class UsageType {
|
|||
|
||||
if (this.globalSearch === 'enabled') {
|
||||
this.addExplanation(
|
||||
$localize`Set <a href="https://sepiasearch.org" target="_blank">SepiaSearch</a> as <strong>default search engine</strong>`
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`:bullet point of "PeerTube will\:":Set <a href="https://sepiasearch.org" target="_blank">SepiaSearch</a> as <strong>default search engine</strong>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -426,7 +456,8 @@ export class UsageType {
|
|||
|
||||
if (this.transcription === 'enabled') {
|
||||
this.addExplanation(
|
||||
$localize`<strong>Enable automatic transcription</strong> of videos to create subtitles and improve accessibility`
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`:bullet point of "PeerTube will\:":<strong>Enable automatic transcription</strong> of videos to create subtitles and improve accessibility`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -434,18 +465,26 @@ export class UsageType {
|
|||
private computeAuth () {
|
||||
if (!exists(this.authType)) return
|
||||
|
||||
const configStr = $localize` The plugin <strong>must be configured</strong> after the pre-configuration wizard confirmation.`
|
||||
const configStr =
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`:bullet point of "PeerTube will\:": The plugin <strong>must be configured</strong> after the pre-configuration wizard confirmation.`
|
||||
|
||||
if (this.authType === 'ldap') {
|
||||
this.addExplanation($localize`Install the <strong>LDAP</strong> authentication plugin.` + configStr)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":Install the <strong>LDAP</strong> authentication plugin.` + configStr
|
||||
)
|
||||
|
||||
this.plugins.push('peertube-plugin-auth-ldap')
|
||||
} else if (this.authType === 'saml') {
|
||||
this.addExplanation($localize`Install the <strong>SAML 2.0</strong> authentication plugin.` + configStr)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":Install the <strong>SAML 2.0</strong> authentication plugin.` + configStr
|
||||
)
|
||||
|
||||
this.plugins.push('peertube-plugin-auth-saml2')
|
||||
} else if (this.authType === 'oidc') {
|
||||
this.addExplanation($localize`Install the <strong>OpenID Connect</strong> authentication plugin.` + configStr)
|
||||
this.addExplanation(
|
||||
$localize`:bullet point of "PeerTube will\:":Install the <strong>OpenID Connect</strong> authentication plugin.` + configStr
|
||||
)
|
||||
|
||||
this.plugins.push('peertube-plugin-auth-openid-connect')
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="actor" *ngIf="actor">
|
||||
<div class="position-relative me-3">
|
||||
<my-actor-avatar [actor]="actor" [actorType]="actorType()" [previewImage]="preview" size="100"></my-actor-avatar>
|
||||
<my-actor-avatar [actor]="actor" [actorType]="actorType()" [previewImage]="previewUrl" size="100"></my-actor-avatar>
|
||||
|
||||
@if (editable()) {
|
||||
@if (hasAvatar()) {
|
||||
|
|
|
@ -41,7 +41,7 @@ export class ActorAvatarEditComponent implements OnInit, OnChanges {
|
|||
maxAvatarSize = 0
|
||||
avatarExtensions = ''
|
||||
|
||||
preview: string
|
||||
previewUrl: string
|
||||
|
||||
actor: ActorAvatarInput
|
||||
|
||||
|
@ -55,6 +55,8 @@ export class ActorAvatarEditComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
ngOnChanges () {
|
||||
this.previewUrl = undefined
|
||||
|
||||
this.actor = {
|
||||
avatars: this.avatars(),
|
||||
name: this.username()
|
||||
|
@ -73,16 +75,23 @@ export class ActorAvatarEditComponent implements OnInit, OnChanges {
|
|||
this.avatarChange.emit(formData)
|
||||
|
||||
if (this.previewImage()) {
|
||||
imageToDataURL(avatarfile).then(result => this.preview = result)
|
||||
imageToDataURL(avatarfile).then(result => this.previewUrl = result)
|
||||
}
|
||||
}
|
||||
|
||||
deleteAvatar () {
|
||||
this.preview = undefined
|
||||
if (this.previewImage()) {
|
||||
this.previewUrl = null
|
||||
this.actor.avatars = []
|
||||
}
|
||||
|
||||
this.avatarDelete.emit()
|
||||
}
|
||||
|
||||
hasAvatar () {
|
||||
return !!this.preview || this.avatars().length !== 0
|
||||
// User deleted the avatar
|
||||
if (this.previewUrl === null) return false
|
||||
|
||||
return !!this.previewUrl || this.avatars().length !== 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
<div class="actor">
|
||||
<div class="actor-img-edit-container">
|
||||
<div class="banner-placeholder">
|
||||
<img *ngIf="hasBanner()" [src]="preview || bannerUrl()" alt="Banner" />
|
||||
<img *ngIf="hasBanner()" [src]="getBannerUrl()" alt="Banner" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="!hasBanner()" class="actor-img-edit-button button-file primary-button button-focus-within" [ngbTooltip]="bannerFormat" placement="right" container="body">
|
||||
<ng-container *ngTemplateOutlet="uploadNewBanner"></ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasBanner()" ngbDropdown placement="right">
|
||||
<button type="button" class="actor-img-edit-button button-file primary-button" ngbDropdownToggle>
|
||||
<my-global-icon iconName="edit"></my-global-icon>
|
||||
<span i18n>Change your banner</span>
|
||||
</button>
|
||||
|
||||
<div ngbDropdownMenu>
|
||||
<div class="dropdown-item dropdown-file button-focus-within" [ngbTooltip]="bannerFormat">
|
||||
<ng-container *ngTemplateOutlet="uploadNewBanner"></ng-container>
|
||||
</div>
|
||||
|
||||
<button type="button" class="dropdown-item" (click)="deleteBanner()">
|
||||
<my-global-icon iconName="delete"></my-global-icon>
|
||||
<span i18n>Remove banner</span>
|
||||
</button>
|
||||
@if (!hasBanner()) {
|
||||
<div class="actor-img-edit-button button-file primary-button button-focus-within" [ngbTooltip]="bannerFormat" placement="right" container="body">
|
||||
<ng-container *ngTemplateOutlet="uploadNewBanner"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div ngbDropdown placement="right">
|
||||
<button type="button" class="actor-img-edit-button button-file primary-button" ngbDropdownToggle>
|
||||
<my-global-icon iconName="edit"></my-global-icon>
|
||||
<span i18n>Change your banner</span>
|
||||
</button>
|
||||
|
||||
<div ngbDropdownMenu>
|
||||
<div class="dropdown-item dropdown-file button-focus-within" [ngbTooltip]="bannerFormat">
|
||||
<ng-container *ngTemplateOutlet="uploadNewBanner"></ng-container>
|
||||
</div>
|
||||
|
||||
<button type="button" class="dropdown-item" (click)="deleteBanner()">
|
||||
<my-global-icon iconName="delete"></my-global-icon>
|
||||
<span i18n>Remove banner</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { NgIf, NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, ElementRef, OnInit, inject, input, output, viewChild } from '@angular/core'
|
||||
import { CommonModule, NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, ElementRef, OnInit, booleanAttribute, inject, input, output, viewChild } from '@angular/core'
|
||||
import { SafeResourceUrl } from '@angular/platform-browser'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { NgbDropdown, NgbDropdownMenu, NgbDropdownToggle, NgbPopover, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbDropdownModule, NgbPopover, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { getBytes } from '@root-helpers/bytes'
|
||||
import { imageToDataURL } from '@root-helpers/images'
|
||||
import { GlobalIconComponent } from '../shared-icons/global-icon.component'
|
||||
|
@ -14,7 +14,7 @@ import { GlobalIconComponent } from '../shared-icons/global-icon.component'
|
|||
'./actor-image-edit.scss',
|
||||
'./actor-banner-edit.component.scss'
|
||||
],
|
||||
imports: [ NgIf, NgbTooltip, NgTemplateOutlet, NgbDropdown, NgbDropdownToggle, GlobalIconComponent, NgbDropdownMenu ]
|
||||
imports: [ CommonModule, NgbTooltipModule, NgTemplateOutlet, NgbDropdownModule, GlobalIconComponent ]
|
||||
})
|
||||
export class ActorBannerEditComponent implements OnInit {
|
||||
private serverService = inject(ServerService)
|
||||
|
@ -23,8 +23,8 @@ export class ActorBannerEditComponent implements OnInit {
|
|||
readonly bannerfileInput = viewChild<ElementRef<HTMLInputElement>>('bannerfileInput')
|
||||
readonly bannerPopover = viewChild<NgbPopover>('bannerPopover')
|
||||
|
||||
readonly bannerUrl = input<string>(undefined)
|
||||
readonly previewImage = input(false)
|
||||
readonly bannerUrl = input<string>()
|
||||
readonly previewImage = input(false, { transform: booleanAttribute })
|
||||
|
||||
readonly bannerChange = output<FormData>()
|
||||
readonly bannerDelete = output()
|
||||
|
@ -63,11 +63,23 @@ export class ActorBannerEditComponent implements OnInit {
|
|||
}
|
||||
|
||||
deleteBanner () {
|
||||
this.preview = undefined
|
||||
if (this.previewImage()) {
|
||||
this.preview = null
|
||||
}
|
||||
|
||||
this.bannerDelete.emit()
|
||||
}
|
||||
|
||||
hasBanner () {
|
||||
// User deleted the avatar
|
||||
if (this.preview === null) return false
|
||||
|
||||
return !!this.preview || !!this.bannerUrl()
|
||||
}
|
||||
|
||||
getBannerUrl () {
|
||||
if (this.preview === null) return ''
|
||||
|
||||
return this.preview || this.bannerUrl()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ export class AdminConfigService {
|
|||
private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/config'
|
||||
|
||||
transcodingThreadOptions: SelectOptionsItem[] = []
|
||||
transcodingResolutionOptions: ResolutionOption[] = []
|
||||
|
||||
constructor () {
|
||||
this.transcodingThreadOptions = [
|
||||
|
@ -48,13 +47,18 @@ export class AdminConfigService {
|
|||
{ id: 16, label: '16' },
|
||||
{ id: 32, label: '32' }
|
||||
]
|
||||
}
|
||||
|
||||
this.transcodingResolutionOptions = [
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getTranscodingOptions (type: 'live' | 'vod'): ResolutionOption[] {
|
||||
return [
|
||||
{
|
||||
id: '0p',
|
||||
label: $localize`Audio-only`,
|
||||
description:
|
||||
$localize`"Split audio and video" must be enabled for the PeerTube player to propose an "Audio only" resolution to users`
|
||||
description: type === 'vod'
|
||||
? $localize`"Split audio and video" must be enabled for the PeerTube player to propose an "Audio only" resolution to users`
|
||||
: undefined
|
||||
},
|
||||
{
|
||||
id: '144p',
|
||||
|
@ -141,10 +145,10 @@ export class AdminConfigService {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
buildFormResolutions () {
|
||||
buildFormResolutions (type: 'live' | 'vod') {
|
||||
const formResolutions = {} as Record<keyof FormResolutions, BuildFormValidator>
|
||||
|
||||
for (const resolution of this.transcodingResolutionOptions) {
|
||||
for (const resolution of this.getTranscodingOptions(type)) {
|
||||
formResolutions[resolution.id] = null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
<my-video-playlist-miniature *ngIf="playlist" [playlist]="playlist">
|
||||
<my-video-playlist-miniature *ngIf="playlist" [playlist]="playlist" toManage="false">
|
||||
</my-video-playlist-miniature>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<ng-template [ngTemplateOutlet]="customItemTemplate" [ngTemplateOutletContext]="{ $implicit: item }"></ng-template>
|
||||
} @else {
|
||||
<div>
|
||||
<div class="d-flex align-items-center item-label">
|
||||
<div class="d-flex align-items-center item-label custom-item">
|
||||
<img *ngIf="item.imageUrl" alt="" class="me-2" [src]="item.imageUrl" />
|
||||
|
||||
<span class="ellipsis" [ngClass]="item.classes">{{ item.label }}</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
|
@ -10,8 +10,15 @@ img {
|
|||
|
||||
.muted {
|
||||
font-size: 90%;
|
||||
line-height: 1.2;
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
p-select ::ng-deep p-overlay {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, forwardRef, inject, input, OnInit } from '@angular/core'
|
||||
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { ServerService } from '@app/core'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { PlayerChannelSettings, PlayerTheme, PlayerVideoSettings, VideoChannel } from '@peertube/peertube-models'
|
||||
import { of } from 'rxjs'
|
||||
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
||||
import { SelectOptionsComponent } from './select-options.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-select-player-theme',
|
||||
template: `
|
||||
<my-select-options
|
||||
[inputId]="inputId()"
|
||||
|
||||
[items]="themes"
|
||||
|
||||
[(ngModel)]="selectedId"
|
||||
(ngModelChange)="onModelChange()"
|
||||
filter="false"
|
||||
></my-select-options>
|
||||
`,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SelectPlayerThemeComponent),
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
imports: [ FormsModule, CommonModule, SelectOptionsComponent ]
|
||||
})
|
||||
export class SelectPlayerThemeComponent implements ControlValueAccessor, OnInit {
|
||||
private serverService = inject(ServerService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
|
||||
readonly inputId = input.required<string>()
|
||||
readonly mode = input.required<'instance' | 'video' | 'channel'>()
|
||||
|
||||
readonly channel = input<Pick<VideoChannel, 'name' | 'displayName'>>()
|
||||
|
||||
themes: SelectOptionsItem<PlayerVideoSettings['theme']>[]
|
||||
selectedId: PlayerTheme
|
||||
|
||||
ngOnInit () {
|
||||
if (this.mode() === 'video' && !this.channel()) {
|
||||
throw new Error('Channel must be specified in video mode')
|
||||
}
|
||||
|
||||
this.buildOptions()
|
||||
}
|
||||
|
||||
propagateChange = (_: any) => {
|
||||
// empty
|
||||
}
|
||||
|
||||
writeValue (id: PlayerTheme) {
|
||||
this.selectedId = id
|
||||
}
|
||||
|
||||
registerOnChange (fn: (_: any) => void) {
|
||||
this.propagateChange = fn
|
||||
}
|
||||
|
||||
registerOnTouched () {
|
||||
// Unused
|
||||
}
|
||||
|
||||
onModelChange () {
|
||||
this.propagateChange(this.selectedId)
|
||||
}
|
||||
|
||||
private buildOptions () {
|
||||
const config = this.serverService.getHTMLConfig()
|
||||
const instanceName = config.instance.name
|
||||
const instancePlayerTheme = this.getLabelOf(config.defaults.player.theme)
|
||||
|
||||
this.themes = []
|
||||
|
||||
if (this.mode() === 'channel' || this.mode() === 'video') {
|
||||
this.themes.push(
|
||||
{ id: 'instance-default', label: $localize`${instanceName} setting (${instancePlayerTheme})` }
|
||||
)
|
||||
}
|
||||
|
||||
if (this.mode() === 'video') {
|
||||
this.themes.push(
|
||||
{ id: 'channel-default', label: $localize`${this.channel().displayName} setting` }
|
||||
)
|
||||
|
||||
this.scheduleChannelUpdate()
|
||||
}
|
||||
|
||||
this.themes = this.themes.concat(this.getPlayerThemes())
|
||||
}
|
||||
|
||||
private scheduleChannelUpdate () {
|
||||
this.playerSettingsService.getChannelSettings({ channelHandle: this.channel().name, raw: true }).subscribe({
|
||||
next: settings => {
|
||||
this.themes.find(t => t.id === 'channel-default').label = this.buildChannelLabel(settings)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private buildChannelLabel (channelRawPlayerSettings: PlayerChannelSettings) {
|
||||
const config = this.serverService.getHTMLConfig()
|
||||
const instanceName = config.instance.name
|
||||
const instancePlayerTheme = this.getLabelOf(config.defaults.player.theme)
|
||||
|
||||
const channelRawTheme = channelRawPlayerSettings.theme
|
||||
|
||||
const channelPlayerTheme = channelRawTheme === 'instance-default'
|
||||
? $localize`from ${instanceName} setting\: ${instancePlayerTheme}`
|
||||
: this.getLabelOf(channelRawTheme)
|
||||
|
||||
return $localize`${this.channel().displayName} channel setting (${channelPlayerTheme})`
|
||||
}
|
||||
|
||||
private getLabelOf (playerTheme: PlayerTheme) {
|
||||
return this.getPlayerThemes().find(t => t.id === playerTheme)?.label
|
||||
}
|
||||
|
||||
private getPlayerThemes (): SelectOptionsItem<PlayerVideoSettings['theme']>[] {
|
||||
return [
|
||||
{ id: 'galaxy', label: $localize`Galaxy`, description: $localize`Original theme` },
|
||||
{ id: 'lucide', label: $localize`Lucide`, description: $localize`A clean and modern theme` }
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { AfterViewChecked, booleanAttribute, Directive, ElementRef, inject, input, OnDestroy, OnInit, output } from '@angular/core'
|
||||
import { PeerTubeRouterService } from '@app/core'
|
||||
import { fromEvent, Observable, Subscription } from 'rxjs'
|
||||
import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators'
|
||||
|
||||
|
@ -8,7 +7,6 @@ import { distinctUntilChanged, filter, map, share, startWith, throttleTime } fro
|
|||
standalone: true
|
||||
})
|
||||
export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewChecked {
|
||||
private peertubeRouter = inject(PeerTubeRouterService)
|
||||
private el = inject(ElementRef)
|
||||
|
||||
readonly percentLimit = input(70)
|
||||
|
@ -18,7 +16,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh
|
|||
readonly nearOfBottom = output()
|
||||
|
||||
private decimalLimit = 0
|
||||
private lastCurrentBottom = -1
|
||||
private lastCurrentBottom: number
|
||||
private scrollDownSub: Subscription
|
||||
private container: HTMLElement
|
||||
|
||||
|
@ -98,6 +96,11 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh
|
|||
}
|
||||
|
||||
private isScrollingDown (current: number) {
|
||||
if (this.lastCurrentBottom === undefined) {
|
||||
this.lastCurrentBottom = current
|
||||
return false
|
||||
}
|
||||
|
||||
const result = this.lastCurrentBottom < current
|
||||
|
||||
this.lastCurrentBottom = current
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
h1 {
|
||||
color: pvar(--fg-200);
|
||||
|
|
|
@ -31,6 +31,8 @@ import {
|
|||
VideoDetails as VideoDetailsServerModel,
|
||||
VideoFile,
|
||||
VideoFileMetadata,
|
||||
VideoLicence,
|
||||
VideoLicenceType,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
VideosCommonQuery,
|
||||
|
@ -526,6 +528,8 @@ export class VideoService {
|
|||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
explainedPrivacyLabels (serverPrivacies: VideoConstant<VideoPrivacyType>[], defaultPrivacyId: VideoPrivacyType = VideoPrivacy.PUBLIC) {
|
||||
const descriptions = {
|
||||
[VideoPrivacy.PRIVATE]: $localize`Only I can see this video`,
|
||||
|
@ -549,6 +553,30 @@ export class VideoService {
|
|||
}
|
||||
}
|
||||
|
||||
explainedLicenceLabels (serverLicences: VideoConstant<VideoLicenceType>[]) {
|
||||
const descriptions = {
|
||||
[VideoLicence['CC-BY']]: $localize`CC-BY`,
|
||||
[VideoLicence['CC-BY-SA']]: $localize`CC-BY-SA`,
|
||||
[VideoLicence['CC-BY-ND']]: $localize`CC-BY-ND`,
|
||||
[VideoLicence['CC-BY-NC']]: $localize`CC-BY-NC`,
|
||||
[VideoLicence['CC-BY-NC-SA']]: $localize`CC-BY-NC-SA`,
|
||||
[VideoLicence['CC-BY-NC-ND']]: $localize`CC-BY-NC-ND`,
|
||||
[VideoLicence['CC0']]: '',
|
||||
[VideoLicence.PDM]: $localize`Public domain mark`,
|
||||
[VideoLicence['COPYRIGHT']]: 'You are the owner of the content or you have the rights of the copyright holders'
|
||||
}
|
||||
|
||||
return serverLicences.map(p => {
|
||||
return {
|
||||
...p,
|
||||
|
||||
description: descriptions[p.id]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
buildNSFWTooltip (video: Pick<VideoServerModel, 'nsfw' | 'nsfwFlags'>) {
|
||||
const flags: string[] = []
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
@if (isInSelectionMode()) {
|
||||
<my-action-dropdown i18n-label label="Batch actions" theme="primary" [actions]="bulkActions()" [entry]="selectedRows"></my-action-dropdown>
|
||||
} @else {
|
||||
<strong *ngIf="totalTitle" i18n [ngClass]="{ 'opacity-0': loading }">
|
||||
<strong *ngIf="totalTitle" [ngClass]="{ 'opacity-0': loading }">
|
||||
<ng-template *ngTemplateOutlet="totalTitle; context: { $implicit: totalRecords }"></ng-template>
|
||||
</strong>
|
||||
|
||||
|
|
|
@ -95,9 +95,6 @@ export class VideoFilters {
|
|||
|
||||
if (noChanges) return
|
||||
|
||||
console.log(currentFormObjectString)
|
||||
console.log(this.oldFormObjectString)
|
||||
|
||||
this.oldFormObjectString = currentFormObjectString
|
||||
|
||||
for (const cb of this.onChangeCallbacks) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
[internalLink]="playlistRouterLink" [href]="playlistHref" [target]="playlistTarget" inheritParentStyle="true" inheritParentDimension="true"
|
||||
[title]="playlist().description" class="miniature-thumbnail" tabindex="-1"
|
||||
>
|
||||
<img alt="" [attr.aria-labelledby]="playlist().displayName" [attr.src]="playlist().thumbnailUrl" />
|
||||
<img alt="" [attr.aria-label]="playlist().displayName" [attr.src]="playlist().thumbnailUrl" />
|
||||
|
||||
<div class="miniature-playlist-info-overlay">
|
||||
<ng-container i18n>{playlist().videosLength, plural, =0 {No videos} =1 {1 video} other {{{ playlist().videosLength }} videos}}</ng-container>
|
||||
|
|
|
@ -17,7 +17,7 @@ export class VideoPlaylistMiniatureComponent implements OnInit {
|
|||
|
||||
readonly playlist = input<VideoPlaylist>(undefined)
|
||||
|
||||
readonly toManage = input(false)
|
||||
readonly toManage = input.required({ transform: booleanAttribute })
|
||||
|
||||
readonly thumbnailOnly = input(false, { transform: booleanAttribute })
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { RestExtractor } from '@app/core'
|
||||
import {
|
||||
PlayerChannelSettings,
|
||||
PlayerChannelSettingsUpdate,
|
||||
PlayerVideoSettings,
|
||||
PlayerVideoSettingsUpdate
|
||||
} from '@peertube/peertube-models'
|
||||
import { catchError } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { VideoPasswordService } from '../shared-main/video/video-password.service'
|
||||
|
||||
@Injectable()
|
||||
export class PlayerSettingsService {
|
||||
static BASE_PLAYER_SETTINGS_URL = environment.apiUrl + '/api/v1/player-settings/'
|
||||
|
||||
private authHttp = inject(HttpClient)
|
||||
private restExtractor = inject(RestExtractor)
|
||||
|
||||
getVideoSettings (options: {
|
||||
videoId: string
|
||||
videoPassword?: string
|
||||
raw: boolean
|
||||
}) {
|
||||
const headers = VideoPasswordService.buildVideoPasswordHeader(options.videoPassword)
|
||||
|
||||
const path = PlayerSettingsService.BASE_PLAYER_SETTINGS_URL + 'videos/' + options.videoId
|
||||
|
||||
let params = new HttpParams()
|
||||
if (options.raw) params = params.set('raw', 'true')
|
||||
|
||||
return this.authHttp.get<PlayerVideoSettings>(path, { params, headers })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
updateVideoSettings (options: {
|
||||
videoId: string
|
||||
settings: PlayerVideoSettingsUpdate
|
||||
}) {
|
||||
const path = PlayerSettingsService.BASE_PLAYER_SETTINGS_URL + 'videos/' + options.videoId
|
||||
|
||||
return this.authHttp.put<PlayerVideoSettings>(path, options.settings)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getChannelSettings (options: {
|
||||
channelHandle: string
|
||||
raw: boolean
|
||||
}) {
|
||||
const path = PlayerSettingsService.BASE_PLAYER_SETTINGS_URL + 'video-channels/' + options.channelHandle
|
||||
|
||||
let params = new HttpParams()
|
||||
if (options.raw) params = params.set('raw', 'true')
|
||||
|
||||
return this.authHttp.get<PlayerChannelSettings>(path, { params })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
updateChannelSettings (options: {
|
||||
channelHandle: string
|
||||
settings: PlayerChannelSettingsUpdate
|
||||
}) {
|
||||
const path = PlayerSettingsService.BASE_PLAYER_SETTINGS_URL + 'video-channels/' + options.channelHandle
|
||||
|
||||
return this.authHttp.put<PlayerChannelSettings>(path, options.settings)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
}
|
|
@ -1,144 +1,98 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { AfterViewInit, Component, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { AfterViewInit, Component, inject } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { AuthService, HooksService, Notifier } from '@app/core'
|
||||
import {
|
||||
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_SUPPORT_VALIDATOR
|
||||
} from '@app/shared/form-validators/video-channel-validators'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
|
||||
import { VideoChannelService } from '@app/shared/shared-main/channel/video-channel.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { HttpStatusCode, VideoChannelCreate } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, PlayerChannelSettings, VideoChannelCreate } from '@peertube/peertube-models'
|
||||
import { of } from 'rxjs'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
import { ActorAvatarEditComponent } from '../shared-actor-image-edit/actor-avatar-edit.component'
|
||||
import { ActorBannerEditComponent } from '../shared-actor-image-edit/actor-banner-edit.component'
|
||||
import { MarkdownTextareaComponent } from '../shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
import { HelpComponent } from '../shared-main/buttons/help.component'
|
||||
import { MarkdownHintComponent } from '../shared-main/text/markdown-hint.component'
|
||||
import { VideoChannelEdit } from './video-channel-edit'
|
||||
import { PlayerSettingsService } from '../shared-video/player-settings.service'
|
||||
import { FormValidatedOutput, VideoChannelEditComponent } from './video-channel-edit.component'
|
||||
|
||||
@Component({
|
||||
templateUrl: './video-channel-edit.component.html',
|
||||
styleUrls: [ './video-channel-edit.component.scss' ],
|
||||
template: `
|
||||
<my-video-channel-edit
|
||||
mode="create" [channel]="channel" [rawPlayerSettings]="rawPlayerSettings" [error]="error"
|
||||
(formValidated)="onFormValidated($event)"
|
||||
>
|
||||
</my-video-channel-edit>
|
||||
`,
|
||||
imports: [
|
||||
NgIf,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ActorBannerEditComponent,
|
||||
ActorAvatarEditComponent,
|
||||
NgClass,
|
||||
HelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
AlertComponent,
|
||||
MarkdownHintComponent
|
||||
VideoChannelEditComponent
|
||||
],
|
||||
providers: [
|
||||
PlayerSettingsService
|
||||
]
|
||||
})
|
||||
export class VideoChannelCreateComponent extends VideoChannelEdit implements OnInit, AfterViewInit {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
export class VideoChannelCreateComponent implements AfterViewInit {
|
||||
private authService = inject(AuthService)
|
||||
private notifier = inject(Notifier)
|
||||
private router = inject(Router)
|
||||
private videoChannelService = inject(VideoChannelService)
|
||||
private hooks = inject(HooksService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
|
||||
error: string
|
||||
videoChannel = new VideoChannel({})
|
||||
|
||||
private avatar: FormData
|
||||
private banner: FormData
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
'name': VIDEO_CHANNEL_NAME_VALIDATOR,
|
||||
'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
'description': VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
'support': VIDEO_CHANNEL_SUPPORT_VALIDATOR
|
||||
})
|
||||
channel = new VideoChannel({})
|
||||
rawPlayerSettings: PlayerChannelSettings = {
|
||||
theme: 'instance-default'
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.hooks.runAction('action:video-channel-create.init', 'video-channel')
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
onFormValidated (output: FormValidatedOutput) {
|
||||
this.error = undefined
|
||||
|
||||
const body = this.form.value
|
||||
const videoChannelCreate: VideoChannelCreate = {
|
||||
name: body.name,
|
||||
displayName: body['display-name'],
|
||||
description: body.description || null,
|
||||
support: body.support || null
|
||||
const channelCreate: VideoChannelCreate = {
|
||||
name: output.channel.name,
|
||||
displayName: output.channel.displayName,
|
||||
description: output.channel.description,
|
||||
support: output.channel.support
|
||||
}
|
||||
|
||||
this.videoChannelService.createVideoChannel(videoChannelCreate)
|
||||
this.videoChannelService.createVideoChannel(channelCreate)
|
||||
.pipe(
|
||||
switchMap(() => this.uploadAvatar()),
|
||||
switchMap(() => this.uploadBanner())
|
||||
switchMap(() => {
|
||||
return this.playerSettingsService.updateChannelSettings({
|
||||
channelHandle: output.channel.name,
|
||||
settings: {
|
||||
theme: output.playerSettings.theme
|
||||
}
|
||||
})
|
||||
}),
|
||||
switchMap(() => this.uploadAvatar(output.channel.name, output.avatar)),
|
||||
switchMap(() => this.uploadBanner(output.channel.name, output.banner))
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.authService.refreshUserInformation()
|
||||
|
||||
this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`)
|
||||
this.notifier.success($localize`Video channel ${channelCreate.displayName} created.`)
|
||||
this.router.navigate([ '/my-library', 'video-channels' ])
|
||||
},
|
||||
|
||||
error: err => {
|
||||
let message = err.message
|
||||
|
||||
if (err.status === HttpStatusCode.CONFLICT_409) {
|
||||
this.error = $localize`This name already exists on this platform.`
|
||||
return
|
||||
message = $localize`Channel name "${channelCreate.name}" already exists on this platform.`
|
||||
}
|
||||
|
||||
this.error = err.message
|
||||
this.notifier.error(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.avatar = formData
|
||||
private uploadAvatar (username: string, avatar?: FormData) {
|
||||
if (!avatar) return of(undefined)
|
||||
|
||||
return this.videoChannelService.changeVideoChannelImage(username, avatar, 'avatar')
|
||||
}
|
||||
|
||||
onAvatarDelete () {
|
||||
this.avatar = null
|
||||
}
|
||||
private uploadBanner (username: string, banner?: FormData) {
|
||||
if (!banner) return of(undefined)
|
||||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.banner = formData
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
this.banner = null
|
||||
}
|
||||
|
||||
isCreation () {
|
||||
return true
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
return $localize`Create your channel`
|
||||
}
|
||||
|
||||
getUsername () {
|
||||
return this.form.value.name
|
||||
}
|
||||
|
||||
private uploadAvatar () {
|
||||
if (!this.avatar) return of(undefined)
|
||||
|
||||
return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.avatar, 'avatar')
|
||||
}
|
||||
|
||||
private uploadBanner () {
|
||||
if (!this.banner) return of(undefined)
|
||||
|
||||
return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.banner, 'banner')
|
||||
return this.videoChannelService.changeVideoChannelImage(username, banner, 'banner')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<my-alert *ngIf="error" type="danger">{{ error }}</my-alert>
|
||||
<my-alert *ngIf="error()" type="danger">{{ error() }}</my-alert>
|
||||
|
||||
<div class="pt-4">
|
||||
<form (ngSubmit)="formValidated()" [formGroup]="form">
|
||||
<form (ngSubmit)="onFormValidated()" [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols"> <!-- channel grid -->
|
||||
<div class="title-col">
|
||||
@if (isCreation()) {
|
||||
@if (mode() === 'create') {
|
||||
<h2 i18n>NEW CHANNEL</h2>
|
||||
} @else {
|
||||
<h2 i18n>UPDATE CHANNEL</h2>
|
||||
|
@ -14,40 +14,40 @@
|
|||
|
||||
<div class="content-col">
|
||||
<my-actor-banner-edit
|
||||
*ngIf="videoChannel" [previewImage]="isCreation()" class="d-block mb-4"
|
||||
[bannerUrl]="videoChannel?.bannerUrl" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
|
||||
*ngIf="channel()" previewImage="true" class="d-block mb-4"
|
||||
[bannerUrl]="channel()?.bannerUrl" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
|
||||
></my-actor-banner-edit>
|
||||
|
||||
<my-actor-avatar-edit
|
||||
*ngIf="videoChannel" class="d-block mb-4" actorType="channel"
|
||||
[displayName]="videoChannel.displayName" [previewImage]="isCreation()" [avatars]="videoChannel.avatars"
|
||||
[username]="!isCreation() && videoChannel.name" [subscribers]="!isCreation() && videoChannel.followersCount"
|
||||
*ngIf="channel()" class="d-block mb-4" actorType="channel"
|
||||
[displayName]="channel().displayName" previewImage="true" [avatars]="channel().avatars"
|
||||
[username]="mode() === 'update' && channel().name" [subscribers]="mode() === 'update' && channel().followersCount"
|
||||
(avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||
></my-actor-avatar-edit>
|
||||
|
||||
<div class="form-group" *ngIf="isCreation()">
|
||||
<div class="form-group" *ngIf="mode() === 'create'">
|
||||
<label i18n for="name">Name</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
|
||||
formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control w-auto flex-grow-1 d-block"
|
||||
formControlName="name" [ngClass]="{ 'input-error': formErrors.name }" class="form-control w-auto flex-grow-1 d-block"
|
||||
>
|
||||
<div class="input-group-text">@{{ instanceHost }}</div>
|
||||
</div>
|
||||
<div *ngIf="formErrors['name']" class="form-error" role="alert">
|
||||
{{ formErrors['name'] }}
|
||||
<div *ngIf="formErrors.name" class="form-error" role="alert">
|
||||
{{ formErrors.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="display-name">Display name</label>
|
||||
<label i18n for="displayName">Display name</label>
|
||||
<input
|
||||
type="text" id="display-name" class="form-control"
|
||||
formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
|
||||
type="text" id="displayName" class="form-control"
|
||||
formControlName="displayName" [ngClass]="{ 'input-error': formErrors.displayName }"
|
||||
>
|
||||
<div *ngIf="formErrors['display-name']" class="form-error" role="alert">
|
||||
{{ formErrors['display-name'] }}
|
||||
<div *ngIf="formErrors.displayName" class="form-error" role="alert">
|
||||
{{ formErrors.displayName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -58,7 +58,7 @@
|
|||
|
||||
<my-markdown-textarea
|
||||
inputId="description" formControlName="description"
|
||||
markdownType="enhanced" [formError]="formErrors['description']" withEmoji="true" withHtml="true"
|
||||
markdownType="enhanced" [formError]="formErrors.description" withEmoji="true" withHtml="true"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors.description" class="form-error" role="alert">
|
||||
|
@ -75,7 +75,7 @@
|
|||
|
||||
<my-markdown-textarea
|
||||
inputId="support" formControlName="support"
|
||||
markdownType="enhanced" [formError]="formErrors['support']" withEmoji="true" withHtml="true"
|
||||
markdownType="enhanced" [formError]="formErrors.support" withEmoji="true" withHtml="true"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
|
@ -86,6 +86,13 @@
|
|||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="playerTheme">Player Theme</label>
|
||||
|
||||
<my-select-player-theme formControlName="playerTheme" inputId="playerTheme" mode="channel">
|
||||
</my-select-player-theme>
|
||||
</div>
|
||||
|
||||
<input type="submit" class="peertube-button primary-button mt-4" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
my-actor-banner-edit {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
input[type="text"] {
|
||||
@include peertube-input-text(340px);
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
input[type="submit"] {
|
||||
@include margin-left(auto);
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,8 @@ input[type=submit] {
|
|||
max-width: 500px;
|
||||
}
|
||||
|
||||
.peertube-select-container {
|
||||
@include peertube-select-container(340px);
|
||||
my-select-player-theme {
|
||||
display: block;
|
||||
|
||||
@include responsive-width(340px);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, input, OnInit, output } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
|
||||
import { PlayerChannelSettings } from '@peertube/peertube-models'
|
||||
import { BuildFormArgumentTyped, FormReactiveErrorsTyped, FormReactiveMessagesTyped } from '../form-validators/form-validator.model'
|
||||
import {
|
||||
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_SUPPORT_VALIDATOR
|
||||
} from '../form-validators/video-channel-validators'
|
||||
import { ActorAvatarEditComponent } from '../shared-actor-image-edit/actor-avatar-edit.component'
|
||||
import { ActorBannerEditComponent } from '../shared-actor-image-edit/actor-banner-edit.component'
|
||||
import { FormReactiveService } from '../shared-forms/form-reactive.service'
|
||||
import { MarkdownTextareaComponent } from '../shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
import { SelectPlayerThemeComponent } from '../shared-forms/select/select-player-theme.component'
|
||||
import { HelpComponent } from '../shared-main/buttons/help.component'
|
||||
import { AlertComponent } from '../shared-main/common/alert.component'
|
||||
import { MarkdownHintComponent } from '../shared-main/text/markdown-hint.component'
|
||||
|
||||
type Form = {
|
||||
name: FormControl<string>
|
||||
displayName: FormControl<string>
|
||||
description: FormControl<string>
|
||||
support: FormControl<string>
|
||||
playerTheme: FormControl<PlayerChannelSettings['theme']>
|
||||
bulkVideosSupportUpdate: FormControl<boolean>
|
||||
}
|
||||
|
||||
export type FormValidatedOutput = {
|
||||
avatar: FormData
|
||||
banner: FormData
|
||||
|
||||
playerSettings: {
|
||||
theme: PlayerChannelSettings['theme']
|
||||
}
|
||||
|
||||
channel: {
|
||||
name: string
|
||||
displayName: string
|
||||
description: string
|
||||
support: string
|
||||
bulkVideosSupportUpdate: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-edit',
|
||||
templateUrl: './video-channel-edit.component.html',
|
||||
styleUrls: [ './video-channel-edit.component.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ActorBannerEditComponent,
|
||||
ActorAvatarEditComponent,
|
||||
HelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
AlertComponent,
|
||||
MarkdownHintComponent,
|
||||
SelectPlayerThemeComponent
|
||||
]
|
||||
})
|
||||
export class VideoChannelEditComponent implements OnInit {
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
|
||||
readonly mode = input.required<'create' | 'update'>()
|
||||
readonly channel = input.required<VideoChannel>()
|
||||
readonly rawPlayerSettings = input.required<PlayerChannelSettings>()
|
||||
readonly error = input<string>()
|
||||
|
||||
readonly formValidated = output<FormValidatedOutput>()
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
private avatar: FormData
|
||||
private banner: FormData
|
||||
private oldSupportField: string
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm()
|
||||
|
||||
this.oldSupportField = this.channel().support
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
name: this.mode() === 'create'
|
||||
? VIDEO_CHANNEL_NAME_VALIDATOR
|
||||
: null,
|
||||
displayName: VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
description: VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
support: VIDEO_CHANNEL_SUPPORT_VALIDATOR,
|
||||
bulkVideosSupportUpdate: null,
|
||||
playerTheme: null
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
displayName: this.channel().displayName,
|
||||
description: this.channel().description,
|
||||
support: this.channel().support,
|
||||
playerTheme: this.rawPlayerSettings().theme
|
||||
}
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
if (this.mode() === 'update') {
|
||||
return $localize`Update ${this.channel().name}`
|
||||
}
|
||||
|
||||
return $localize`Create your channel`
|
||||
}
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.avatar = formData
|
||||
}
|
||||
|
||||
onAvatarDelete () {
|
||||
this.avatar = null
|
||||
}
|
||||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.banner = formData
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
this.banner = null
|
||||
}
|
||||
|
||||
get instanceHost () {
|
||||
return window.location.host
|
||||
}
|
||||
|
||||
isBulkUpdateVideosDisplayed () {
|
||||
if (this.mode() === 'create') return false
|
||||
|
||||
if (this.oldSupportField === undefined) return false
|
||||
|
||||
return this.oldSupportField !== this.form.value.support
|
||||
}
|
||||
|
||||
onFormValidated () {
|
||||
const body = this.form.value
|
||||
|
||||
this.formValidated.emit({
|
||||
avatar: this.avatar,
|
||||
banner: this.banner,
|
||||
playerSettings: {
|
||||
theme: body.playerTheme
|
||||
},
|
||||
channel: {
|
||||
name: body.name,
|
||||
displayName: body.displayName,
|
||||
description: body.description || null,
|
||||
support: body.support || null,
|
||||
|
||||
bulkVideosSupportUpdate: this.mode() === 'update'
|
||||
? body.bulkVideosSupportUpdate || false
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
|
||||
|
||||
export abstract class VideoChannelEdit extends FormReactive {
|
||||
videoChannel: VideoChannel
|
||||
|
||||
abstract isCreation (): boolean
|
||||
abstract getFormButtonTitle (): string
|
||||
|
||||
get instanceHost () {
|
||||
return window.location.host
|
||||
}
|
||||
|
||||
// Should be implemented by the child
|
||||
isBulkUpdateVideosDisplayed () {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,92 +1,65 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { AfterViewInit, Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { AfterViewInit, Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AuthService, HooksService, Notifier, RedirectService } from '@app/core'
|
||||
import { genericUploadErrorHandler } from '@app/helpers'
|
||||
import {
|
||||
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
VIDEO_CHANNEL_SUPPORT_VALIDATOR
|
||||
} from '@app/shared/form-validators/video-channel-validators'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { VideoChannelService } from '@app/shared/shared-main/channel/video-channel.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { shallowCopy } from '@peertube/peertube-core-utils'
|
||||
import { VideoChannelUpdate } from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ActorAvatarEditComponent } from '../shared-actor-image-edit/actor-avatar-edit.component'
|
||||
import { ActorBannerEditComponent } from '../shared-actor-image-edit/actor-banner-edit.component'
|
||||
import { MarkdownTextareaComponent } from '../shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
import { HelpComponent } from '../shared-main/buttons/help.component'
|
||||
import { MarkdownHintComponent } from '../shared-main/text/markdown-hint.component'
|
||||
import { VideoChannelEdit } from './video-channel-edit'
|
||||
import { PlayerChannelSettings, VideoChannelUpdate } from '@peertube/peertube-models'
|
||||
import { catchError, forkJoin, Subscription, switchMap, tap, throwError } from 'rxjs'
|
||||
import { VideoChannel } from '../shared-main/channel/video-channel.model'
|
||||
import { PlayerSettingsService } from '../shared-video/player-settings.service'
|
||||
import { FormValidatedOutput, VideoChannelEditComponent } from './video-channel-edit.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-update',
|
||||
templateUrl: './video-channel-edit.component.html',
|
||||
styleUrls: [ './video-channel-edit.component.scss' ],
|
||||
template: `
|
||||
@if (channel && rawPlayerSettings) {
|
||||
<my-video-channel-edit
|
||||
mode="update" [channel]="channel" [rawPlayerSettings]="rawPlayerSettings" [error]="error"
|
||||
(formValidated)="onFormValidated($event)"
|
||||
>
|
||||
</my-video-channel-edit>
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
NgIf,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ActorBannerEditComponent,
|
||||
ActorAvatarEditComponent,
|
||||
NgClass,
|
||||
HelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
AlertComponent,
|
||||
MarkdownHintComponent
|
||||
VideoChannelEditComponent
|
||||
],
|
||||
providers: [
|
||||
PlayerSettingsService
|
||||
]
|
||||
})
|
||||
export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnInit, AfterViewInit, OnDestroy {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
export class VideoChannelUpdateComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private authService = inject(AuthService)
|
||||
private notifier = inject(Notifier)
|
||||
private route = inject(ActivatedRoute)
|
||||
private videoChannelService = inject(VideoChannelService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
private redirectService = inject(RedirectService)
|
||||
private hooks = inject(HooksService)
|
||||
|
||||
channel: VideoChannel
|
||||
rawPlayerSettings: PlayerChannelSettings
|
||||
error: string
|
||||
|
||||
private paramsSub: Subscription
|
||||
private oldSupportField: string
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
'display-name': VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
'description': VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
'support': VIDEO_CHANNEL_SUPPORT_VALIDATOR,
|
||||
'bulkVideosSupportUpdate': null
|
||||
})
|
||||
|
||||
this.paramsSub = this.route.params.subscribe(routeParams => {
|
||||
const videoChannelName = routeParams['videoChannelName']
|
||||
|
||||
this.videoChannelService.getVideoChannel(videoChannelName)
|
||||
.subscribe({
|
||||
next: videoChannelToUpdate => {
|
||||
this.videoChannel = videoChannelToUpdate
|
||||
forkJoin([
|
||||
this.videoChannelService.getVideoChannel(videoChannelName),
|
||||
this.playerSettingsService.getChannelSettings({ channelHandle: videoChannelName, raw: true })
|
||||
]).subscribe({
|
||||
next: ([ channel, rawPlayerSettings ]) => {
|
||||
this.channel = channel
|
||||
this.rawPlayerSettings = rawPlayerSettings
|
||||
|
||||
this.hooks.runAction('action:video-channel-update.video-channel.loaded', 'video-channel', { videoChannel: this.videoChannel })
|
||||
this.hooks.runAction('action:video-channel-update.video-channel.loaded', 'video-channel', { videoChannel: this.channel })
|
||||
},
|
||||
|
||||
this.oldSupportField = videoChannelToUpdate.support
|
||||
|
||||
this.form.patchValue({
|
||||
'display-name': videoChannelToUpdate.displayName,
|
||||
'description': videoChannelToUpdate.description,
|
||||
'support': videoChannelToUpdate.support
|
||||
})
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.error = err.message
|
||||
}
|
||||
})
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -98,112 +71,84 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI
|
|||
if (this.paramsSub) this.paramsSub.unsubscribe()
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
onFormValidated (output: FormValidatedOutput) {
|
||||
this.error = undefined
|
||||
|
||||
const body = this.form.value
|
||||
const videoChannelUpdate: VideoChannelUpdate = {
|
||||
displayName: body['display-name'],
|
||||
description: body.description || null,
|
||||
support: body.support || null,
|
||||
bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false
|
||||
displayName: output.channel.displayName,
|
||||
description: output.channel.description,
|
||||
support: output.channel.support,
|
||||
bulkVideosSupportUpdate: output.channel.bulkVideosSupportUpdate
|
||||
}
|
||||
|
||||
this.videoChannelService.updateVideoChannel(this.videoChannel.name, videoChannelUpdate)
|
||||
this.videoChannelService.updateVideoChannel(this.channel.name, videoChannelUpdate)
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
return this.playerSettingsService.updateChannelSettings({
|
||||
channelHandle: this.channel.name,
|
||||
settings: {
|
||||
theme: output.playerSettings.theme
|
||||
}
|
||||
})
|
||||
}),
|
||||
switchMap(() => this.updateOrDeleteAvatar(output.avatar)),
|
||||
switchMap(() => this.updateOrDeleteBanner(output.banner))
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
// So my-actor-avatar component detects changes
|
||||
this.channel = shallowCopy(this.channel)
|
||||
|
||||
this.authService.refreshUserInformation()
|
||||
|
||||
this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`)
|
||||
|
||||
this.redirectService.redirectToPreviousRoute('/c/' + this.videoChannel.name)
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.error = err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'avatar')
|
||||
.subscribe({
|
||||
next: data => {
|
||||
this.notifier.success($localize`Avatar changed.`)
|
||||
|
||||
this.videoChannel.updateAvatar(data.avatars)
|
||||
|
||||
// So my-actor-avatar component detects changes
|
||||
this.videoChannel = shallowCopy(this.videoChannel)
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) =>
|
||||
genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`avatar`,
|
||||
notifier: this.notifier
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onAvatarDelete () {
|
||||
this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'avatar')
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Avatar deleted.`)
|
||||
|
||||
this.videoChannel.resetAvatar()
|
||||
|
||||
// So my-actor-avatar component detects changes
|
||||
this.videoChannel = shallowCopy(this.videoChannel)
|
||||
this.redirectService.redirectToPreviousRoute('/c/' + this.channel.name)
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'banner')
|
||||
.subscribe({
|
||||
next: data => {
|
||||
this.notifier.success($localize`Banner changed.`)
|
||||
private updateOrDeleteAvatar (avatar: FormData) {
|
||||
if (!avatar) {
|
||||
return this.videoChannelService.deleteVideoChannelImage(this.channel.name, 'avatar')
|
||||
.pipe(tap(() => this.channel.resetAvatar()))
|
||||
}
|
||||
|
||||
this.videoChannel.updateBanner(data.banners)
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) =>
|
||||
genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`banner`,
|
||||
notifier: this.notifier
|
||||
return this.videoChannelService.changeVideoChannelImage(this.channel.name, avatar, 'avatar')
|
||||
.pipe(
|
||||
tap(data => this.channel.updateAvatar(data.avatars)),
|
||||
catchError(err =>
|
||||
throwError(() => {
|
||||
return new Error(genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`avatar`,
|
||||
notifier: this.notifier
|
||||
}))
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'banner')
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Banner deleted.`)
|
||||
private updateOrDeleteBanner (banner: FormData) {
|
||||
if (!banner) {
|
||||
return this.videoChannelService.deleteVideoChannelImage(this.channel.name, 'banner')
|
||||
.pipe(tap(() => this.channel.resetBanner()))
|
||||
}
|
||||
|
||||
this.videoChannel.resetBanner()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
isCreation () {
|
||||
return false
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
return $localize`Update ${this.videoChannel?.name}`
|
||||
}
|
||||
|
||||
isBulkUpdateVideosDisplayed () {
|
||||
if (this.oldSupportField === undefined) return false
|
||||
|
||||
return this.oldSupportField !== this.form.value['support']
|
||||
return this.videoChannelService.changeVideoChannelImage(this.channel.name, banner, 'banner')
|
||||
.pipe(
|
||||
tap(data => this.channel.updateBanner(data.banners)),
|
||||
catchError(err =>
|
||||
throwError(() => {
|
||||
return new Error(genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`banner`,
|
||||
notifier: this.notifier
|
||||
}))
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
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