mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-12-05 01:10:36 +00:00
Compare commits
337 Commits
a6bb5917dd
...
4376c728b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4376c728b1 | ||
|
|
b548618c27 | ||
|
|
eb23d76a85 | ||
|
|
f2f814f9d7 | ||
|
|
6c47548d51 | ||
|
|
614521a39e | ||
|
|
7cdfc592be | ||
|
|
2aa62a0383 | ||
|
|
b822a2ac49 | ||
|
|
de58fe9499 | ||
|
|
0fd61b1698 | ||
|
|
b810c4a36b | ||
|
|
f8f480950e | ||
|
|
48f3128611 | ||
|
|
bcfed69ed9 | ||
|
|
c823de2f98 | ||
|
|
8aca768257 | ||
|
|
89418dd637 | ||
|
|
dffe018a39 | ||
|
|
516546bea6 | ||
|
|
ca19fe0aba | ||
|
|
a27d3a81d9 | ||
|
|
2858af6918 | ||
|
|
a463adb0a2 | ||
|
|
2d06d07ee1 | ||
|
|
33713e8b0f | ||
|
|
426e73ce8b | ||
|
|
c05117ef4d | ||
|
|
a571428735 | ||
|
|
09fe2af39f | ||
|
|
39c30f9e17 | ||
|
|
abc24fad2e | ||
|
|
ed15e3ca4d | ||
|
|
7ed8ab75b5 | ||
|
|
0052a4d095 | ||
|
|
c0c6b9236c | ||
|
|
4ead7306ec | ||
|
|
ec28327632 | ||
|
|
03a2f5668c | ||
|
|
cb9047c170 | ||
|
|
de8cee18d2 | ||
|
|
0c682a915b | ||
|
|
31e6b39fe0 | ||
|
|
f57110a956 | ||
|
|
9dece339a3 | ||
|
|
2bba9bba87 | ||
|
|
095e49592b | ||
|
|
8a9ed579ab | ||
|
|
0227fc19c2 | ||
|
|
9a5d2ebf48 | ||
|
|
e7a33866c5 | ||
|
|
766e50bf10 | ||
|
|
5d3717766b | ||
|
|
6fa40c0bf0 | ||
|
|
1688b42476 | ||
|
|
b342b0ae11 | ||
|
|
9ca3d8e58b | ||
|
|
ae81387001 | ||
|
|
25f6245680 | ||
|
|
9020e276f6 | ||
|
|
5541054f32 | ||
|
|
7013f04efc | ||
|
|
7eda7b5f65 | ||
|
|
fca1ae4e0d | ||
|
|
eb2e14fccd | ||
|
|
f2adff7e12 | ||
|
|
1d5a8bf589 | ||
|
|
7915680ab7 | ||
|
|
309fce5a18 | ||
|
|
bd3b81d109 | ||
|
|
fc27f6e5c3 | ||
|
|
3bd0991011 | ||
|
|
af40fd1707 | ||
|
|
875d5cd964 | ||
|
|
976ce40842 | ||
|
|
7965e0e16d | ||
|
|
656ca62796 | ||
|
|
80c7dc348e | ||
|
|
5e78d3bf6c | ||
|
|
8f135fec5c | ||
|
|
938f3f9e51 | ||
|
|
810d6c6612 | ||
|
|
e82ba16b29 | ||
|
|
3544b08f59 | ||
|
|
f610c910c0 | ||
|
|
8801004ba9 | ||
|
|
0e97d07629 | ||
|
|
afd913530c | ||
|
|
190987cc72 | ||
|
|
0babdbc243 | ||
|
|
1c94e97923 | ||
|
|
381f8e3b3a | ||
|
|
4fd4c91ec1 | ||
|
|
ed8279eae6 | ||
|
|
f04a11f74d | ||
|
|
1ba09ea2fa | ||
|
|
15e9236373 | ||
|
|
6a51b685ef | ||
|
|
319da3b6a5 | ||
|
|
ba3bb0f142 | ||
|
|
5286f0ed46 | ||
|
|
2f816446a6 | ||
|
|
d867eb367b | ||
|
|
a25c6d65b8 | ||
|
|
77f28ece70 | ||
|
|
f6eefbde0c | ||
|
|
f61581280d | ||
|
|
3a9a76b3b4 | ||
|
|
54bfae82ad | ||
|
|
28a0c774f1 | ||
|
|
d50c8a0b0a | ||
|
|
b2704b2f36 | ||
|
|
e7694b2911 | ||
|
|
16a7a5e8cc | ||
|
|
df86535f46 | ||
|
|
3f079b9398 | ||
|
|
4263d755e3 | ||
|
|
30ca195efa | ||
|
|
68547190c7 | ||
|
|
94e55dfc6c | ||
|
|
bb0c71549a | ||
|
|
2a648e6ea5 | ||
|
|
3a58afef10 | ||
|
|
84dbcb5f11 | ||
|
|
cd8ad77515 | ||
|
|
050461528c | ||
|
|
073cd4f0ba | ||
|
|
42e0c015e8 | ||
|
|
4888717c09 | ||
|
|
449ebe4b54 | ||
|
|
3cca1fdbf3 | ||
|
|
50c184a9a2 | ||
|
|
e2e15e3f0c | ||
|
|
0048bf7326 | ||
|
|
4fd894308d | ||
|
|
496b50f6b1 | ||
|
|
729a58a860 | ||
|
|
2d96b7191d | ||
|
|
dca5e363d1 | ||
|
|
e670b6d924 | ||
|
|
5c1cbcfcb1 | ||
|
|
a11d3102ce | ||
|
|
f184154aaa | ||
|
|
f93870ea31 | ||
|
|
941469df29 | ||
|
|
ef95c3fe72 | ||
|
|
572100b1a3 | ||
|
|
200193262c | ||
|
|
74e97347bb | ||
|
|
b742dbc0fc | ||
|
|
fcca9b72d3 | ||
|
|
5edab3f795 | ||
|
|
a2b99c3c92 | ||
|
|
fde2c8c0c7 | ||
|
|
48ea20c9e4 | ||
|
|
906b5f7f2c | ||
|
|
bbc1afada5 | ||
|
|
efa32646ed | ||
|
|
f1e05043bc | ||
|
|
eedfb8b0a2 | ||
|
|
ef28ba3038 | ||
|
|
dd52e8b89e | ||
|
|
e74bf8ae2a | ||
|
|
9b7edd1c59 | ||
|
|
93926d2700 | ||
|
|
41b3a404dc | ||
|
|
c922886f8a | ||
|
|
2bd5f564a3 | ||
|
|
f4c969fd00 | ||
|
|
c3daefa6e9 | ||
|
|
4c5813463f | ||
|
|
6f318071db | ||
|
|
b3da438a00 | ||
|
|
945604ea79 | ||
|
|
f914e1b7bf | ||
|
|
4cb99c3fd5 | ||
|
|
715719238a | ||
|
|
7b38c21631 | ||
|
|
958aab240d | ||
|
|
4719cf26f4 | ||
|
|
a6266dc4bf | ||
|
|
12c9825658 | ||
|
|
448bc823ef | ||
|
|
78eb54464c | ||
|
|
75fbdbf70e | ||
|
|
326bf8d85f | ||
|
|
38e04969df | ||
|
|
a1bf55c7ae | ||
|
|
707e1e9b98 | ||
|
|
7f9f1feed5 | ||
|
|
52a94815c0 | ||
|
|
6594ac1262 | ||
|
|
6e34fadcc2 | ||
|
|
a16955136d | ||
|
|
d50c038e07 | ||
|
|
b59bded648 | ||
|
|
3d825c56bf | ||
|
|
756b5646f6 | ||
|
|
640be407cf | ||
|
|
d7948ad0bc | ||
|
|
b02cbfce7f | ||
|
|
59d5f28ed6 | ||
|
|
24a0d7fd00 | ||
|
|
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 | ||
|
|
e763ad3036 | ||
|
|
b44dfef3f9 | ||
|
|
9619c2ea7d | ||
|
|
83f74169da | ||
|
|
fc986076c9 |
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -79,7 +79,7 @@ First, you should use a server or PC with at least 4GB of RAM. Less RAM may lead
|
||||
git clone https://github.com/Chocobozzz/PeerTube
|
||||
cd PeerTube
|
||||
git remote add me git@github.com:YOUR_GITHUB_USERNAME/PeerTube.git
|
||||
yarn install --pure-lockfile
|
||||
npm run install-node-dependencies
|
||||
```
|
||||
|
||||
Note that development is done on the `develop` branch. If you want to hack on
|
||||
|
||||
@@ -11,32 +11,20 @@ runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-node-
|
||||
${{ runner.OS }}-
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Install peertube runner dependencies
|
||||
shell: bash
|
||||
run: cd apps/peertube-runner && yarn install --frozen-lockfile
|
||||
|
||||
- name: Install peertube CLI dependencies
|
||||
shell: bash
|
||||
run: cd apps/peertube-cli && yarn install --frozen-lockfile
|
||||
run: npm run install-node-dependencies
|
||||
|
||||
- name: Display PeerTube dependencies
|
||||
shell: bash
|
||||
|
||||
@@ -8,6 +8,7 @@ runs:
|
||||
- name: Setup system dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install postgresql-client-common redis-tools parallel libimage-exiftool-perl
|
||||
wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
|
||||
tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
|
||||
|
||||
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.
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
# FIXME: https://github.com/actions/checkout/issues/290
|
||||
git fetch --force --tags
|
||||
|
||||
one="{ \"build-peertube\": true, \"file\": \"./support/docker/production/Dockerfile.bookworm\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-bookworm\" }"
|
||||
one="{ \"build-peertube\": true, \"file\": \"./support/docker/production/Dockerfile\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-trixie,chocobozzz/peertube:develop\" }"
|
||||
two="{ \"build-peertube\": true, \"file\": \"./support/docker/production/Dockerfile.bookworm\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube:production-bookworm,chocobozzz/peertube:$(git describe --abbrev=0)-bookworm\" }"
|
||||
three="{ \"build-peertube\": false, \"file\": \"./support/docker/production/Dockerfile.nginx\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube-webserver:latest\" }"
|
||||
|
||||
|
||||
2
.github/workflows/stats.yml
vendored
2
.github/workflows/stats.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
run: |
|
||||
wget "https://github.com/boyter/scc/releases/download/v3.0.0/scc-3.0.0-x86_64-unknown-linux.zip"
|
||||
unzip "scc-3.0.0-x86_64-unknown-linux.zip"
|
||||
./scc --format=json --exclude-dir .git,node_modules,client/node_modules,client/dist,dist,yarn.lock,client/yarn.lock,client/src/locale,test1,test2,test3,client/src/assets/images,config,storage,packages/tests/fixtures,support/openapi,.idea,.vscode,docker-volume,ffmpeg-3,ffmpeg-4 > ./scc.json
|
||||
./scc --format=json --exclude-dir .git,node_modules,client/node_modules,client/dist,dist,pnpm-lock.yaml,client/src/locale,test1,test2,test3,client/src/assets/images,config,storage,packages/tests/fixtures,support/openapi,.idea,.vscode,docker-volume,ffmpeg-3,ffmpeg-4 > ./scc.json
|
||||
|
||||
- name: PeerTube client stats
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
17
.github/workflows/test.yml
vendored
17
.github/workflows/test.yml
vendored
@@ -38,6 +38,14 @@ jobs:
|
||||
ports:
|
||||
- 9444:9000
|
||||
|
||||
keycloak:
|
||||
image: chocobozzz/peertube-tests-keycloak
|
||||
ports:
|
||||
- 8082:8080
|
||||
env:
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -90,6 +98,15 @@ jobs:
|
||||
~/.cache/huggingface
|
||||
key: ${{ runner.OS }}-${{ matrix.test_suite }}-hugging-face-v1
|
||||
|
||||
# Transcription tools take a lot of disk space, we need to cleanup before running transcription tests
|
||||
- name: Cleanup disk space
|
||||
if: matrix.test_suite == 'transcription' || matrix.test_suite == 'external-plugins'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
|
||||
sudo docker image prune --all --force
|
||||
sudo docker builder prune -a
|
||||
df -h
|
||||
|
||||
- name: Set env test variable (schedule)
|
||||
if: github.event_name != 'schedule'
|
||||
run: |
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,9 +1,7 @@
|
||||
# NPM instalation
|
||||
node_modules
|
||||
*npm-debug.log
|
||||
yarn-error.log
|
||||
*-ci.log
|
||||
.yarn
|
||||
|
||||
# Testing
|
||||
/test1/
|
||||
|
||||
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,5 +1,75 @@
|
||||
# Changelog
|
||||
|
||||
## v7.3.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* Minimum supported NodeJS version is `20.19`
|
||||
|
||||
### NGINX
|
||||
|
||||
* Disable request buffering on upload endpoints to fix HTTP request timeouts: https://github.com/Chocobozzz/PeerTube/commit/d1a35e8421195088e2754b787c4af1e765b9eaa9
|
||||
|
||||
### Plugins/Themes/Embed API
|
||||
|
||||
* **Breaking change** Plugin and themes must use `:root` CSS selector instead of `body` to inject CSS variables
|
||||
* 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
|
||||
* Fix overflow in discover page
|
||||
* Fix restoring scroll position when going back in the web browser on the homepage set by the admin
|
||||
* Fill video support on channel sync
|
||||
* Respect instance default privacy setting when publishing imports and lives
|
||||
* Remove useless help for live transcoding
|
||||
* Fix RTL margins on some components
|
||||
|
||||
|
||||
## v7.2.3
|
||||
|
||||
### SECURITY
|
||||
|
||||
@@ -846,6 +846,7 @@
|
||||
* `peertube-x` by Solen DP (CC-BY)
|
||||
* `flame` by Freepik (Flaticon License)
|
||||
* `local` by Larea (CC-BY)
|
||||
* X (Twitter) icon: [Wikimedia Commons](https://fr.m.wikipedia.org/wiki/Fichier:X_logo_2023.svg)
|
||||
|
||||
|
||||
# Contributors to our 2020 crowdfunding :heart:
|
||||
|
||||
@@ -10,8 +10,7 @@ See https://docs.joinpeertube.org/maintain/tools#remote-tools
|
||||
|
||||
```bash
|
||||
cd peertube-root
|
||||
yarn install --pure-lockfile
|
||||
cd apps/peertube-cli && yarn install --pure-lockfile
|
||||
npm run install-node-dependencies
|
||||
```
|
||||
|
||||
## Develop
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@colors/colors@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||
|
||||
application-config-path@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-1.0.0.tgz#9c25b8c00ac9a342db27275abd3f38c67bbe5a05"
|
||||
integrity sha512-6ZDlLTlfqrTybVzZJDpX2K2ZufqyMyiTbOG06GpxmkmczFgTN+YYRGcTcMCXv/F5P5SrZijVjzzpPUE9BvheLg==
|
||||
|
||||
application-config@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/application-config/-/application-config-3.0.0.tgz#9adec84dd2d81e97dd78ea0dffcbf97381a1f55c"
|
||||
integrity sha512-7ViR4soQJDx2O9iLf1vGxvekkPqvwqV/AZ2OL3DNcAQrg03UjJE1VeBk7oYNoN9AKB0eNyVrcM7kPD30NKeLLw==
|
||||
dependencies:
|
||||
application-config-path "^1.0.0"
|
||||
load-json-file "^7.0.1"
|
||||
write-json-file "^5.0.0"
|
||||
|
||||
cli-table3@^0.6.0:
|
||||
version "0.6.5"
|
||||
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f"
|
||||
integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
optionalDependencies:
|
||||
"@colors/colors" "1.5.0"
|
||||
|
||||
cross-spawn@^6.0.0:
|
||||
version "6.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57"
|
||||
integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==
|
||||
dependencies:
|
||||
nice-try "^1.0.4"
|
||||
path-key "^2.0.1"
|
||||
semver "^5.5.0"
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
detect-indent@^7.0.0:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25"
|
||||
integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||
|
||||
execa@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
|
||||
integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==
|
||||
dependencies:
|
||||
cross-spawn "^6.0.0"
|
||||
get-stream "^3.0.0"
|
||||
is-stream "^1.1.0"
|
||||
npm-run-path "^2.0.0"
|
||||
p-finally "^1.0.0"
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
get-stream@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
|
||||
integrity sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==
|
||||
|
||||
imurmurhash@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
|
||||
|
||||
is-fullwidth-code-point@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
is-plain-obj@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
|
||||
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==
|
||||
|
||||
is-typedarray@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
load-json-file@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-7.0.1.tgz#a3c9fde6beffb6bedb5acf104fad6bb1604e1b00"
|
||||
integrity sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==
|
||||
|
||||
ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
netrc-parser@^3.1.6:
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/netrc-parser/-/netrc-parser-3.1.6.tgz#7243c9ec850b8e805b9bdc7eae7b1450d4a96e72"
|
||||
integrity sha512-lY+fmkqSwntAAjfP63jB4z5p5WbuZwyMCD3pInT7dpHU/Gc6Vv90SAC6A0aNiqaRGHiuZFBtiwu+pu8W/Eyotw==
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
execa "^0.10.0"
|
||||
|
||||
nice-try@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==
|
||||
dependencies:
|
||||
path-key "^2.0.0"
|
||||
|
||||
p-finally@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
||||
integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
|
||||
|
||||
path-key@^2.0.0, path-key@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
|
||||
integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
|
||||
|
||||
semver@^5.5.0:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
|
||||
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
|
||||
|
||||
shebang-command@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
||||
integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==
|
||||
dependencies:
|
||||
shebang-regex "^1.0.0"
|
||||
|
||||
shebang-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
|
||||
integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==
|
||||
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||
|
||||
sort-keys@^5.0.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-5.1.0.tgz#50a3f3d1ad3c5a76d043e0aeeba7299241e9aa5c"
|
||||
integrity sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==
|
||||
dependencies:
|
||||
is-plain-obj "^4.0.0"
|
||||
|
||||
string-width@^4.2.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-eof@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
|
||||
integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==
|
||||
|
||||
typedarray-to-buffer@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||
integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
|
||||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
which@^1.2.9:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
write-file-atomic@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
|
||||
integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
|
||||
dependencies:
|
||||
imurmurhash "^0.1.4"
|
||||
is-typedarray "^1.0.0"
|
||||
signal-exit "^3.0.2"
|
||||
typedarray-to-buffer "^3.1.5"
|
||||
|
||||
write-json-file@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-5.0.0.tgz#11c329a8ea9e8e23fb92a87cc27412a15f87708b"
|
||||
integrity sha512-ddSsCLa4aQ3kI21BthINo4q905/wfhvQ3JL3774AcRjBaiQmfn5v4rw77jQ7T6CmAit9VOQO+FsLyPkwxoB1fw==
|
||||
dependencies:
|
||||
detect-indent "^7.0.0"
|
||||
is-plain-obj "^4.0.0"
|
||||
sort-keys "^5.0.0"
|
||||
write-file-atomic "^3.0.3"
|
||||
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## v0.3.0
|
||||
|
||||
* Add generate storyboard support (PeerTube >= 8.0)
|
||||
|
||||
## v0.2.0
|
||||
|
||||
* Add runner version in request and register payloads
|
||||
|
||||
@@ -10,8 +10,7 @@ Commands below has to be run at the root of PeerTube git repository.
|
||||
|
||||
```bash
|
||||
cd peertube-root
|
||||
yarn install --pure-lockfile
|
||||
cd apps/peertube-runner && yarn install --pure-lockfile
|
||||
npm run install-node-dependencies
|
||||
```
|
||||
|
||||
### Develop
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@peertube/peertube-runner",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"main": "dist/peertube-runner.js",
|
||||
"bin": "dist/peertube-runner.js",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@peertube/net-ipc": "^2.2.0",
|
||||
"@types/follow-redirects": "1.14.4",
|
||||
"cli-table3": "^0.6.5",
|
||||
"env-paths": "^3.0.0",
|
||||
"follow-redirects": "^1.15.5",
|
||||
"net-ipc": "^2.2.2",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
RunnerJobVODWebVideoTranscodingPayload
|
||||
} from '@peertube/peertube-models'
|
||||
import { logger } from '../../shared/index.js'
|
||||
import { processAudioMergeTranscoding, processHLSTranscoding, ProcessOptions, processWebVideoTranscoding } from './shared/index.js'
|
||||
import {
|
||||
processAudioMergeTranscoding,
|
||||
processGenerateStoryboard,
|
||||
processHLSTranscoding,
|
||||
ProcessOptions,
|
||||
processWebVideoTranscoding
|
||||
} from './shared/index.js'
|
||||
import { ProcessLiveRTMPHLSTranscoding } from './shared/process-live.js'
|
||||
import { processStudioTranscoding } from './shared/process-studio.js'
|
||||
import { processVideoTranscription } from './shared/process-transcription.js'
|
||||
@@ -15,7 +21,7 @@ import { processVideoTranscription } from './shared/process-transcription.js'
|
||||
export async function processJob (options: ProcessOptions) {
|
||||
const { server, job } = options
|
||||
|
||||
logger.info(`[${server.url}] Processing job of type ${job.type}: ${job.uuid}`, { payload: job.payload })
|
||||
logger.info({ payload: job.payload }, `[${server.url}] Processing job of type ${job.type}: ${job.uuid}`)
|
||||
|
||||
switch (job.type) {
|
||||
case 'vod-audio-merge-transcoding':
|
||||
@@ -42,6 +48,10 @@ export async function processJob (options: ProcessOptions) {
|
||||
await processVideoTranscription(options as ProcessOptions<RunnerJobTranscriptionPayload>)
|
||||
break
|
||||
|
||||
case 'generate-video-storyboard':
|
||||
await processGenerateStoryboard(options as any)
|
||||
break
|
||||
|
||||
default:
|
||||
logger.error(`Unknown job ${job.type} to process`)
|
||||
return
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { FFmpegEdition, FFmpegLive, FFmpegVOD, getDefaultAvailableEncoders, getDefaultEncodersToTry } from '@peertube/peertube-ffmpeg'
|
||||
import {
|
||||
FFmpegEdition,
|
||||
FFmpegImage,
|
||||
FFmpegLive,
|
||||
FFmpegVOD,
|
||||
getDefaultAvailableEncoders,
|
||||
getDefaultEncodersToTry
|
||||
} from '@peertube/peertube-ffmpeg'
|
||||
import { RunnerJob, RunnerJobPayload } from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
@@ -8,9 +15,9 @@ import { join } from 'path'
|
||||
import { ConfigManager, downloadFile, logger } from '../../../shared/index.js'
|
||||
import { getWinstonLogger } from './winston-logger.js'
|
||||
|
||||
export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string }
|
||||
export type JobWithToken<T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string }
|
||||
|
||||
export type ProcessOptions <T extends RunnerJobPayload = RunnerJobPayload> = {
|
||||
export type ProcessOptions<T extends RunnerJobPayload = RunnerJobPayload> = {
|
||||
server: PeerTubeServer
|
||||
job: JobWithToken<T>
|
||||
runnerToken: string
|
||||
@@ -108,6 +115,10 @@ export function buildFFmpegEdition () {
|
||||
return new FFmpegEdition(getCommonFFmpegOptions())
|
||||
}
|
||||
|
||||
export function buildFFmpegImage () {
|
||||
return new FFmpegImage(getCommonFFmpegOptions())
|
||||
}
|
||||
|
||||
function getCommonFFmpegOptions () {
|
||||
const config = ConfigManager.Instance.getConfig()
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './common.js'
|
||||
export * from './process-vod.js'
|
||||
export * from './winston-logger.js'
|
||||
export * from './process-storyboard.js'
|
||||
|
||||
@@ -23,11 +23,11 @@ import { ConfigManager } from '../../../shared/config-manager.js'
|
||||
import { logger } from '../../../shared/index.js'
|
||||
import { buildFFmpegLive, ProcessOptions } from './common.js'
|
||||
|
||||
type CustomLiveRTMPHLSTranscodingUpdatePayload =
|
||||
Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & { resolutionPlaylistFile?: [ Buffer, string ] | Blob | string }
|
||||
type CustomLiveRTMPHLSTranscodingUpdatePayload = Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & {
|
||||
resolutionPlaylistFile?: [Buffer, string] | Blob | string
|
||||
}
|
||||
|
||||
export class ProcessLiveRTMPHLSTranscoding {
|
||||
|
||||
private readonly outputPath: string
|
||||
private readonly fsWatchers: FSWatcher[] = []
|
||||
|
||||
@@ -326,7 +326,7 @@ export class ProcessLiveRTMPHLSTranscoding {
|
||||
|
||||
const p = payloadBuilder().then(p => this.updateWithRetry(p))
|
||||
|
||||
if (!sequentialPromises) sequentialPromises = p
|
||||
if (sequentialPromises === undefined) sequentialPromises = p
|
||||
else sequentialPromises = sequentialPromises.then(() => p)
|
||||
}
|
||||
|
||||
@@ -388,7 +388,7 @@ export class ProcessLiveRTMPHLSTranscoding {
|
||||
return [
|
||||
Buffer.from(this.latestFilteredPlaylistContent[playlistName], 'utf-8'),
|
||||
join(this.outputPath, 'master.m3u8')
|
||||
] as [ Buffer, string ]
|
||||
] as [Buffer, string]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { RunnerJobGenerateStoryboardPayload, GenerateStoryboardSuccess } from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { remove } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { ConfigManager } from '../../../shared/config-manager.js'
|
||||
import { logger } from '../../../shared/index.js'
|
||||
import { buildFFmpegImage, downloadInputFile, ProcessOptions, scheduleTranscodingProgress } from './common.js'
|
||||
|
||||
export async function processGenerateStoryboard (options: ProcessOptions<RunnerJobGenerateStoryboardPayload>) {
|
||||
const { server, job, runnerToken } = options
|
||||
|
||||
const payload = job.payload
|
||||
|
||||
let ffmpegProgress: number
|
||||
let videoInputPath: string
|
||||
|
||||
const outputPath = join(ConfigManager.Instance.getStoryboardDirectory(), `storyboard-${buildUUID()}.jpg`)
|
||||
|
||||
const updateProgressInterval = scheduleTranscodingProgress({
|
||||
job,
|
||||
server,
|
||||
runnerToken,
|
||||
progressGetter: () => ffmpegProgress
|
||||
})
|
||||
|
||||
try {
|
||||
logger.info(`Downloading input file ${payload.input.videoFileUrl} for storyboard job ${job.jobToken}`)
|
||||
|
||||
videoInputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
|
||||
|
||||
logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Generating storyboard.`)
|
||||
|
||||
const ffmpegImage = buildFFmpegImage()
|
||||
|
||||
await ffmpegImage.generateStoryboardFromVideo({
|
||||
path: videoInputPath,
|
||||
destination: outputPath,
|
||||
inputFileMutexReleaser: () => {},
|
||||
sprites: payload.sprites
|
||||
})
|
||||
|
||||
const successBody: GenerateStoryboardSuccess = {
|
||||
storyboardFile: outputPath
|
||||
}
|
||||
|
||||
await server.runnerJobs.success({
|
||||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
runnerToken,
|
||||
payload: successBody,
|
||||
reqPayload: payload
|
||||
})
|
||||
} finally {
|
||||
if (videoInputPath) await remove(videoInputPath)
|
||||
if (outputPath) await remove(outputPath)
|
||||
if (updateProgressInterval) clearInterval(updateProgressInterval)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ import {
|
||||
ProcessOptions,
|
||||
scheduleTranscodingProgress
|
||||
} from './common.js'
|
||||
import {
|
||||
canDoQuickAudioTranscode,
|
||||
canDoQuickVideoTranscode,
|
||||
ffprobePromise,
|
||||
getVideoStreamDimensionsInfo,
|
||||
getVideoStreamFPS
|
||||
} from '@peertube/peertube-ffmpeg'
|
||||
|
||||
export async function processWebVideoTranscoding (options: ProcessOptions<RunnerJobVODWebVideoTranscodingPayload>) {
|
||||
const { server, job, runnerToken } = options
|
||||
@@ -46,7 +53,9 @@ export async function processWebVideoTranscoding (options: ProcessOptions<Runner
|
||||
logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running web video transcoding.`)
|
||||
|
||||
const ffmpegVod = buildFFmpegVOD({
|
||||
onJobProgress: progress => { ffmpegProgress = progress }
|
||||
onJobProgress: progress => {
|
||||
ffmpegProgress = progress
|
||||
}
|
||||
})
|
||||
|
||||
await ffmpegVod.transcode({
|
||||
@@ -108,15 +117,28 @@ export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVO
|
||||
videoInputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
|
||||
separatedAudioInputPath = await downloadSeparatedAudioFileIfNeeded({ urls: payload.input.separatedAudioFileUrl, runnerToken, job })
|
||||
|
||||
const inputProbe = await ffprobePromise(videoInputPath)
|
||||
const { resolution } = await getVideoStreamDimensionsInfo(videoInputPath, inputProbe)
|
||||
const fps = await getVideoStreamFPS(videoInputPath, inputProbe)
|
||||
|
||||
// Copy codecs if the input file can be quick transcoded (appropriate bitrate, codecs, etc.)
|
||||
// And if the input resolution/fps are the same as the output resolution/fps
|
||||
const copyCodecs = await canDoQuickAudioTranscode(videoInputPath, inputProbe) &&
|
||||
await canDoQuickVideoTranscode(videoInputPath, fps) &&
|
||||
resolution === payload.output.resolution &&
|
||||
(!resolution || fps === payload.output.fps)
|
||||
|
||||
logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running HLS transcoding.`)
|
||||
|
||||
const ffmpegVod = buildFFmpegVOD({
|
||||
onJobProgress: progress => { ffmpegProgress = progress }
|
||||
onJobProgress: progress => {
|
||||
ffmpegProgress = progress
|
||||
}
|
||||
})
|
||||
|
||||
await ffmpegVod.transcode({
|
||||
type: 'hls',
|
||||
copyCodecs: false,
|
||||
copyCodecs,
|
||||
|
||||
videoInputPath,
|
||||
separatedAudioInputPath,
|
||||
@@ -172,7 +194,7 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
|
||||
try {
|
||||
logger.info(
|
||||
`Downloading input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` +
|
||||
`for audio merge transcoding job ${job.jobToken}`
|
||||
`for audio merge transcoding job ${job.jobToken}`
|
||||
)
|
||||
|
||||
audioPath = await downloadInputFile({ url: payload.input.audioFileUrl, runnerToken, job })
|
||||
@@ -180,11 +202,13 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
|
||||
|
||||
logger.info(
|
||||
`Downloaded input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` +
|
||||
`for job ${job.jobToken}. Running audio merge transcoding.`
|
||||
`for job ${job.jobToken}. Running audio merge transcoding.`
|
||||
)
|
||||
|
||||
const ffmpegVod = buildFFmpegVOD({
|
||||
onJobProgress: progress => { ffmpegProgress = progress }
|
||||
onJobProgress: progress => {
|
||||
ffmpegProgress = progress
|
||||
}
|
||||
})
|
||||
|
||||
await ffmpegVod.transcode({
|
||||
|
||||
@@ -57,7 +57,7 @@ export class RunnerServer {
|
||||
try {
|
||||
await ipcServer.run(this)
|
||||
} catch (err) {
|
||||
logger.error('Cannot start local socket for IPC communication', err)
|
||||
logger.error(err, 'Cannot start local socket for IPC communication')
|
||||
process.exit(-1)
|
||||
}
|
||||
|
||||
@@ -74,9 +74,11 @@ export class RunnerServer {
|
||||
|
||||
// Process jobs
|
||||
await ensureDir(ConfigManager.Instance.getTranscodingDirectory())
|
||||
await ensureDir(ConfigManager.Instance.getStoryboardDirectory())
|
||||
await this.cleanupTMP()
|
||||
|
||||
logger.info(`Using ${ConfigManager.Instance.getTranscodingDirectory()} for transcoding directory`)
|
||||
logger.info(`Using ${ConfigManager.Instance.getStoryboardDirectory()} for storyboard directory`)
|
||||
|
||||
this.initialized = true
|
||||
await this.checkAvailableJobs()
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
RunnerJobVODAudioMergeTranscodingPayload,
|
||||
RunnerJobVODHLSTranscodingPayload,
|
||||
RunnerJobVODWebVideoTranscodingPayload,
|
||||
VideoStudioTaskPayload
|
||||
VideoStudioTaskPayload,
|
||||
RunnerJobGenerateStoryboardPayload
|
||||
} from '@peertube/peertube-models'
|
||||
|
||||
const supportedMatrix: { [ id in RunnerJobType ]: (payload: RunnerJobPayload) => boolean } = {
|
||||
@@ -33,7 +34,8 @@ const supportedMatrix: { [ id in RunnerJobType ]: (payload: RunnerJobPayload) =>
|
||||
},
|
||||
'video-transcription': (_payload: RunnerJobTranscriptionPayload) => {
|
||||
return true
|
||||
}
|
||||
},
|
||||
'generate-video-storyboard': (_payload: RunnerJobGenerateStoryboardPayload) => true
|
||||
}
|
||||
|
||||
export function isJobSupported (job: { type: RunnerJobType, payload: RunnerJobPayload }, enabledJobs?: Set<RunnerJobType>) {
|
||||
|
||||
@@ -108,6 +108,10 @@ export class ConfigManager {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getStoryboardDirectory () {
|
||||
return join(paths.cache, this.id, 'storyboard')
|
||||
}
|
||||
|
||||
getTranscodingDirectory () {
|
||||
return join(paths.cache, this.id, 'transcoding')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Client as NetIPC } from '@peertube/net-ipc'
|
||||
import { Client as NetIPC } from 'net-ipc'
|
||||
import CliTable3 from 'cli-table3'
|
||||
import { ensureDir } from 'fs-extra/esm'
|
||||
import { ConfigManager } from '../config-manager.js'
|
||||
@@ -20,7 +20,7 @@ export class IPCClient {
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
throw new Error(
|
||||
'This runner is not currently running in server mode on this system. ' +
|
||||
'Please run it using the `server` command first (in another terminal for example) and then retry your command.'
|
||||
'Please run it using the `server` command first (in another terminal for example) and then retry your command.'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ensureDir } from 'fs-extra/esm'
|
||||
import { Server as NetIPC } from '@peertube/net-ipc'
|
||||
import { Server as NetIPC } from 'net-ipc'
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { RunnerServer } from '../../server/index.js'
|
||||
import { ConfigManager } from '../config-manager.js'
|
||||
@@ -58,11 +58,11 @@ export class IPCServer {
|
||||
}
|
||||
}
|
||||
|
||||
private sendResponse <T extends IPCResponseData> (
|
||||
private sendResponse<T extends IPCResponseData> (
|
||||
response: (data: any) => Promise<void>,
|
||||
body: IPCResponse<T>
|
||||
) {
|
||||
response(body)
|
||||
.catch(err => logger.error('Cannot send response after IPC request', err))
|
||||
.catch(err => logger.error(err, 'Cannot send response after IPC request'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@iarna/toml@^2.2.5":
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
|
||||
integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz#9edec61b22c3082018a79f6d1c30289ddf3d9d11"
|
||||
integrity sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz#33677a275204898ad8acbf62734fc4dc0b6a4855"
|
||||
integrity sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz#19edf7cdc2e7063ee328403c1d895a86dd28f4bb"
|
||||
integrity sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz#94fb0543ba2e28766c3fc439cabbe0440ae70159"
|
||||
integrity sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz#4a0609ab5fe44d07c9c60a11e4484d3c38bbd6e3"
|
||||
integrity sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242"
|
||||
integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==
|
||||
|
||||
"@peertube/net-ipc@^2.2.0":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@peertube/net-ipc/-/net-ipc-2.2.1.tgz#3d1c154a08b57cfea31ed760ec76fe2f69e35a19"
|
||||
integrity sha512-RyKIGC3EeQ+xnSccf592qqsaXWrGp4wGfGl4W+wxDoZkwsThZJuiSbX8aCC1qZBHaDo3EuRH3ZrwsKpNjnyDAQ==
|
||||
optionalDependencies:
|
||||
fast-zlib "^2.0.1"
|
||||
msgpackr "^1.3.2"
|
||||
|
||||
"@types/follow-redirects@1.14.4":
|
||||
version "1.14.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/follow-redirects/-/follow-redirects-1.14.4.tgz#ca054d72ef574c77949fc5fff278b430fcd508ec"
|
||||
integrity sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "24.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.1.0.tgz#0993f7dc31ab5cc402d112315b463e383d68a49c"
|
||||
integrity sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==
|
||||
dependencies:
|
||||
undici-types "~7.8.0"
|
||||
|
||||
atomic-sleep@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
|
||||
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
|
||||
|
||||
colorette@^2.0.7:
|
||||
version "2.0.20"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||
|
||||
dateformat@^4.6.3:
|
||||
version "4.6.3"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
|
||||
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
|
||||
|
||||
detect-libc@^2.0.1:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8"
|
||||
integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c"
|
||||
integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
env-paths@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da"
|
||||
integrity sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==
|
||||
|
||||
fast-copy@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35"
|
||||
integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==
|
||||
|
||||
fast-redact@^3.1.1:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4"
|
||||
integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==
|
||||
|
||||
fast-safe-stringify@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||
|
||||
fast-zlib@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-zlib/-/fast-zlib-2.0.1.tgz#be624f592fc80ad8019ee2025d16a367a4e9b024"
|
||||
integrity sha512-DCoYgNagM2Bt1VIpXpdGnRx4LzqJeYG0oh6Nf/7cWo6elTXkFGMw9CrRCYYUIapYNrozYMoyDRflx9mgT3Awyw==
|
||||
|
||||
follow-redirects@^1.15.5:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
help-me@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6"
|
||||
integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==
|
||||
|
||||
joycon@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
||||
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
msgpackr-extract@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz#e9d87023de39ce714872f9e9504e3c1996d61012"
|
||||
integrity sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==
|
||||
dependencies:
|
||||
node-gyp-build-optional-packages "5.2.2"
|
||||
optionalDependencies:
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.3"
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3"
|
||||
|
||||
msgpackr@^1.3.2:
|
||||
version "1.11.5"
|
||||
resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.5.tgz#edf0b9d9cb7d8ed6897dd0e42cfb865a2f4b602e"
|
||||
integrity sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==
|
||||
optionalDependencies:
|
||||
msgpackr-extract "^3.0.2"
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4"
|
||||
integrity sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==
|
||||
dependencies:
|
||||
detect-libc "^2.0.1"
|
||||
|
||||
on-exit-leak-free@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8"
|
||||
integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==
|
||||
|
||||
once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
pino-abstract-transport@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60"
|
||||
integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==
|
||||
dependencies:
|
||||
split2 "^4.0.0"
|
||||
|
||||
pino-pretty@^13.0.0:
|
||||
version "13.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-13.0.0.tgz#21d57fe940e34f2e279905d7dba2d7e2c4f9bf17"
|
||||
integrity sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==
|
||||
dependencies:
|
||||
colorette "^2.0.7"
|
||||
dateformat "^4.6.3"
|
||||
fast-copy "^3.0.2"
|
||||
fast-safe-stringify "^2.1.1"
|
||||
help-me "^5.0.0"
|
||||
joycon "^3.1.1"
|
||||
minimist "^1.2.6"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^2.0.0"
|
||||
pump "^3.0.0"
|
||||
secure-json-parse "^2.4.0"
|
||||
sonic-boom "^4.0.1"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
pino-std-serializers@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b"
|
||||
integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==
|
||||
|
||||
pino@^9.6.0:
|
||||
version "9.7.0"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-9.7.0.tgz#ff7cd86eb3103ee620204dbd5ca6ffda8b53f645"
|
||||
integrity sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
fast-redact "^3.1.1"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^2.0.0"
|
||||
pino-std-serializers "^7.0.0"
|
||||
process-warning "^5.0.0"
|
||||
quick-format-unescaped "^4.0.3"
|
||||
real-require "^0.2.0"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
sonic-boom "^4.0.1"
|
||||
thread-stream "^3.0.0"
|
||||
|
||||
process-warning@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7"
|
||||
integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d"
|
||||
integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
quick-format-unescaped@^4.0.3:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
|
||||
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
|
||||
|
||||
real-require@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
|
||||
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
|
||||
|
||||
safe-stable-stringify@^2.3.1:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
|
||||
integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
|
||||
|
||||
secure-json-parse@^2.4.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
|
||||
integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
|
||||
|
||||
sonic-boom@^4.0.1:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d"
|
||||
integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
|
||||
split2@^4.0.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
|
||||
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
thread-stream@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1"
|
||||
integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==
|
||||
dependencies:
|
||||
real-require "^0.2.0"
|
||||
|
||||
undici-types@~7.8.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294"
|
||||
integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
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`.
|
||||
@@ -185,7 +185,7 @@
|
||||
"."
|
||||
],
|
||||
"sass": {
|
||||
"silenceDeprecations": [ "import", "mixed-decls", "color-functions", "global-builtin" ]
|
||||
"silenceDeprecations": [ "import", "color-functions", "global-builtin" ]
|
||||
}
|
||||
},
|
||||
"assets": [
|
||||
@@ -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 () {
|
||||
|
||||
@@ -86,7 +86,7 @@ export class VideoListPage {
|
||||
async expectVideoNSFWTooltip (name: string, summary?: string) {
|
||||
const miniature = await this.getVideoMiniature(name)
|
||||
|
||||
const warning = await miniature.$('.nsfw-warning')
|
||||
const warning = miniature.$('.nsfw-warning')
|
||||
await warning.waitForDisplayed()
|
||||
|
||||
expect(await warning.getAttribute('aria-label')).toEqual(summary)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -151,10 +151,7 @@ export class VideoWatchPage {
|
||||
await this.clickOnMoreDropdownIcon()
|
||||
|
||||
// We need the await expression
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
const items = await $$('.dropdown-menu.show .dropdown-item')
|
||||
|
||||
for (const item of items) {
|
||||
return $$('.dropdown-menu.show .dropdown-item').mapSeries(async item => {
|
||||
const content = await item.getText()
|
||||
|
||||
if (content.includes('Manage')) {
|
||||
@@ -162,11 +159,12 @@ export class VideoWatchPage {
|
||||
await $('#name').waitForClickable()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 +174,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()
|
||||
|
||||
@@ -65,9 +67,9 @@ export async function selectCustomSelect (id: string, valueLabel: string) {
|
||||
|
||||
export async function findParentElement (
|
||||
el: ChainablePromiseElement,
|
||||
finder: (el: WebdriverIO.Element) => Promise<boolean>
|
||||
finder: (el: ChainablePromiseElement) => Promise<boolean>
|
||||
) {
|
||||
if (await finder(el) === true) return el
|
||||
|
||||
return findParentElement(await el.parentElement(), finder)
|
||||
return findParentElement(el.parentElement(), finder)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"module": "commonjs",
|
||||
"target": "ES2018",
|
||||
"typeRoots": [
|
||||
"../node_modules/@wdio",
|
||||
"../node_modules/@types",
|
||||
"../node_modules"
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
@@ -53,6 +53,7 @@
|
||||
"@ngx-loading-bar/http-client": "^7.0.0",
|
||||
"@ngx-loading-bar/router": "^7.0.0",
|
||||
"@peertube/maildev": "^1.2.0",
|
||||
"@peertube/player": "workspace:*",
|
||||
"@peertube/xliffmerge": "^2.0.3",
|
||||
"@plussub/srt-vtt-parser": "^2.0.5",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
@@ -67,9 +68,9 @@
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/sanitize-html": "2.11.0",
|
||||
"@types/sha.js": "^2.4.0",
|
||||
"@types/video.js": "^7.3.40",
|
||||
"@wdio/browserstack-service": "^9.12.7",
|
||||
"@wdio/cli": "^9.12.7",
|
||||
"@wdio/globals": "^9.17.0",
|
||||
"@wdio/local-runner": "^9.12.7",
|
||||
"@wdio/mocha-framework": "^9.12.6",
|
||||
"@wdio/shared-store-service": "^9.12.7",
|
||||
@@ -109,10 +110,10 @@
|
||||
"type-fest": "^4.37.0",
|
||||
"typescript": "~5.7.3",
|
||||
"ua-parser-js": "^2.0.3",
|
||||
"video.js": "^7.19.2",
|
||||
"video.js": "^8.23.4",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-checker": "^0.9.3",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -27,17 +27,25 @@
|
||||
*ngIf="config.instance.social.mastodonLink"
|
||||
class="media peertube-button-link rounded-icon-button mb-3" i18n-title title="Go to the Mastodon profile"
|
||||
target="_blank" rel="noopener noreferrer" [href]="config.instance.social.mastodonLink"
|
||||
>
|
||||
>
|
||||
<my-global-icon iconName="mastodon"></my-global-icon>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="config.instance.social.xLink"
|
||||
class="media peertube-button-link rounded-icon-button mb-3" i18n-title title="Go to the X profile"
|
||||
target="_blank" rel="noopener noreferrer" [href]="config.instance.social.xLink"
|
||||
>
|
||||
<my-global-icon iconName="x-twitter"></my-global-icon>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="config.instance.social.blueskyLink"
|
||||
class="media peertube-button-link rounded-icon-button mb-3" i18n-title title="Go to the Bluesky profile"
|
||||
target="_blank" rel="noopener noreferrer" [href]="config.instance.social.blueskyLink"
|
||||
>
|
||||
<my-global-icon iconName="bluesky"></my-global-icon>
|
||||
</a>
|
||||
<my-global-icon iconName="bluesky"></my-global-icon>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="config.instance.social.externalLink"
|
||||
|
||||
@@ -93,7 +93,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
|
||||
sort: '-updatedAt'
|
||||
}
|
||||
|
||||
this.videoChannelService.listAccountVideoChannels(options)
|
||||
this.videoChannelService.listAccountChannels(options)
|
||||
.pipe(
|
||||
tap(res => {
|
||||
this.channelPagination.totalItems = res.total
|
||||
|
||||
@@ -95,7 +95,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||
distinctUntilChanged(),
|
||||
switchMap(accountId => this.accountService.getAccount(accountId)),
|
||||
tap(account => this.onAccount(account)),
|
||||
switchMap(account => this.videoChannelService.listAccountVideoChannels({ account })),
|
||||
switchMap(account => this.videoChannelService.listAccountChannels({ account })),
|
||||
catchError(err =>
|
||||
this.restExtractor.redirectTo404IfNotFound(err, 'other', [
|
||||
HttpStatusCode.BAD_REQUEST_400,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { AdminConfigCustomizationComponent } from './pages/admin-config-customiz
|
||||
import { AdminConfigService } from '../../shared/shared-admin/admin-config.service'
|
||||
import { AdminConfigLogoComponent } from './pages/admin-config-logo.component'
|
||||
import { InstanceLogoService } from '../../shared/shared-instance/instance-logo.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
|
||||
export const customConfigResolver: ResolveFn<CustomConfig> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(AdminConfigService).getCustomConfig()
|
||||
@@ -71,7 +72,8 @@ export const configRoutes: Routes = [
|
||||
customConfig: customConfigResolver
|
||||
},
|
||||
providers: [
|
||||
InstanceLogoService
|
||||
InstanceLogoService,
|
||||
PlayerSettingsService
|
||||
],
|
||||
component: AdminConfigComponent,
|
||||
children: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<my-admin-save-bar i18n-title title="Advanced configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<form [formGroup]="form" id="admin-config-form">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
|
||||
|
||||
@@ -36,8 +36,11 @@ input[type="checkbox"] {
|
||||
@include peertube-select-container($form-base-input-width);
|
||||
}
|
||||
|
||||
my-select-videos-sort,
|
||||
my-select-videos-scope,
|
||||
my-select-checkbox,
|
||||
my-select-options,
|
||||
my-select-player-theme,
|
||||
my-select-custom-value {
|
||||
display: block;
|
||||
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
<my-admin-save-bar i18n-title title="Platform customization" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<form [formGroup]="form" id="admin-config-form">
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h2 i18n>APPEARANCE</h2>
|
||||
</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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<my-admin-save-bar i18n-title title="General configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<form [formGroup]="form" id="admin-config-form">
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h2 i18n>BEHAVIOR</h2>
|
||||
</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,42 +327,74 @@
|
||||
|
||||
<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>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>BROWSE VIDEOS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<div class="form-group" formGroupName="client">
|
||||
<ng-container formGroupName="browseVideos">
|
||||
<div class="form-group">
|
||||
<label i18n for="browseVideosDefaultSort">Default sort</label>
|
||||
|
||||
<my-select-videos-sort inputId="browseVideosDefaultSort" formControlName="defaultSort"></my-select-videos-sort>
|
||||
|
||||
<div *ngIf="formErrors.client.browseVideos.defaultSort" class="form-error" role="alert">{{ formErrors.client.browseVideos.defaultSort }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="browseVideosDefaultScope">Default scope</label>
|
||||
|
||||
<my-select-videos-scope inputId="browseVideosDefaultScope" formControlName="defaultScope"></my-select-videos-scope>
|
||||
|
||||
<div *ngIf="formErrors.client.browseVideos.defaultScope" class="form-error" role="alert">{{ formErrors.client.browseVideos.defaultScope }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>VIDEOS</h2>
|
||||
</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 +403,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,38 +415,47 @@
|
||||
|
||||
<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>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div class="form-group" formGroupName="remoteRunners" [ngClass]="getStoryboardRunnerDisabledClass()">
|
||||
<my-peertube-checkbox
|
||||
inputName="storyboardsRemoteRunnersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable remote runners to generate storyboards"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to generate storyboards.</div>
|
||||
<div i18n>Remote runners have to register on your instance first.</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<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.
|
||||
</span>
|
||||
<div i18n>Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process transcription tasks.</div>
|
||||
<div i18n>Remote runners have to register on your instance first.</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
@@ -402,7 +466,6 @@
|
||||
|
||||
<ng-container formGroupName="defaults">
|
||||
<ng-container formGroupName="publish">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="defaultsPublishPrivacy">Default video privacy</label>
|
||||
|
||||
@@ -442,8 +505,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 +529,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 +548,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 +576,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 +587,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 +605,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 +627,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 +653,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 +694,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 +711,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 +735,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 +786,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 +800,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 +818,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 +830,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>
|
||||
|
||||
@@ -33,6 +33,8 @@ import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { SelectVideosScopeComponent } from '../../../shared/shared-forms/select/select-videos-scope.component'
|
||||
import { SelectVideosSortComponent } from '../../../shared/shared-forms/select/select-videos-sort.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
@@ -43,6 +45,11 @@ type Form = {
|
||||
}>
|
||||
|
||||
client: FormGroup<{
|
||||
browseVideos: FormGroup<{
|
||||
defaultSort: FormControl<string>
|
||||
defaultScope: FormControl<string>
|
||||
}>
|
||||
|
||||
menu: FormGroup<{
|
||||
login: FormGroup<{
|
||||
redirectOnSingleExternalAuth: FormControl<boolean>
|
||||
@@ -175,6 +182,10 @@ type Form = {
|
||||
|
||||
storyboards: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
|
||||
remoteRunners: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
|
||||
defaults: FormGroup<{
|
||||
@@ -220,7 +231,9 @@ type Form = {
|
||||
UserRealQuotaInfoComponent,
|
||||
SelectOptionsComponent,
|
||||
AlertComponent,
|
||||
AdminSaveBarComponent
|
||||
AdminSaveBarComponent,
|
||||
SelectVideosSortComponent,
|
||||
SelectVideosScopeComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
@@ -295,6 +308,10 @@ export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanCompon
|
||||
defaultClientRoute: null
|
||||
},
|
||||
client: {
|
||||
browseVideos: {
|
||||
defaultSort: null,
|
||||
defaultScope: null
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: null
|
||||
@@ -410,7 +427,10 @@ export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanCompon
|
||||
}
|
||||
},
|
||||
storyboards: {
|
||||
enabled: null
|
||||
enabled: null,
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
defaults: {
|
||||
publish: {
|
||||
@@ -523,6 +543,16 @@ export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanCompon
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isStoryboardEnabled () {
|
||||
return this.form.value.storyboards.enabled === true
|
||||
}
|
||||
|
||||
getStoryboardRunnerDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isStoryboardEnabled() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isAutoFollowIndexEnabled () {
|
||||
return this.form.value.followings.instance.autoFollowIndex.enabled === true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<my-admin-save-bar i18n-title title="Edit your homepage" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form class="homepage pt-two-cols" [formGroup]="form">
|
||||
<form class="homepage pt-two-cols" [formGroup]="form" id="admin-config-form">
|
||||
<div class="title-col">
|
||||
<h2 i18n>HOMEPAGE</h2>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<my-admin-save-bar i18n-title title="Platform information" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<form [formGroup]="form" id="admin-config-form">
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
@@ -172,6 +172,17 @@
|
||||
<div *ngIf="formErrors.instance.social.mastodonLink" class="form-error" role="alert">{{ formErrors.instance.social.mastodonLink }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceSocialXLink">X link</label>
|
||||
|
||||
<input
|
||||
type="text" id="instanceSocialXLink" class="form-control"
|
||||
formControlName="xLink" [ngClass]="{ 'input-error': formErrors.instance.social.xLink }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.instance.social.xLink" class="form-error" role="alert">{{ formErrors.instance.social.xLink }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceSocialBlueskyLink">Bluesky link</label>
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ type Form = {
|
||||
externalLink: FormControl<string>
|
||||
mastodonLink: FormControl<string>
|
||||
blueskyLink: FormControl<string>
|
||||
xLink: FormControl<string>
|
||||
}>
|
||||
|
||||
isNSFW: FormControl<boolean>
|
||||
@@ -205,7 +206,8 @@ export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanCo
|
||||
social: {
|
||||
externalLink: URL_VALIDATOR,
|
||||
mastodonLink: URL_VALIDATOR,
|
||||
blueskyLink: URL_VALIDATOR
|
||||
blueskyLink: URL_VALIDATOR,
|
||||
xLink: URL_VALIDATOR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<my-admin-save-bar i18n-title title="Live configuration" (save)="save()" [form]="form" [formErrors]="formErrors" [inconsistentOptions]="checkTranscodingConsistentOptions()"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<form [formGroup]="form" id="admin-config-form">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
@@ -167,10 +167,8 @@
|
||||
i18n-labelText labelText="Enable remote runners for lives"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>
|
||||
Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process live transcoding.
|
||||
Remote runners has to register on your instance first.
|
||||
</span>
|
||||
<div i18n>Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process live transcoding.</div>
|
||||
<div i18n>Remote runners have to register on your instance first.</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
<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">
|
||||
<form [formGroup]="form" id="admin-config-form">
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>LOGO</h2>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<my-admin-save-bar i18n-title title="VOD configuration" (save)="save()" [form]="form" [formErrors]="formErrors" [inconsistentOptions]="checkTranscodingConsistentOptions()"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<form [formGroup]="form" id="admin-config-form">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
@@ -185,10 +185,8 @@
|
||||
i18n-labelText labelText="Enable remote runners for VOD"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>
|
||||
Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process VOD transcoding.
|
||||
Remote runners has to register on your instance first.
|
||||
</span>
|
||||
<div i18n>Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process VOD transcoding.</div>
|
||||
<div i18n>Remote runners have to register on your instance first.</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
@@ -262,19 +260,19 @@
|
||||
<ng-container ngProjectAs="description" *ngIf="!isTranscodingEnabled()">
|
||||
<span i18n>⚠️ You need to enable transcoding first to enable video studio</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="remoteRunners" [ngClass]="getStudioDisabledClass()">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoStudioRemoteRunnersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable remote runners for studio"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>
|
||||
Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process studio transcoding tasks.
|
||||
Remote runners has to register on your instance first.
|
||||
</span>
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div class="form-group" formGroupName="remoteRunners" [ngClass]="getStudioRunnerDisabledClass()">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoStudioRemoteRunnersEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable remote runners for studio"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process studio transcoding tasks.</div>
|
||||
<div i18n>Remote runners have to register on your instance first.</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
@@ -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: {
|
||||
@@ -226,7 +226,7 @@ export class AdminConfigVODComponent implements OnInit, OnDestroy, CanComponentD
|
||||
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() || this.isRemoteRunnerVODEnabled() }
|
||||
}
|
||||
|
||||
getStudioDisabledClass () {
|
||||
getStudioRunnerDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isStudioEnabled() }
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="buttons">
|
||||
<my-button theme="secondary" class="pre-config" (click)="openConfigWizard()" i18n>Open config wizard</my-button>
|
||||
|
||||
<my-button theme="primary" class="save-button" icon="circle-tick" [disabled]="!canUpdate()" (click)="onSave($event)" i18n>Save</my-button>
|
||||
<my-button form="admin-config-form" theme="primary" class="save-button" icon="circle-tick" [disabled]="!canUpdate()" (click)="onSave($event)" i18n>Save</my-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
.root {
|
||||
position: sticky;
|
||||
|
||||
@@ -13,23 +13,27 @@
|
||||
<form novalidate [formGroup]="form" (ngSubmit)="processRegistration()">
|
||||
<div class="modal-body mb-3">
|
||||
|
||||
<my-alert i18n *ngIf="!registration.emailVerified" type="warning">
|
||||
Registration email has not been verified. Email delivery has been disabled by default.
|
||||
</my-alert>
|
||||
|
||||
<div class="description">
|
||||
<ng-container *ngIf="isAccept()">
|
||||
<p i18n>
|
||||
<strong>Accepting</strong> <em>{{ registration.username }}</em> registration will create the account and channel.
|
||||
</p>
|
||||
|
||||
<p *ngIf="isEmailEnabled()" i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }">
|
||||
An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below.
|
||||
</p>
|
||||
|
||||
<my-alert *ngIf="!isEmailEnabled()" type="warning" i18n>
|
||||
Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its account has been created.
|
||||
</my-alert>
|
||||
@if (isEmailEnabled()) {
|
||||
@if (!registration.emailVerified) {
|
||||
<my-alert i18n type="warning">
|
||||
Registration email has not been verified. Email delivery has been disabled by default.
|
||||
</my-alert>
|
||||
} @else {
|
||||
<p i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }">
|
||||
An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below.
|
||||
</p>
|
||||
}
|
||||
} @else {
|
||||
<my-alert type="warning" i18n>
|
||||
Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its account has been created.
|
||||
</my-alert>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isReject()">
|
||||
@@ -58,7 +62,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="preventEmailDelivery" formControlName="preventEmailDelivery" [disabled]="!isEmailEnabled()"
|
||||
inputName="preventEmailDelivery" formControlName="preventEmailDelivery"
|
||||
i18n-labelText labelText="Prevent email from being sent to the user"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, inject, output, viewChild } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
@@ -16,7 +16,7 @@ import { REGISTRATION_MODERATION_RESPONSE_VALIDATOR } from './process-registrati
|
||||
@Component({
|
||||
selector: 'my-process-registration-modal',
|
||||
templateUrl: './process-registration-modal.component.html',
|
||||
imports: [ NgIf, GlobalIconComponent, FormsModule, ReactiveFormsModule, NgClass, PeertubeCheckboxComponent, AlertComponent ]
|
||||
imports: [ CommonModule, GlobalIconComponent, FormsModule, ReactiveFormsModule, PeertubeCheckboxComponent, AlertComponent ]
|
||||
})
|
||||
export class ProcessRegistrationModalComponent extends FormReactive implements OnInit {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
@@ -53,6 +53,12 @@ export class ProcessRegistrationModalComponent extends FormReactive implements O
|
||||
this.processMode = mode
|
||||
this.registration = registration
|
||||
|
||||
if (this.registration.emailVerified !== true || !this.isEmailEnabled()) {
|
||||
this.form.get('preventEmailDelivery').disable()
|
||||
} else {
|
||||
this.form.get('preventEmailDelivery').enable()
|
||||
}
|
||||
|
||||
this.form.patchValue({
|
||||
preventEmailDelivery: !this.isEmailEnabled() || registration.emailVerified !== true
|
||||
})
|
||||
|
||||
@@ -6,12 +6,12 @@ import { AuthService, Notifier, ScreenService, ServerService } from '@app/core'
|
||||
import {
|
||||
USER_CHANNEL_NAME_VALIDATOR,
|
||||
USER_EMAIL_VALIDATOR,
|
||||
USER_PASSWORD_OPTIONAL_VALIDATOR,
|
||||
USER_PASSWORD_VALIDATOR,
|
||||
USER_ROLE_VALIDATOR,
|
||||
USER_USERNAME_VALIDATOR,
|
||||
USER_VIDEO_QUOTA_DAILY_VALIDATOR,
|
||||
USER_VIDEO_QUOTA_VALIDATOR
|
||||
USER_VIDEO_QUOTA_VALIDATOR,
|
||||
getUserNewPasswordOptionalValidator,
|
||||
getUserNewPasswordValidator
|
||||
} from '@app/shared/form-validators/user-validators'
|
||||
import { AdminConfigService } from '@app/shared/shared-admin/admin-config.service'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
@@ -76,11 +76,17 @@ export class UserCreateComponent extends UserEdit implements OnInit {
|
||||
videoQuotaDaily: -1
|
||||
}
|
||||
|
||||
const passwordConstraints = this.serverService.getHTMLConfig().fieldsConstraints.users.password
|
||||
|
||||
this.buildForm({
|
||||
username: USER_USERNAME_VALIDATOR,
|
||||
channelName: USER_CHANNEL_NAME_VALIDATOR,
|
||||
email: USER_EMAIL_VALIDATOR,
|
||||
password: this.isPasswordOptional() ? USER_PASSWORD_OPTIONAL_VALIDATOR : USER_PASSWORD_VALIDATOR,
|
||||
|
||||
password: this.isPasswordOptional()
|
||||
? getUserNewPasswordOptionalValidator(passwordConstraints.minLength, passwordConstraints.maxLength)
|
||||
: getUserNewPasswordValidator(passwordConstraints.minLength, passwordConstraints.maxLength),
|
||||
|
||||
role: USER_ROLE_VALIDATOR,
|
||||
videoQuota: USER_VIDEO_QUOTA_VALIDATOR,
|
||||
videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject, input } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { getUserNewPasswordValidator } from '@app/shared/form-validators/user-validators'
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { UserUpdate } from '@peertube/peertube-models'
|
||||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { UserAdminService } from '@app/shared/shared-users/user-admin.service'
|
||||
import { UserUpdate } from '@peertube/peertube-models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-user-password',
|
||||
@@ -18,6 +18,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
private notifier = inject(Notifier)
|
||||
private userAdminService = inject(UserAdminService)
|
||||
private serverService = inject(ServerService)
|
||||
|
||||
readonly userId = input<number>(undefined)
|
||||
readonly username = input<string>(undefined)
|
||||
@@ -26,9 +27,9 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
|
||||
showPassword = false
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
password: USER_PASSWORD_VALIDATOR
|
||||
})
|
||||
const { minLength, maxLength } = this.serverService.getHTMLConfig().fieldsConstraints.users.password
|
||||
|
||||
this.buildForm({ password: getUserNewPasswordValidator(minLength, maxLength) })
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
<ng-template #actionCell let-video>
|
||||
<my-video-actions-dropdown
|
||||
placement="bottom auto" buttonDirection="horizontal" buttonStyled="true" [video]="video" [displayOptions]="videoActionsOptions"
|
||||
placement="bottom auto" buttonIcon="more-horizontal" buttonStyled="true" [video]="video" [displayOptions]="videoActionsOptions"
|
||||
(videoRemoved)="table.loadData()" (videoFilesRemoved)="table.loadData()" (transcodingCreated)="table.loadData()"
|
||||
></my-video-actions-dropdown>
|
||||
</ng-template>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Subscription } from 'rxjs'
|
||||
import { map, switchMap } from 'rxjs/operators'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { HooksService, Notifier, PluginService } from '@app/core'
|
||||
import { HooksService, Notifier, PluginService, ServerService } from '@app/core'
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { PeerTubePlugin, RegisterServerSettingOptions } from '@peertube/peertube-models'
|
||||
@@ -24,6 +24,7 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit
|
||||
private notifier = inject(Notifier)
|
||||
private hooks = inject(HooksService)
|
||||
private route = inject(ActivatedRoute)
|
||||
private server = inject(ServerService)
|
||||
|
||||
plugin: PeerTubePlugin
|
||||
registeredSettings: RegisterServerSettingOptions[] = []
|
||||
@@ -50,6 +51,7 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit
|
||||
const settings = this.form.value
|
||||
|
||||
this.pluginAPIService.updatePluginSettings(this.plugin.name, this.plugin.type, settings)
|
||||
.pipe(switchMap(() => this.server.resetConfig()))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Settings updated.`)
|
||||
|
||||
@@ -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>
|
||||
@@ -39,23 +39,23 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #tableCells let-job>
|
||||
<td *ngIf="table.isColumnDisplayed('id')" class="job-id c-hand" [title]="job.id">{{ job.id }}</td>
|
||||
<td *ngIf="table.isColumnDisplayed('id')" class="job-i" [title]="job.id">{{ job.id }}</td>
|
||||
|
||||
<td *ngIf="table.isColumnDisplayed('type')" class="job-type c-hand" >
|
||||
<td *ngIf="table.isColumnDisplayed('type')" class="job-typ" >
|
||||
<span class="pt-badge ellipsis" [ngClass]="getRandomJobTypeBadge(job.type)">{{ job.type }}</span>
|
||||
</td>
|
||||
|
||||
<td *ngIf="table.isColumnDisplayed('priority')" class="job-priority c-hand" >{{ job.priority }}</td>
|
||||
<td *ngIf="table.isColumnDisplayed('priority')" class="job-priorit" >{{ job.priority }}</td>
|
||||
|
||||
<td *ngIf="table.isColumnDisplayed('state') && table.isColumnDisplayed('state')" class="job-state c-hand" >
|
||||
<td *ngIf="table.isColumnDisplayed('state') && table.isColumnDisplayed('state')" class="job-stat" >
|
||||
<span class="ellipsis" [ngClass]="getJobStateClasses(job.state)">{{ job.state }}</span>
|
||||
</td>
|
||||
|
||||
<td *ngIf="table.isColumnDisplayed('progress')" class="job-progress c-hand" >
|
||||
<td *ngIf="table.isColumnDisplayed('progress')" class="job-progres" >
|
||||
<ng-container *ngIf="hasProgress(job)">{{ getProgress(job) }}</ng-container>
|
||||
</td>
|
||||
|
||||
<td *ngIf="table.isColumnDisplayed('createdAt')" class="job-date c-hand" >{{ job.createdAt }}</td>
|
||||
<td *ngIf="table.isColumnDisplayed('createdAt')" class="job-dat" >{{ job.createdAt }}</td>
|
||||
|
||||
<td *ngIf="table.isColumnDisplayed('processed')" class="fs-7">
|
||||
<div>{{ job.processedOn }}</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<label i18n for="log-start-date">Start date</label>
|
||||
|
||||
<my-select-options inputId="log-start-date" [items]="timeChoices" [(ngModel)]="startDate" (ngModelChange)="refresh()">
|
||||
<ng-template ptTemplate="item" let-item>
|
||||
<ng-template #selectOption let-item>
|
||||
{{ item.label }} ({{ item.id | ptDate: item.dateFormat }} - <span i18n>now</span>)
|
||||
</ng-template>
|
||||
</my-select-options>
|
||||
@@ -25,7 +25,7 @@
|
||||
<label i18n for="log-level">Log level</label>
|
||||
|
||||
<my-select-options inputId="log-level" [items]="levelChoices" [(ngModel)]="level" (ngModelChange)="refresh()">
|
||||
<ng-template ptTemplate="item" let-item>
|
||||
<ng-template #selectOption let-item>
|
||||
<span class="level-choice me-1" [ngClass]="item.id">⬤</span> {{ item.label }}
|
||||
</ng-template>
|
||||
</my-select-options>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { FormsModule } from '@angular/forms'
|
||||
import { LocalStorageService, Notifier } from '@app/core'
|
||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { PeerTubeTemplateDirective } from '@app/shared/shared-main/common/peertube-template.directive'
|
||||
import { ServerLogLevel } from '@peertube/peertube-models'
|
||||
import { SelectTagsComponent } from '../../../shared/shared-forms/select/select-tags.component'
|
||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||
@@ -24,8 +23,7 @@ import { LogsService } from './logs.service'
|
||||
ButtonComponent,
|
||||
PTDatePipe,
|
||||
CopyButtonComponent,
|
||||
SelectOptionsComponent,
|
||||
PeerTubeTemplateDirective
|
||||
SelectOptionsComponent
|
||||
]
|
||||
})
|
||||
export class LogsComponent implements OnInit {
|
||||
|
||||
@@ -18,6 +18,7 @@ export class ErrorPageComponent implements OnInit {
|
||||
type: 'video' | 'other' = 'other'
|
||||
|
||||
public constructor () {
|
||||
// Keep this in constructor so we getCurrentNavigation() doesn't return null
|
||||
const state = this.router.getCurrentNavigation()?.extras.state
|
||||
this.type = state?.type || this.type
|
||||
this.status = state?.obj.status || this.status
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Component, ElementRef, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { Component, ElementRef, inject, OnDestroy, OnInit, viewChild } from '@angular/core'
|
||||
import { DisableForReuseHook, PeerTubeRouterService } from '@app/core'
|
||||
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
|
||||
import { debounceTime, fromEvent, Subscription } from 'rxjs'
|
||||
import { CustomMarkupContainerComponent } from '../shared/shared-custom-markup/custom-markup-container.component'
|
||||
|
||||
@Component({
|
||||
templateUrl: './home.component.html',
|
||||
imports: [ CustomMarkupContainerComponent ]
|
||||
})
|
||||
export class HomeComponent implements OnInit {
|
||||
export class HomeComponent implements OnInit, OnDestroy, DisableForReuseHook {
|
||||
private customPageService = inject(CustomPageService)
|
||||
private sub: Subscription
|
||||
private peertubeRouter = inject(PeerTubeRouterService)
|
||||
private disabled = false
|
||||
|
||||
readonly contentWrapper = viewChild<ElementRef<HTMLInputElement>>('contentWrapper')
|
||||
|
||||
@@ -16,5 +21,33 @@ export class HomeComponent implements OnInit {
|
||||
ngOnInit () {
|
||||
this.customPageService.getInstanceHomepage()
|
||||
.subscribe(({ content }) => this.homepageContent = content)
|
||||
|
||||
// If this has been a redirection from the homepage,
|
||||
// replace the URL on scroll to correctly restore scroll position on web browser back button
|
||||
if (window.location.pathname === '/') {
|
||||
this.sub = fromEvent(window, 'scroll')
|
||||
.pipe(debounceTime(250))
|
||||
.subscribe(() => {
|
||||
if (this.disabled) return
|
||||
|
||||
if (window.pageYOffset > 300) {
|
||||
this.peertubeRouter.silentNavigate([], {})
|
||||
this.sub.unsubscribe()
|
||||
this.sub = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.sub) this.sub.unsubscribe()
|
||||
}
|
||||
|
||||
disableForReuse () {
|
||||
this.disabled = true
|
||||
}
|
||||
|
||||
enabledForReuse () {
|
||||
this.disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ export default [
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Homepage`
|
||||
},
|
||||
reuse: {
|
||||
enabled: true,
|
||||
key: 'home'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<div class="margin-content">
|
||||
<h1 class="title-page" i18n>Login on {{ instanceName }}</h1>
|
||||
|
||||
<my-alert type="danger" i18n *ngIf="externalAuthError">
|
||||
Sorry but there was an issue with the external login process. Please <a class="link-primary" routerLink="/about">contact an administrator</a>.
|
||||
</my-alert>
|
||||
|
||||
<ng-container *ngIf="!externalAuthError && !isAuthenticatedWithExternalAuth">
|
||||
|
||||
<my-alert type="primary">
|
||||
@@ -25,10 +29,6 @@
|
||||
}
|
||||
</my-alert>
|
||||
|
||||
<my-alert type="danger" i18n *ngIf="externalAuthError">
|
||||
Sorry but there was an issue with the external login process. Please <a class="link-primary" routerLink="/about">contact an administrator</a>.
|
||||
</my-alert>
|
||||
|
||||
<my-alert *ngIf="error" type="danger">
|
||||
{{ error }}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Routes } from '@angular/router'
|
||||
import { VideoChannelCreateComponent } from '@app/shared/standalone-channels/video-channel-create.component'
|
||||
import { VideoChannelUpdateComponent } from '@app/shared/standalone-channels/video-channel-update.component'
|
||||
|
||||
export default [
|
||||
{
|
||||
path: 'create',
|
||||
component: VideoChannelCreateComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Create a new video channel`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'update/:videoChannelName',
|
||||
component: VideoChannelUpdateComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Update video channel`
|
||||
}
|
||||
}
|
||||
}
|
||||
] satisfies Routes
|
||||
@@ -30,6 +30,7 @@
|
||||
<li i18n>A directory containing static files (thumbnails, avatars, video files etc.)</li>
|
||||
</ul>
|
||||
|
||||
<p i18n>Videos and playlists from collaborated channels, as well as your channel collaborators, <strong>will not</strong> be included in the archive.</p>
|
||||
<p i18n>You can only request one archive at a time.</p>
|
||||
|
||||
@if (isEmailEnabled()) {
|
||||
|
||||
@@ -26,4 +26,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<my-user-notifications #userNotification></my-user-notifications>
|
||||
<div class="user-notifications-container">
|
||||
<my-user-notifications #userNotification></my-user-notifications>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_button-mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_button-mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
@@ -13,3 +13,8 @@
|
||||
@include peertube-select-container(auto);
|
||||
}
|
||||
}
|
||||
|
||||
.user-notifications-container {
|
||||
padding: 1rem;
|
||||
background-color: pvar(--bg-secondary-400);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export class MyAccountNotificationsComponent {
|
||||
}
|
||||
|
||||
hasUnreadNotifications () {
|
||||
return this.userNotification().notifications.filter(n => n.read === false).length !== 0
|
||||
return this.userNotification().notifications.filter(n => n.payload.read === false).length !== 0
|
||||
}
|
||||
|
||||
onChangeSortColumn () {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { AuthService, Notifier, UserService } from '@app/core'
|
||||
import { AuthService, Notifier, ServerService, UserService } from '@app/core'
|
||||
import {
|
||||
USER_CONFIRM_PASSWORD_VALIDATOR,
|
||||
USER_EXISTING_PASSWORD_VALIDATOR,
|
||||
USER_PASSWORD_VALIDATOR
|
||||
getUserNewPasswordValidator
|
||||
} from '@app/shared/form-validators/user-validators'
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
@@ -25,14 +25,17 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
|
||||
private notifier = inject(Notifier)
|
||||
private authService = inject(AuthService)
|
||||
private userService = inject(UserService)
|
||||
private serverService = inject(ServerService)
|
||||
|
||||
error: string
|
||||
user: User
|
||||
|
||||
ngOnInit () {
|
||||
const { minLength, maxLength } = this.serverService.getHTMLConfig().fieldsConstraints.users.password
|
||||
|
||||
this.buildForm({
|
||||
'current-password': USER_EXISTING_PASSWORD_VALIDATOR,
|
||||
'new-password': USER_PASSWORD_VALIDATOR,
|
||||
'new-password': getUserNewPasswordValidator(minLength, maxLength),
|
||||
'new-confirmed-password': USER_CONFIRM_PASSWORD_VALIDATOR
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
<h2 class="form-title">
|
||||
<my-global-icon iconName="cog"></my-global-icon>
|
||||
|
||||
<ng-container i18n>General</ng-container>
|
||||
</h2>
|
||||
|
||||
<form id="channel-manage-form" [formGroup]="form">
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h3 i18n>IMAGES</h3>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<my-actor-banner-edit
|
||||
previewImage="true"
|
||||
class="d-block mb-4"
|
||||
[bannerUrl]="videoChannelEdit.apiInfo.bannerUrl"
|
||||
(bannerChange)="onBannerChange($event)"
|
||||
(bannerDelete)="onBannerDelete()"
|
||||
></my-actor-banner-edit>
|
||||
|
||||
<my-actor-avatar-edit
|
||||
class="d-block mb-4"
|
||||
actorType="channel"
|
||||
previewImage="true"
|
||||
[avatars]="videoChannelEdit.apiInfo.avatars"
|
||||
[username]="mode === 'update' && videoChannelEdit.channel.name"
|
||||
[subscribers]="mode === 'update' && videoChannelEdit.apiInfo.followersCount"
|
||||
(avatarChange)="onAvatarChange($event)"
|
||||
(avatarDelete)="onAvatarDelete()"
|
||||
></my-actor-avatar-edit>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h3 i18n>INFORMATION</h3>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<div class="form-group" [hidden]="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"
|
||||
>
|
||||
<div class="input-group-text">@{{ instanceHost }}</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.name" class="form-error" role="alert">{{ formErrors.name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="displayName">Display name</label>
|
||||
<input type="text" id="displayName" class="form-control" formControlName="displayName" [ngClass]="{ 'input-error': formErrors.displayName }">
|
||||
<div *ngIf="formErrors.displayName" class="form-error" role="alert">
|
||||
{{ formErrors.displayName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group markdown-block">
|
||||
<label i18n for="description">Description</label>
|
||||
|
||||
<my-help helpType="markdownText" supportRelMe="true"></my-help>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="description"
|
||||
formControlName="description"
|
||||
markdownType="enhanced"
|
||||
[formError]="formErrors.description"
|
||||
withEmoji="true"
|
||||
withHtml="true"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group markdown-block">
|
||||
<label i18n for="support">Support</label>
|
||||
|
||||
<div class="form-group-description" i18n>
|
||||
This is text that helps people visiting your channel page <strong>understand how to support you</strong>. You can use <my-markdown-hint
|
||||
helpType="markdownText"
|
||||
>Markdown Language</my-markdown-hint>, including <strong>links to your fundraising tools</strong>.
|
||||
</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="support"
|
||||
formControlName="support"
|
||||
markdownType="enhanced"
|
||||
[formError]="formErrors.support"
|
||||
withEmoji="true"
|
||||
withHtml="true"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
|
||||
<my-peertube-checkbox
|
||||
inputName="bulkVideosSupportUpdate"
|
||||
formControlName="bulkVideosSupportUpdate"
|
||||
i18n-labelText
|
||||
labelText="Overwrite support field of all videos of this channel"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h3 i18n>VIDEOS</h3>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,25 @@
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
my-actor-banner-edit {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
@include peertube-input-text(340px);
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
@include margin-left(auto);
|
||||
}
|
||||
|
||||
.markdown-block {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
my-select-player-theme {
|
||||
display: block;
|
||||
|
||||
@include responsive-width(340px);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user