Compare commits

..

52 Commits

Author SHA1 Message Date
Glassed Silver
072210b605 Merge pull request #1144 from ItsOpenSourceSoftware/patch-1
Update README.md
2026-03-05 00:18:37 +01:00
Glassed Silver
5b0ec39dcd Merge pull request #1141 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2026-03-05 00:17:25 +01:00
Mateus Liberale Gomes
1ec0b38e92 Translated using Weblate (Portuguese (Brazil))
Currently translated at 67.7% (328 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt_BR/
2025-12-24 21:00:23 +00:00
Mateus Liberale Gomes
3857e3cad2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 40.7% (197 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt_BR/
2025-12-22 13:00:22 +01:00
Mateus Liberale Gomes
d297ea59f9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 23.9% (116 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt_BR/
2025-12-12 03:00:19 +01:00
Guðmundur Erlingsson
74ef4a322e Added translation using Weblate (Icelandic) 2025-10-08 11:16:33 +02:00
Zash012
be1a64ffa2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 23.1% (112 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt_BR/
2025-09-25 20:02:00 +00:00
marvel at flowers
04d1ed5b8d Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 45.8% (222 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hant/
2025-09-01 01:40:09 +02:00
marvel at flowers
d5ad026dc8 Translated using Weblate (Estonian)
Currently translated at 30.9% (150 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/et/
2025-09-01 01:40:09 +02:00
Yurt Page
75757ab65b Translated using Weblate (Russian)
Currently translated at 100.0% (484 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2025-07-16 13:01:51 +02:00
Yurt Page
5a7b794179 Translated using Weblate (Russian)
Currently translated at 100.0% (484 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2025-07-16 13:01:49 +02:00
ItsOpenSourceSoftware
f9011bee7c Update README.md
Fixed shortcut typo
2025-05-29 09:30:04 +10:00
polarwood
3fa7f5fa1d Translated using Weblate (Turkish)
Currently translated at 96.6% (468 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/tr/
2025-04-15 13:36:04 +02:00
Szabolcs Milán
976f120692 Translated using Weblate (Hungarian)
Currently translated at 0.8% (4 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/hu/
2025-02-28 21:03:41 +00:00
Glassed Silver
100e6f8174 Merge pull request #1136 from Foxy-NC/patch-1
Update messages.fr.xlf
2025-02-28 00:55:35 +01:00
Glassed Silver
95e73b99f5 Merge pull request #1069 from weblate/weblate-youtubedl-material-ytdl-material
Translations update from Hosted Weblate
2025-02-28 00:54:46 +01:00
Szabolcs Milán
e82deb6413 Added translation using Weblate (Hungarian) 2025-02-27 21:03:42 +01:00
reimu105
8e4a1a090f Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 42.1% (204 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hant/
2025-01-26 03:02:34 +01:00
reimu105
639624aee8 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 30.9% (150 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hant/
2025-01-22 01:00:36 +01:00
reimu105
9e0cdd09cf Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 29.7% (144 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hant/
2025-01-11 21:17:57 +01:00
reimu105
5369f4e6cc Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 24.3% (118 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/zh_Hant/
2025-01-08 16:03:23 +01:00
reimu105
97703b4142 Added translation using Weblate (Chinese (Traditional Han script)) 2025-01-07 15:38:42 +01:00
Fedor M
127ddd88ce Translated using Weblate (Russian)
Currently translated at 98.5% (477 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2025-01-05 14:56:05 +01:00
Fedor M
7b3965f624 Translated using Weblate (Russian)
Currently translated at 89.4% (433 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2025-01-04 00:07:54 +01:00
தமிழ்நேரம்
84b3a6a49b Translated using Weblate (Tamil)
Currently translated at 100.0% (484 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ta/
2024-11-14 19:01:01 +01:00
Foxy-NC
ea578c1d70 Update messages.fr.xlf
French translation completed
2024-11-10 18:23:36 +11:00
David Chevalier
4ee50792b2 Translated using Weblate (French)
Currently translated at 100.0% (484 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2024-11-10 04:14:09 +01:00
David Chevalier
db25e824b7 Translated using Weblate (French)
Currently translated at 100.0% (484 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/fr/
2024-11-10 03:45:23 +01:00
Marko Galevski
afd20bc06b Translated using Weblate (Macedonian)
Currently translated at 61.9% (300 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/mk/
2024-07-26 20:09:23 +02:00
Ktilis
8e85d84ba3 Translated using Weblate (Russian)
Currently translated at 85.9% (416 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ru/
2024-07-18 23:09:25 +02:00
Wojciech Teichert
9d54ab7ccf Translated using Weblate (Polish)
Currently translated at 100.0% (484 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pl/
2024-06-14 21:09:28 +02:00
தமிழ்நேரம்
1f34b735c1 Translated using Weblate (Tamil)
Currently translated at 100.0% (484 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/ta/
2024-05-29 23:55:01 +02:00
தமிழ்நேரம்
f0ed6b396a Added translation using Weblate (Tamil) 2024-05-29 17:52:38 +02:00
ssantos
a018f56e44 Translated using Weblate (Portuguese)
Currently translated at 55.7% (270 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt/
2024-03-24 20:02:02 +01:00
Eryk Michalak
792b0f9896 Translated using Weblate (Polish)
Currently translated at 99.1% (480 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pl/
2024-03-17 13:02:01 +01:00
Scrambled777
c227a019f5 Translated using Weblate (Hindi)
Currently translated at 100.0% (484 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/hi/
2024-03-01 16:00:29 +01:00
Scrambled777
1adb6fe76c Added translation using Weblate (Hindi) 2024-02-29 15:53:01 +01:00
Mariane
6fbf314afd Translated using Weblate (Portuguese)
Currently translated at 55.7% (270 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pt/
2024-02-05 06:02:17 +01:00
Fernando Zamorano
3ed2499707 Translated using Weblate (Spanish)
Currently translated at 100.0% (484 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/es/
2024-01-18 14:00:30 +01:00
Szymon Scholz
55c1151f98 Translated using Weblate (Polish)
Currently translated at 96.0% (465 of 484 strings)

Translation: YoutubeDL-Material/ytdl-material
Translate-URL: https://hosted.weblate.org/projects/youtubedl-material/ytdl-material/pl/
2024-01-18 14:00:28 +01:00
Dedy Martadinata S
58f4313e50 Merge pull request #1079 from Tzahi12345/dependabot/github_actions/dot-github/workflows/actions/cache-4
Bump actions/cache from 3 to 4 in /.github/workflows
2024-01-18 18:53:59 +07:00
dependabot[bot]
20bc0a2632 Bump actions/cache from 3 to 4 in /.github/workflows
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-18 11:39:28 +00:00
Dedy Martadinata S
208cd837be Merge pull request #1057 from Tzahi12345/dependabot/github_actions/dot-github/workflows/actions/download-artifact-4
Bump actions/download-artifact from 3 to 4 in /.github/workflows
2023-12-15 19:14:13 +07:00
Dedy Martadinata S
ea90346eef Merge pull request #1058 from Tzahi12345/dependabot/github_actions/dot-github/workflows/actions/upload-artifact-4
Bump actions/upload-artifact from 3 to 4 in /.github/workflows
2023-12-15 19:13:53 +07:00
dependabot[bot]
185c1fd0fc Bump actions/upload-artifact from 3 to 4 in /.github/workflows
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 11:49:30 +00:00
dependabot[bot]
a032dbe84f Bump actions/download-artifact from 3 to 4 in /.github/workflows
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 11:49:25 +00:00
Dedy Martadinata S
e89bc10725 Merge pull request #1048 from Tzahi12345/dependabot/github_actions/dot-github/workflows/docker/setup-qemu-action-3
Bump docker/setup-qemu-action from 2 to 3 in /.github/workflows
2023-12-08 08:43:58 +07:00
Dedy Martadinata S
86464fc646 Merge pull request #1047 from Tzahi12345/dependabot/github_actions/dot-github/workflows/docker/setup-buildx-action-3
Bump docker/setup-buildx-action from 2 to 3 in /.github/workflows
2023-12-08 08:43:01 +07:00
Dedy Martadinata S
387ff4036f Merge pull request #1049 from Tzahi12345/dependabot/github_actions/dot-github/workflows/actions/setup-node-4
Bump actions/setup-node from 3 to 4 in /.github/workflows
2023-12-08 08:27:37 +07:00
dependabot[bot]
1fdc4ed983 Bump actions/setup-node from 3 to 4 in /.github/workflows
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-07 11:28:23 +00:00
dependabot[bot]
1968ce47d4 Bump docker/setup-qemu-action from 2 to 3 in /.github/workflows
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-07 11:28:18 +00:00
dependabot[bot]
dfc522a845 Bump docker/setup-buildx-action from 2 to 3 in /.github/workflows
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-07 11:28:16 +00:00
82 changed files with 36875 additions and 10067 deletions

View File

@@ -15,9 +15,9 @@ jobs:
- name: checkout code
uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: '16'
cache: 'npm'
- name: install dependencies
run: |
@@ -55,7 +55,7 @@ jobs:
Copy-Item -Path ./backend/*.js -Destination ./build/youtubedl-material
Copy-Item -Path ./backend/*.json -Destination ./build/youtubedl-material
- name: upload build artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: youtubedl-material
path: build
@@ -81,7 +81,7 @@ jobs:
draft: true
prerelease: false
- name: download build artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: youtubedl-material
path: ${{runner.temp}}/youtubedl-material

View File

@@ -24,9 +24,9 @@ jobs:
json: '{"type": "docker", "tag": "nightly", "commit": "${{ steps.vars.outputs.sha_short }}", "date": "${{ steps.date.outputs.date }}"}'
dir: 'backend/'
- name: setup platform emulator
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: build & push images
uses: docker/build-push-action@v5
with:

View File

@@ -57,10 +57,10 @@ jobs:
type=raw,value=latest
- name: setup platform emulator
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3

View File

@@ -41,10 +41,10 @@ jobs:
dir: 'backend/'
- name: setup platform emulator
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: setup multi-arch docker build
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Generate Docker image metadata
id: docker-meta

View File

@@ -16,14 +16,14 @@ jobs:
strategy:
matrix:
node:
- 18
- 16
steps:
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '${{ matrix.node }}'
- uses: actions/checkout@v4
- name: 'Cache node_modules'
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
@@ -33,7 +33,7 @@ jobs:
- uses: FedericoCarboni/setup-ffmpeg@v2
id: setup-ffmpeg
- name: Install Dependencies
run: npm install --dev
run: npm install
working-directory: ./backend
- name: Run All Node.js Tests
run: npm run test

View File

@@ -1,5 +1,5 @@
# Fetching our utils
FROM ubuntu:23.04 AS utils
FROM ubuntu:22.04 AS utils
ENV DEBIAN_FRONTEND=noninteractive
# Use script due local build compability
COPY docker-utils/*.sh .
@@ -8,12 +8,13 @@ RUN sh ./ffmpeg-fetch.sh
RUN sh ./fetch-twitchdownloader.sh
# Create our Ubuntu 22.04 with node 18.19.0
FROM ubuntu:23.04 AS base
# Create our Ubuntu 22.04 with node 16.14.2 (that specific version is required as per: https://stackoverflow.com/a/72855258/8088021)
# Go to 20.04
FROM ubuntu:22.04 AS base
ARG TARGETPLATFORM
ARG DEBIAN_FRONTEND=noninteractive
ENV UID=1001
ENV GID=1001
ENV UID=1000
ENV GID=1000
ENV USER=youtube
ENV NO_UPDATE_NOTIFIER=true
ENV PM2_HOME=/app/pm2
@@ -21,10 +22,10 @@ ENV ALLOW_CONFIG_MUTATIONS=true
ENV npm_config_cache=/app/.npm
# Use NVM to get specific node version
ENV NODE_VERSION=18.19.0
ENV NODE_VERSION=16.14.2
RUN groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
apt update && \
apt install -y --no-install-recommends curl ca-certificates tzdata libatomic1 && \
apt install -y --no-install-recommends curl ca-certificates tzdata libicu70 libatomic1 && \
apt clean && \
rm -rf /var/lib/apt/lists/*
@@ -36,11 +37,9 @@ RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
RUN npm install -g npm
# Build frontend
ARG BUILDPLATFORM
FROM --platform=${BUILDPLATFORM} node:18 as frontend
FROM --platform=${BUILDPLATFORM} node:16 as frontend
RUN npm install -g @angular/cli
WORKDIR /build
COPY [ "package.json", "package-lock.json", "angular.json", "tsconfig.json", "/build/" ]
@@ -57,8 +56,6 @@ FROM base as backend
WORKDIR /app
COPY [ "backend/","/app/" ]
RUN npm config set strict-ssl false && \
npm config set registry https://registry.npm.taobao.org && \
npm config set fetch-retry-maxtimeout 60000 && \
npm install --prod && \
ls -al
@@ -75,10 +72,10 @@ RUN npm config set strict-ssl false && \
# Final image
FROM base
RUN apt update && \
curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash && \
RUN npm install -g pm2 && \
apt update && \
apt install -y --no-install-recommends gosu python3-minimal python-is-python3 python3-pip atomicparsley build-essential && \
pip install pycryptodomex --break-system-packages && \
pip install pycryptodomex && \
apt remove -y --purge build-essential && \
apt autoremove -y --purge && \
apt clean && \

View File

@@ -6,7 +6,7 @@
[![GitHub issues badge](https://img.shields.io/github/issues/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/issues)
[![License badge](https://img.shields.io/github/license/Tzahi12345/YoutubeDL-Material)](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/LICENSE.md)
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 17](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 15](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
Now with [Docker](#Docker) support!
@@ -30,7 +30,7 @@ NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker
Required dependencies:
* Node.js 18
* Node.js 16
* Python
Optional dependencies:
@@ -42,7 +42,7 @@ Optional dependencies:
<summary>Debian/Ubuntu</summary>
```bash
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install nodejs youtube-dl ffmpeg unzip python npm
```
@@ -57,7 +57,7 @@ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfu
sudo yum install centos-release-scl-rh
sudo yum install rh-nodejs12
scl enable rh-nodejs12 bash
curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -
curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo bash -
sudo yum install nodejs youtube-dl ffmpeg ffmpeg-devel
```
@@ -136,7 +136,7 @@ Once you have enabled the API and have the key, you can start sending requests b
## iOS Shortcut
If you are using iOS, try YoutubeDL-Material more conveniently with a Shortcut. With this Shorcut, you can easily start downloading YouTube video with just two taps! (Or maybe three?)
If you are using iOS, try YoutubeDL-Material more conveniently with a Shortcut. With this Shortcut, you can easily start downloading YouTube video with just two taps! (Or maybe three?)
You can download Shortcut [here.](https://routinehub.co/shortcut/10283/)

View File

@@ -83,24 +83,24 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"buildTarget": "youtube-dl-material:build"
"browserTarget": "youtube-dl-material:build"
},
"configurations": {
"production": {
"buildTarget": "youtube-dl-material:build:production"
"browserTarget": "youtube-dl-material:build:production"
},
"es": {
"buildTarget": "youtube-dl-material:build:es"
"browserTarget": "youtube-dl-material:build:es"
},
"codespaces": {
"buildTarget": "youtube-dl-material:build:codespaces"
"browserTarget": "youtube-dl-material:build:codespaces"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "youtube-dl-material:build"
"browserTarget": "youtube-dl-material:build"
}
},
"serve-electron": {

View File

@@ -521,8 +521,6 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
downloadConfig.push('--write-thumbnail');
}
downloadConfig.push('-i');
if (globalArgs && globalArgs !== '') {
// adds global args
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
@@ -573,8 +571,8 @@ exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
let {callback} = await youtubedl_api.runYoutubeDL(url, new_args);
const {parsed_output, err} = await callback;
if (!parsed_output || parsed_output.length === 0) {
let error_message = `Error while retrieving info on video with URL ${url}`;
if (err.stderr) error_message += ` with the following message: \n${err.stderr}`;
let error_message = `Error while retrieving info on video with URL ${url} with the following message: ${err}`;
if (err.stderr) error_message += `\n\n${err.stderr}`;
logger.error(error_message);
if (download_uid) {
await handleDownloadError(download_uid, error_message, 'info_retrieve_failed');

View File

@@ -30,6 +30,7 @@
"lodash": "^4.17.21",
"lowdb": "^1.0.0",
"md5": "^2.2.1",
"mocha": "^9.2.2",
"moment": "^2.29.4",
"mongodb": "^3.6.9",
"multer": "1.4.5-lts.1",
@@ -53,9 +54,6 @@
"winston": "^3.7.2",
"xmlbuilder2": "^3.0.2"
},
"devDependencies": {
"mocha": "^10.2.0"
},
"engines": {
"node": "^16",
"npm": "6.14.4"
@@ -365,6 +363,11 @@
"@types/node": "*"
}
},
"node_modules/@ungap/promise-all-settled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
},
"node_modules/@vladfrangu/async_event_emitter": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.1.tgz",
@@ -410,7 +413,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -419,7 +421,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -428,7 +429,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -443,7 +443,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -454,8 +453,7 @@
"node_modules/ansi-styles/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/any-promise": {
"version": "1.3.0",
@@ -466,7 +464,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -534,8 +531,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/array-buffer-byte-length": {
"version": "1.0.0",
@@ -711,7 +707,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -775,7 +770,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
@@ -786,8 +780,7 @@
"node_modules/browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
"dev": true
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
},
"node_modules/bson": {
"version": "1.1.6",
@@ -942,7 +935,6 @@
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
@@ -953,7 +945,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -1262,7 +1253,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
"integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -1341,7 +1331,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
"dev": true,
"engines": {
"node": ">=0.3.1"
}
@@ -1535,7 +1524,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -1549,7 +1537,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -1810,7 +1797,6 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -1839,7 +1825,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
@@ -1855,7 +1840,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
"dev": true,
"bin": {
"flat": "cli.js"
}
@@ -1980,7 +1964,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -2038,7 +2021,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
@@ -2113,7 +2095,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -2200,6 +2181,14 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
},
"node_modules/growl": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
"engines": {
"node": ">=4.x"
}
},
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -2291,7 +2280,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"bin": {
"he": "bin/he"
}
@@ -2462,7 +2450,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@@ -2519,7 +2506,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2528,7 +2514,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -2566,7 +2551,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
@@ -2589,7 +2573,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -2688,7 +2671,6 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -2726,7 +2708,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -2924,7 +2905,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"dependencies": {
"p-locate": "^5.0.0"
},
@@ -2969,7 +2949,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
@@ -2985,7 +2964,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -3001,7 +2979,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -3010,7 +2987,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -3202,39 +3178,41 @@
}
},
"node_modules/mocha": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
"integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
"dev": true,
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz",
"integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==",
"dependencies": {
"@ungap/promise-all-settled": "1.1.2",
"ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
"chokidar": "3.5.3",
"debug": "4.3.4",
"debug": "4.3.3",
"diff": "5.0.0",
"escape-string-regexp": "4.0.0",
"find-up": "5.0.0",
"glob": "7.2.0",
"growl": "1.10.5",
"he": "1.2.0",
"js-yaml": "4.1.0",
"log-symbols": "4.1.0",
"minimatch": "5.0.1",
"minimatch": "4.2.1",
"ms": "2.1.3",
"nanoid": "3.3.3",
"nanoid": "3.3.1",
"serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1",
"supports-color": "8.1.1",
"workerpool": "6.2.1",
"which": "2.0.2",
"workerpool": "6.2.0",
"yargs": "16.2.0",
"yargs-parser": "20.2.4",
"yargs-unparser": "2.0.0"
},
"bin": {
"_mocha": "bin/_mocha",
"mocha": "bin/mocha.js"
"mocha": "bin/mocha"
},
"engines": {
"node": ">= 14.0.0"
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
@@ -3245,7 +3223,6 @@
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
@@ -3269,10 +3246,9 @@
}
},
"node_modules/mocha/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dependencies": {
"ms": "2.1.2"
},
@@ -3288,14 +3264,12 @@
"node_modules/mocha/node_modules/debug/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mocha/node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -3315,7 +3289,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -3327,43 +3300,30 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/mocha/node_modules/minimatch": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
"dev": true,
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz",
"integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^1.1.7"
},
"engines": {
"node": ">=10"
}
},
"node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/mocha/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mocha/node_modules/nanoid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
"dev": true,
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
"integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -3375,7 +3335,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -3387,7 +3346,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"engines": {
"node": ">=8"
},
@@ -3399,7 +3357,6 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -3804,7 +3761,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
@@ -3819,7 +3775,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
@@ -3929,7 +3884,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -3981,7 +3935,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
"dev": true,
"engines": {
"node": ">=8.6"
},
@@ -4086,7 +4039,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -4293,7 +4245,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4440,7 +4391,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
"dev": true,
"dependencies": {
"randombytes": "^2.1.0"
}
@@ -4618,7 +4568,6 @@
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -4631,14 +4580,12 @@
"node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string-width/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -4647,7 +4594,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.0"
},
@@ -4780,7 +4726,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
@@ -5161,16 +5106,14 @@
}
},
"node_modules/workerpool": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
"integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
"dev": true
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz",
"integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A=="
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -5187,7 +5130,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -5278,7 +5220,6 @@
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
@@ -5292,7 +5233,6 @@
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
@@ -5310,7 +5250,6 @@
"version": "20.2.4",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
"integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
"dev": true,
"engines": {
"node": ">=10"
}
@@ -5319,7 +5258,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
"integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
"dev": true,
"dependencies": {
"camelcase": "^6.0.0",
"decamelize": "^4.0.0",
@@ -5334,7 +5272,6 @@
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -5346,7 +5283,6 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"engines": {
"node": ">=10"
},

View File

@@ -44,6 +44,7 @@
"lodash": "^4.17.21",
"lowdb": "^1.0.0",
"md5": "^2.2.1",
"mocha": "^9.2.2",
"moment": "^2.29.4",
"mongodb": "^3.6.9",
"multer": "1.4.5-lts.1",
@@ -66,8 +67,5 @@
"uuid": "^9.0.1",
"winston": "^3.7.2",
"xmlbuilder2": "^3.0.2"
},
"devDependencies": {
"mocha": "^10.2.0"
}
}

View File

@@ -529,9 +529,7 @@ exports.parseOutputJSON = (output, err) => {
let split_output = [];
// const output_jsons = [];
if (err && !output) {
const attempt_backup_errs = ['This video is unavailable', 'Private video', 'unavailable video'];
const attempt_backup = err.stderr ? attempt_backup_errs.some(err_msg => err.stderr.includes(err_msg)) : false;
if (!attempt_backup) {
if (!err.stderr.includes('This video is unavailable') && !err.stderr.includes('Private video')) {
return null;
}
logger.info('An error was encountered with at least one video, backup method will be used.')

View File

@@ -67,9 +67,7 @@ const runYoutubeDLProcess = async (url, args, youtubedl_fork = config_api.getCon
const parsed_output = utils.parseOutputJSON(stdout.trim().split(/\r?\n/), stderr);
resolve({parsed_output, err: stderr});
} catch (e) {
// Attempt to not fail
const parsed_output = utils.parseOutputJSON(e && e.stdout && e.stdout.trim().split(/\r?\n/), e && e.stderr);
resolve({parsed_output: parsed_output, err: parsed_output ? null : e});
resolve({parsed_output: null, err: e})
}
});
return {child_process, callback}

View File

@@ -22,7 +22,7 @@ esac
echo "(INFO) Architecture detected: $ARCH"
echo "(1/5) READY - Install unzip"
apt-get update && apt-get -y install unzip curl jq
apt-get update && apt-get -y install unzip curl jq libicu70
VERSION=$(curl --silent "https://api.github.com/repos/lay295/TwitchDownloader/releases" | jq -r --arg arch "$ARCH" '[.[] | select(.assets | length > 0) | select(.assets[].name | contains("CLI") and contains($arch))] | max_by(.published_at) | .tag_name')
echo "(2/5) DOWNLOAD - Acquire twitchdownloader"
curl -o twitchdownloader.zip \

13639
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,23 +17,23 @@
"i18n-source": "ng extract-i18n --output-path=src/assets/i18n --out-file=messages.en.xlf"
},
"engines": {
"node": "18.19.0",
"npm": "10.2.3"
"node": "12.3.1",
"npm": "6.10.3"
},
"private": true,
"dependencies": {
"@angular-devkit/core": "^17.0.5",
"@angular/animations": "^17.0.5",
"@angular/cdk": "^17.0.2",
"@angular/common": "^17.0.5",
"@angular/compiler": "^17.0.5",
"@angular/core": "^17.0.5",
"@angular/forms": "^17.0.5",
"@angular/localize": "^17.0.5",
"@angular/material": "^17.0.2",
"@angular/platform-browser": "^17.0.5",
"@angular/platform-browser-dynamic": "^17.0.5",
"@angular/router": "^17.0.5",
"@angular-devkit/core": "^15.0.1",
"@angular/animations": "^15.0.1",
"@angular/cdk": "^15.0.0",
"@angular/common": "^15.0.1",
"@angular/compiler": "^15.0.1",
"@angular/core": "^15.0.1",
"@angular/forms": "^15.0.1",
"@angular/localize": "^15.0.1",
"@angular/material": "^15.0.0",
"@angular/platform-browser": "^15.0.1",
"@angular/platform-browser-dynamic": "^15.0.1",
"@angular/router": "^15.0.1",
"@fontsource/material-icons": "^4.5.4",
"@ngneat/content-loader": "^7.0.0",
"@videogular/ngx-videogular": "^6.0.0",
@@ -44,19 +44,20 @@
"fs-extra": "^10.0.0",
"material-icons": "^1.10.8",
"nan": "^2.14.1",
"ngx-avatars": "^1.10.0",
"ngx-avatars": "^1.4.1",
"ngx-file-drop": "^15.0.0",
"rxjs": "^6.6.3",
"rxjs-compat": "^6.6.7",
"tslib": "^2.0.0",
"typescript": "~4.8.4",
"xliff-to-json": "^1.0.4",
"zone.js": "~0.14.2"
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.5",
"@angular/cli": "^17.0.5",
"@angular/compiler-cli": "^17.0.5",
"@angular/language-service": "^17.0.5",
"@angular-devkit/build-angular": "^15.0.1",
"@angular/cli": "^15.0.1",
"@angular/compiler-cli": "^15.0.1",
"@angular/language-service": "^15.0.1",
"@types/core-js": "^2.5.2",
"@types/file-saver": "^2.0.1",
"@types/jasmine": "^4.3.1",
@@ -66,7 +67,7 @@
"ajv": "^7.2.4",
"codelyzer": "^6.0.0",
"eslint": "^7.32.0",
"jasmine-core": "~3.8.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.4.2",
"karma-chrome-launcher": "~3.1.0",
@@ -77,13 +78,12 @@
"openapi-typescript-codegen": "^0.23.0",
"protractor": "~7.0.0",
"ts-node": "~3.0.4",
"tslint": "~6.1.0",
"typescript": "~5.2.0"
"tslint": "~6.1.0"
},
"overrides": {
"ngx-avatars": {
"@angular/common": "^17.0.0",
"@angular/core": "^17.0.0"
"@angular/common": "15.0.1",
"@angular/core": "15.0.1"
}
}
}
}

View File

@@ -1,95 +1,67 @@
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; height: 100%;">
<div class="mat-elevation-z3" style="position: relative; z-index: 10;">
<mat-toolbar class="sticky-toolbar top-toolbar">
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px">
<div class="row" width="100%" height="100%">
<div class="col-6" style="text-align: left; margin-top: 1px;">
<div style="display: flex; align-items: center;">
@if (router.url.split(';')[0] !== '/player') {
<button #hamburgerMenu style="outline: none" mat-icon-button aria-label="Toggle side navigation" (click)="toggleSidenav()"><mat-icon>menu</mat-icon></button>
} @else {
<button (click)="goBack()" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
}
<div style="margin-left: 8px; display: inline-block;"><button mat-icon-button routerLink='/home'><img style="width: 32px;" src="assets/images/logo_128px.png"></button></div>
<div class="mat-elevation-z3" style="position: relative; z-index: 10;">
<mat-toolbar class="sticky-toolbar top-toolbar">
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px">
<div class="row" width="100%" height="100%">
<div class="col-6" style="text-align: left; margin-top: 1px;">
<div style="display: flex; align-items: center;">
<button #hamburgerMenu style="outline: none" *ngIf="router.url.split(';')[0] !== '/player'" mat-icon-button aria-label="Toggle side navigation" (click)="toggleSidenav()"><mat-icon>menu</mat-icon></button>
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
<div style="margin-left: 8px; display: inline-block;"><button mat-icon-button routerLink='/home'><img style="width: 32px;" src="assets/images/logo_128px.png"></button></div>
</div>
</div>
<div class="col-6" style="text-align: right; align-items: flex-end; display: inline-block">
<button *ngIf="postsService.config?.Extra.enable_notifications" [matMenuTriggerFor]="notificationsMenu" (menuOpened)="notificationMenuOpened()" mat-icon-button><mat-icon [matBadge]="notification_count" matBadgeColor="warn" matBadgeSize="small" *ngIf="notification_count > 0">notifications</mat-icon><mat-icon *ngIf="notification_count === 0">notifications_none</mat-icon></button>
<mat-menu [classList]="'notifications-menu'" (close)="notificationMenuClosed()" #notificationsMenu="matMenu">
<app-notifications #notifications (notificationCount)="notificationCountUpdate($event)" (click)="$event.stopPropagation()"></app-notifications>
</mat-menu>
<button [matMenuTriggerFor]="menuSettings" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #menuSettings="matMenu">
<button class="top-menu-button" (click)="openProfileDialog()" mat-menu-item>
<mat-icon>person</mat-icon>
<span i18n="Profile menu label">Profile</span>
</button>
<button *ngIf="!postsService.config?.Advanced.multi_user_mode || postsService.isLoggedIn" class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
<mat-icon>topic</mat-icon>
<span i18n="Archives menu label">Archives</span>
</button>
<button class="top-menu-button" (click)="themeMenuItemClicked($event)" *ngIf="allowThemeChange" mat-menu-item>
<mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon>
<span i18n="Dark mode toggle label">Dark</span>
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
</button>
<button class="top-menu-button" (click)="openAboutDialog()" mat-menu-item>
<mat-icon>info</mat-icon>
<span i18n="About menu label">About</span>
</button>
</mat-menu>
</div>
</div>
</div>
</div>
<div class="col-6" style="text-align: right; align-items: flex-end; display: inline-block">
@if (postsService.config?.Extra.enable_notifications) {
<button [matMenuTriggerFor]="notificationsMenu" (menuOpened)="notificationMenuOpened()" mat-icon-button>
@if (notification_count > 0) {
<mat-icon [matBadge]="notification_count" matBadgeColor="warn" matBadgeSize="small">notifications</mat-icon>
} @else {
<mat-icon>notifications_none</mat-icon>
}</button>
}
<mat-menu [classList]="'notifications-menu'" (close)="notificationMenuClosed()" #notificationsMenu="matMenu">
<app-notifications #notifications (notificationCount)="notificationCountUpdate($event)" (click)="$event.stopPropagation()"></app-notifications>
</mat-menu>
<button [matMenuTriggerFor]="menuSettings" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #menuSettings="matMenu">
<button class="top-menu-button" (click)="openProfileDialog()" mat-menu-item>
<mat-icon>person</mat-icon>
<span i18n="Profile menu label">Profile</span>
</button>
@if (!postsService.config?.Advanced.multi_user_mode || postsService.isLoggedIn) {
<button class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
<mat-icon>topic</mat-icon>
<span i18n="Archives menu label">Archives</span>
</button>
}
@if (allowThemeChange) {
<button class="top-menu-button" (click)="themeMenuItemClicked($event)" mat-menu-item>
<mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon>
<span i18n="Dark mode toggle label">Dark</span>
<mat-slide-toggle class="theme-slide-toggle" [checked]="postsService.theme.key === 'dark'"></mat-slide-toggle>
</button>
}
<button class="top-menu-button" (click)="openAboutDialog()" mat-menu-item>
<mat-icon>info</mat-icon>
<span i18n="About menu label">About</span>
</button>
</mat-menu>
</div>
</div>
</div>
</mat-toolbar>
</div>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
<mat-nav-list>
@if (postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)) {
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
}
@if (postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn) {
<a mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
}
@if (postsService.config && allowSubscriptions && postsService.hasPermission('subscriptions')) {
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
}
@if (postsService.config && enableDownloadsManager && postsService.hasPermission('downloads_manager')) {
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
}
@if (postsService.config && postsService.hasPermission('tasks_manager')) {
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/tasks'><ng-container i18n="Navigation menu Tasks Page title">Tasks</ng-container></a>
}
@if (postsService.config && postsService.hasPermission('settings')) {
<mat-divider></mat-divider>
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/settings'><ng-container i18n="Settings menu label">Settings</ng-container></a>
}
@if (postsService.config && allowSubscriptions && postsService.subscriptions && postsService.hasPermission('subscriptions')) {
@if (postsService.subscriptions.length > 0) {
<mat-divider></mat-divider>
}
@for (subscription of postsService.subscriptions; track subscription) {
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatars [style.display]="'inline-block'" [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatars>{{subscription.name}}</a>
}
}
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>
</div>
</mat-toolbar>
</div>
<div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%">
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
<mat-nav-list>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && postsService.hasPermission('subscriptions')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && postsService.hasPermission('downloads_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<a *ngIf="postsService.config && postsService.hasPermission('tasks_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/tasks'><ng-container i18n="Navigation menu Tasks Page title">Tasks</ng-container></a>
<ng-container *ngIf="postsService.config && postsService.hasPermission('settings')">
<mat-divider></mat-divider>
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/settings'><ng-container i18n="Settings menu label">Settings</ng-container></a>
</ng-container>
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && postsService.hasPermission('subscriptions')">
<mat-divider *ngIf="postsService.subscriptions.length > 0"></mat-divider>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatars [style.display]="'inline-block'" [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatars>{{subscription.name}}</a>
</ng-container>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content [style.background]="postsService.theme ? postsService.theme.background_color : null">
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>
</div>
</div>

View File

@@ -3,68 +3,75 @@
<mat-label i18n="Filter">Filter</mat-label>
<input matInput [(ngModel)]="text_filter" (keyup)="applyFilter($event)" #input>
</mat-form-field>
<div [hidden]="!(archives && archives.length > 0)">
<div class="mat-elevation-z8">
<mat-table matSort [dataSource]="dataSource">
<!-- Select Column -->
<!-- Checkbox Column -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
</mat-checkbox>
<mat-icon class="audio-video-icon">{{(row.type === 'audio') ? 'audiotrack' : 'movie'}}</mat-icon>
</mat-cell>
</ng-container>
<!-- Date Column -->
<ng-container matColumnDef="timestamp">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.timestamp*1000 | date: 'short'}} </mat-cell>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span class="max-two-lines" [matTooltip]="element.title ? element.title : null">
{{element.title}}
</span>
</mat-cell>
</ng-container>
<!-- ID Column -->
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="ID">ID</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span class="one-line" [matTooltip]="element.title ? element.title : null">
{{element.id}}
</span>
</mat-cell>
</ng-container>
<!-- Extractor Column -->
<ng-container matColumnDef="extractor">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Extractor">Extractor</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span class="one-line" [matTooltip]="element.extractor? element.extractor : null">
{{element.extractor}}
</span>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
</div>
<div class="mat-elevation-z8">
<mat-table matSort [dataSource]="dataSource">
<!-- Select Column -->
<!-- Checkbox Column -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
</mat-checkbox>
<mat-icon class="audio-video-icon">{{(row.type === 'audio') ? 'audiotrack' : 'movie'}}</mat-icon>
</mat-cell>
</ng-container>
<!-- Date Column -->
<ng-container matColumnDef="timestamp">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.timestamp*1000 | date: 'short'}} </mat-cell>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span class="max-two-lines" [matTooltip]="element.title ? element.title : null">
{{element.title}}
</span>
</mat-cell>
</ng-container>
<!-- ID Column -->
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="ID">ID</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span class="one-line" [matTooltip]="element.title ? element.title : null">
{{element.id}}
</span>
</mat-cell>
</ng-container>
<!-- Extractor Column -->
<ng-container matColumnDef="extractor">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Extractor">Extractor</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span class="one-line" [matTooltip]="element.extractor? element.extractor : null">
{{element.extractor}}
</span>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
</div>
</div>
@if ((!archives || archives.length === 0)) {
<div>
<h4 style="text-align: center; margin-top: 10px;" i18n="Archives empty">Archives empty</h4>
</div>
}
<div *ngIf="(!archives || archives.length === 0)">
<h4 style="text-align: center; margin-top: 10px;" i18n="Archives empty">Archives empty</h4>
</div>
<div style="margin: 10px 10px 10px 0px; display: flex;">
<span style="flex-grow: 1;" class="flex-items">
<button [disabled]="selection.selected.length === 0" color="warn" style="margin: 10px;" mat-stroked-button i18n="Delete selected" (click)="openDeleteSelectedArchivesDialog()">Delete selected</button>
@@ -75,9 +82,7 @@
<mat-label i18n="Subscription">Subscription</mat-label>
<mat-select [ngModel]="sub_id" (ngModelChange)="subFilterSelectionChanged($event)">
<mat-option [value]="'none'" i18n="None">None</mat-option>
@for (sub of postsService.subscriptions; track sub) {
<mat-option [value]="sub.id">{{sub.name}}</mat-option>
}
<mat-option *ngFor="let sub of postsService.subscriptions" [value]="sub.id">{{sub.name}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field style="width: 100px; margin-bottom: -1.25em; margin-left: 10px;">
@@ -90,55 +95,49 @@
</mat-form-field>
</span>
</div>
<div class="file-drop-parent">
<ngx-file-drop [multiple]="false" accept=".txt" dropZoneLabel="Drop file here" (onFileDrop)="dropped($event)">
<ng-template class="file-drop" ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
<div style="text-align: center">
<div>
<ng-container i18n="Drag and Drop">Drag and Drop</ng-container>
</div>
<div style="margin-top: 6px;">
<button mat-stroked-button (click)="openFileSelector()">Browse Files</button>
</div>
</div>
</ng-template>
<ng-template class="file-drop" ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
<div style="text-align: center">
<div>
<ng-container i18n="Drag and Drop">Drag and Drop</ng-container>
</div>
<div style="margin-top: 6px;">
<button mat-stroked-button (click)="openFileSelector()">Browse Files</button>
</div>
</div>
</ng-template>
</ngx-file-drop>
</div>
<div style="margin-top: 10px; color: white">
<table class="table">
<tbody class="upload-name-style">
@for (item of files; track item; let i = $index) {
<tr>
<td style="vertical-align: middle; border-top: unset">
<strong>{{ item.relativePath }}</strong>
</td>
<td style="border-top: unset">
<div style="float: right">
<mat-form-field style="width: 150px;">
<mat-label i18n="Subscription">Subscription</mat-label>
<mat-select [ngModel]="upload_sub_id" (ngModelChange)="subUploadFilterSelectionChanged($event)">
<mat-option [value]="'none'" i18n="None">None</mat-option>
@for (sub of postsService.subscriptions; track sub) {
<mat-option [value]="sub.id">{{sub.name}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field style="width: 100px; margin-left: 10px">
<mat-label i18n="File type">File type</mat-label>
<mat-select [(ngModel)]="upload_type" [value]="upload_type" [disabled]="upload_sub_id !== 'none'">
<mat-option [value]="'video'" i18n="Video">Video</mat-option>
<mat-option [value]="'audio'" i18n="Audio">Audio</mat-option>
</mat-select>
</mat-form-field>
<button style="margin-left: 10px" [disabled]="uploading_archive || uploaded_archive" (click)="importArchive()" matTooltip="Upload" i18n-matTooltip="Upload" mat-mini-fab><mat-icon>publish</mat-icon>
@if (uploading_archive) {
<mat-spinner class="spinner" [diameter]="38"></mat-spinner>
}
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
<tbody class="upload-name-style">
<tr *ngFor="let item of files; let i=index">
<td style="vertical-align: middle; border-top: unset">
<strong>{{ item.relativePath }}</strong>
</td>
<td style="border-top: unset">
<div style="float: right">
<mat-form-field style="width: 150px;">
<mat-label i18n="Subscription">Subscription</mat-label>
<mat-select [ngModel]="upload_sub_id" (ngModelChange)="subUploadFilterSelectionChanged($event)">
<mat-option [value]="'none'" i18n="None">None</mat-option>
<mat-option *ngFor="let sub of postsService.subscriptions" [value]="sub.id">{{sub.name}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field style="width: 100px; margin-left: 10px">
<mat-label i18n="File type">File type</mat-label>
<mat-select [(ngModel)]="upload_type" [value]="upload_type" [disabled]="upload_sub_id !== 'none'">
<mat-option [value]="'video'" i18n="Video">Video</mat-option>
<mat-option [value]="'audio'" i18n="Audio">Audio</mat-option>
</mat-select>
</mat-form-field>
<button style="margin-left: 10px" [disabled]="uploading_archive || uploaded_archive" (click)="importArchive()" matTooltip="Upload" i18n-matTooltip="Upload" mat-mini-fab><mat-icon>publish</mat-icon><mat-spinner *ngIf="uploading_archive" class="spinner" [diameter]="38"></mat-spinner></button>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -8,7 +8,6 @@ import { Archive } from 'api-types/models/Archive';
import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialog.component';
import { PostsService } from 'app/posts.services';
import { NgxFileDropEntry } from 'ngx-file-drop';
import { saveAs } from 'file-saver';
@Component({
selector: 'app-archive-viewer',

View File

@@ -1,14 +1,6 @@
<div class="buttons-container">
@if (!watch_together_clicked) {
<button (click)="startWatching()" mat-flat-button>Watch together</button>
} @else {
@if (!started) {
@if (server_already_exists) {
<button (click)="startClient()" mat-flat-button>Join stream</button>
} @else if (server_mode) {
<button (click)="startServer()" mat-flat-button>Start stream</button>
}
}
<button style="margin-left: 10px;" (click)="stop()" mat-flat-button>Stop</button>
}
<button (click)="startWatching()" *ngIf="!watch_together_clicked" mat-flat-button>Watch together</button>
<button (click)="startServer()" *ngIf="watch_together_clicked && !started && server_mode && server_already_exists === false" mat-flat-button>Start stream</button>
<button (click)="startClient()" *ngIf="watch_together_clicked && !started && server_already_exists === true" mat-flat-button>Join stream</button>
<button style="margin-left: 10px;" (click)="stop()" *ngIf="watch_together_clicked" mat-flat-button>Stop</button>
</div>

View File

@@ -1,18 +1,13 @@
@if (playlists) {
<div>
<div *ngIf="playlists && playlists.length > 0">
<div class="container">
<div class="row justify-content-center">
@for (playlist of playlists; track playlist; let i = $index) {
<div class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''" [loading]="false"></app-unified-file-card>
</div>
} @empty {
<div style="text-align: center;">
No playlists available. Create one from your downloading files by clicking the blue plus button.
</div>
}
</div>
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''" [loading]="false"></app-unified-file-card>
</div>
</div>
</div>
</div>
}
</div>
<div *ngIf="playlists && playlists.length === 0" style="text-align: center;">
No playlists available. Create one from your downloading files by clicking the blue plus button.
</div>
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog()" mat-fab><mat-icon>add</mat-icon></button></div>

View File

@@ -4,7 +4,6 @@ import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
import { Playlist } from 'api-types';
import { saveAs } from 'file-saver';
@Component({
selector: 'app-custom-playlists',

View File

@@ -1,102 +1,97 @@
<div [hidden]="!(downloads && downloads.length > 0)">
<div style="overflow: hidden;" [ngClass]="uids ? 'rounded mat-elevation-z2' : 'mat-elevation-z8'">
<mat-table style="overflow: hidden" [ngClass]="uids ? 'rounded-top' : null" matSort [dataSource]="dataSource">
<!-- Date Column -->
<ng-container matColumnDef="timestamp_start">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.timestamp_start | date: 'short'}} </mat-cell>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header style="flex: 2"> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element" style="flex: 2">
<span class="one-line" [matTooltip]="element.title ? element.title : null">
{{element.title}}
</span>
</mat-cell>
</ng-container>
<!-- Subscription Column -->
<ng-container matColumnDef="sub_name">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Subscription">Subscription</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
@if (element.sub_name) {
{{element.sub_name}}
} @else {
<ng-container i18n="N/A">N/A</ng-container>
}
</mat-cell>
</ng-container>
<!-- Progress Column -->
<ng-container matColumnDef="percent_complete">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Progress">Progress</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
@if (!element.error) {
@if (element.step_index !== 2) {
{{STEP_INDEX_TO_LABEL[element.step_index]}}
} @else {
@if (element.percent_complete) {
{{+(element.percent_complete) > 100 ? '100' : element.percent_complete}}%
} @else {
<ng-container i18n="N/A">N/A</ng-container>
}
}
} @else {
<ng-container i18n="Error">Error</ng-container>
}
</mat-cell>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef [ngStyle]="{flex: actionsFlex}"> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element" [ngStyle]="{flex: actionsFlex}">
@if (!minimizeButtons) {
<div>
@for (downloadAction of downloadActions; track downloadAction) {
<span class="button-span">
@if (downloadAction.loading && downloadAction.loading(element)) {
<mat-spinner [diameter]="28" class="icon-button-spinner"></mat-spinner>
}
@if (downloadAction.show(element)) {
<button (click)="downloadAction.action(element)" [disabled]="downloadAction.loading && downloadAction.loading(element)" [matTooltip]="downloadAction.tooltip" mat-icon-button><mat-icon>{{downloadAction.icon}}</mat-icon></button>
}
<div style="overflow: hidden;" [ngClass]="uids ? 'rounded mat-elevation-z2' : 'mat-elevation-z8'">
<mat-table style="overflow: hidden" [ngClass]="uids ? 'rounded-top' : null" matSort [dataSource]="dataSource">
<!-- Date Column -->
<ng-container matColumnDef="timestamp_start">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Date">Date</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.timestamp_start | date: 'short'}} </mat-cell>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header style="flex: 2"> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element" style="flex: 2">
<span class="one-line" [matTooltip]="element.title ? element.title : null">
{{element.title}}
</span>
}
</div>
} @else {
<div>
<button [matMenuTriggerFor]="download_actions" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #download_actions="matMenu">
@for (downloadAction of downloadActions; track downloadAction) {
@if (downloadAction.show(element)) {
<button (click)="downloadAction.action(element)" [disabled]="downloadAction.loading && downloadAction.loading(element)" mat-menu-item>
<mat-icon>{{downloadAction.icon}}</mat-icon>
<span>{{downloadAction.tooltip}}</span>
</button>
}
}
</mat-menu>
</div>
}
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="uids ? 'rounded-top' : null" *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<mat-paginator [ngClass]="uids ? 'rounded-bottom' : null" [pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
aria-label="Select page of downloads">
</mat-paginator>
</div>
@if (!uids) {
<div class="downloads-action-button-div">
<button class="downloads-action-button" [disabled]="!running_download_exists" mat-stroked-button (click)="pauseAllDownloads()"><ng-container i18n="Pause all downloads">Pause all downloads</ng-container></button>
<button class="downloads-action-button" [disabled]="!paused_download_exists" mat-stroked-button (click)="resumeAllDownloads()"><ng-container i18n="Resume all downloads">Resume all downloads</ng-container></button>
<button class="downloads-action-button" color="warn" mat-stroked-button (click)="clearDownloadsByType()"><ng-container i18n="Clear downloads">Clear downloads</ng-container></button>
</div>
}
</mat-cell>
</ng-container>
<!-- Subscription Column -->
<ng-container matColumnDef="sub_name">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Subscription">Subscription</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<ng-container *ngIf="element.sub_name">
{{element.sub_name}}
</ng-container>
<ng-container *ngIf="!element.sub_name">
N/A
</ng-container>
</mat-cell>
</ng-container>
<!-- Progress Column -->
<ng-container matColumnDef="percent_complete">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Progress">Progress</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<ng-container *ngIf="!element.error && element.step_index !== 2">
{{STEP_INDEX_TO_LABEL[element.step_index]}}
</ng-container>
<ng-container *ngIf="!element.error && element.step_index === 2">
<ng-container *ngIf="element.percent_complete">
{{+(element.percent_complete) > 100 ? '100' : element.percent_complete}}%
</ng-container>
<ng-container *ngIf="!element.percent_complete">
N/A
</ng-container>
</ng-container>
<ng-container *ngIf="element.error" i18n="Error">Error</ng-container>
</mat-cell>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef [ngStyle]="{flex: actionsFlex}"> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element" [ngStyle]="{flex: actionsFlex}">
<div *ngIf="!minimizeButtons">
<ng-container *ngFor="let downloadAction of downloadActions">
<span class="button-span">
<mat-spinner [diameter]="28" *ngIf="downloadAction.loading && downloadAction.loading(element)" class="icon-button-spinner"></mat-spinner>
<button *ngIf="downloadAction.show(element)" (click)="downloadAction.action(element)" [disabled]="downloadAction.loading && downloadAction.loading(element)" [matTooltip]="downloadAction.tooltip" mat-icon-button><mat-icon>{{downloadAction.icon}}</mat-icon></button>
</span>
</ng-container>
</div>
<div *ngIf="minimizeButtons">
<button [matMenuTriggerFor]="download_actions" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #download_actions="matMenu">
<ng-container *ngFor="let downloadAction of downloadActions">
<button *ngIf="downloadAction.show(element)" (click)="downloadAction.action(element)" [disabled]="downloadAction.loading && downloadAction.loading(element)" mat-menu-item>
<mat-icon>{{downloadAction.icon}}</mat-icon>
<span>{{downloadAction.tooltip}}</span>
</button>
</ng-container>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="uids ? 'rounded-top' : null" *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<mat-paginator [ngClass]="uids ? 'rounded-bottom' : null" [pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
aria-label="Select page of downloads">
</mat-paginator>
</div>
<div *ngIf="!uids" class="downloads-action-button-div">
<button class="downloads-action-button" [disabled]="!running_download_exists" mat-stroked-button (click)="pauseAllDownloads()"><ng-container i18n="Pause all downloads">Pause all downloads</ng-container></button>
<button class="downloads-action-button" [disabled]="!paused_download_exists" mat-stroked-button (click)="resumeAllDownloads()"><ng-container i18n="Resume all downloads">Resume all downloads</ng-container></button>
<button class="downloads-action-button" color="warn" mat-stroked-button (click)="clearDownloadsByType()"><ng-container i18n="Clear downloads">Clear downloads</ng-container></button>
</div>
</div>
@if ((!downloads || downloads.length === 0) && downloads_retrieved && !uids) {
<div>
<h4 style="text-align: center; margin-top: 10px;" i18n="No downloads label">No downloads available!</h4>
</div>
}
<div *ngIf="(!downloads || downloads.length === 0) && downloads_retrieved && !uids">
<h4 style="text-align: center; margin-top: 10px;" i18n="No downloads label">No downloads available!</h4>
</div>

View File

@@ -1,55 +1,46 @@
<mat-card class="login-card">
<mat-tab-group mat-stretch-tabs style="margin-bottom: 20px" [(selectedIndex)]="selectedTabIndex">
<mat-tab label="Login" i18n-label="Login">
<div style="margin-top: 10px;">
<mat-form-field style="width: 100%">
<mat-label i18n="User name">User name</mat-label>
<input [(ngModel)]="loginUsernameInput" matInput>
</mat-form-field>
</div>
<div>
<mat-form-field style="width: 100%">
<mat-label i18n="Password">Password</mat-label>
<input [(ngModel)]="loginPasswordInput" (keyup.enter)="login()" type="password" matInput>
</mat-form-field>
</div>
</mat-tab>
@if (registrationEnabled) {
<mat-tab label="Register" i18n-label="Register">
<div style="margin-top: 10px;">
<mat-form-field style="width: 100%">
<mat-label i18n="User name">User name</mat-label>
<input [(ngModel)]="registrationUsernameInput" matInput>
</mat-form-field>
</div>
<div>
<mat-form-field style="width: 100%">
<mat-label i18n="Password">Password</mat-label>
<input [(ngModel)]="registrationPasswordInput" (click)="register()" type="password" matInput>
</mat-form-field>
</div>
<div>
<mat-form-field style="width: 100%">
<mat-label i18n="Confirm Password">Confirm Password</mat-label>
<input [(ngModel)]="registrationPasswordConfirmationInput" (click)="register()" type="password" matInput>
</mat-form-field>
</div>
</mat-tab>
}
</mat-tab-group>
@if (selectedTabIndex === 0) {
<div class="login-button-div">
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
@if (loggingIn) {
<mat-progress-bar class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
}
<mat-tab-group mat-stretch-tabs style="margin-bottom: 20px" [(selectedIndex)]="selectedTabIndex">
<mat-tab label="Login" i18n-label="Login">
<div style="margin-top: 10px;">
<mat-form-field style="width: 100%">
<mat-label i18n="User name">User name</mat-label>
<input [(ngModel)]="loginUsernameInput" matInput>
</mat-form-field>
</div>
<div>
<mat-form-field style="width: 100%">
<mat-label i18n="Password">Password</mat-label>
<input [(ngModel)]="loginPasswordInput" (keyup.enter)="login()" type="password" matInput>
</mat-form-field>
</div>
</mat-tab>
<mat-tab *ngIf="registrationEnabled" label="Register" i18n-label="Register">
<div style="margin-top: 10px;">
<mat-form-field style="width: 100%">
<mat-label i18n="User name">User name</mat-label>
<input [(ngModel)]="registrationUsernameInput" matInput>
</mat-form-field>
</div>
<div>
<mat-form-field style="width: 100%">
<mat-label i18n="Password">Password</mat-label>
<input [(ngModel)]="registrationPasswordInput" (click)="register()" type="password" matInput>
</mat-form-field>
</div>
<div>
<mat-form-field style="width: 100%">
<mat-label i18n="Confirm Password">Confirm Password</mat-label>
<input [(ngModel)]="registrationPasswordConfirmationInput" (click)="register()" type="password" matInput>
</mat-form-field>
</div>
</mat-tab>
</mat-tab-group>
<div *ngIf="selectedTabIndex === 0" class="login-button-div">
<button [disabled]="loggingIn" color="primary" (click)="login()" mat-raised-button><ng-container i18n="Login">Login</ng-container></button>
<mat-progress-bar *ngIf="loggingIn" class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
</div>
} @else {
<div class="login-button-div">
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
@if (registering) {
<mat-progress-bar class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
}
<div *ngIf="selectedTabIndex === 1" class="login-button-div">
<button [disabled]="registering" color="primary" (click)="register()" mat-raised-button><ng-container i18n="Register">Register</ng-container></button>
<mat-progress-bar *ngIf="registering" class="login-progress-bar" mode="indeterminate"></mat-progress-bar>
</div>
}
</mat-card>

View File

@@ -1,38 +1,37 @@
<div style="height: 100%;">
@if (logs_loading) {
<div style="z-index: 999; position: absolute; top: 40%; left: 50%">
<mat-spinner [diameter]="32"></mat-spinner>
<div *ngIf="logs_loading" style="z-index: 999; position: absolute; top: 40%; left: 50%">
<mat-spinner [diameter]="32"></mat-spinner>
</div>
}
<!-- Virtual mode (fast, select text buggy) -->
<!--<cdk-virtual-scroll-viewport style="height: 274px;" itemSize="50" class="example-viewport">
<div *cdkVirtualFor="let log of logs; let i = index" class="example-item">
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
</div>
</cdk-virtual-scroll-viewport>-->
<!-- Non-virtual mode (slow, bug-free) -->
<div style="height: 100%; overflow-y: auto">
@for (log of logs; track log) {
<div class="example-item">
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
<!-- Virtual mode (fast, select text buggy) -->
<!--<cdk-virtual-scroll-viewport style="height: 274px;" itemSize="50" class="example-viewport">
<div *cdkVirtualFor="let log of logs; let i = index" class="example-item">
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
</div>
</cdk-virtual-scroll-viewport>-->
<!-- Non-virtual mode (slow, bug-free) -->
<div style="height: 100%; overflow-y: auto">
<div *ngFor="let log of logs; let i = index" class="example-item">
<span [ngStyle]="{'color':log.color}">{{log.text}}</span>
</div>
</div>
}
</div>
<div>
<button style="position: absolute; right: 0px; top: 12px;" [cdkCopyToClipboard]="logs_text" (click)="copiedLogsToClipboard()" mat-mini-fab color="primary"><mat-icon style="font-size: 22px !important;">content_copy</mat-icon></button>
<div style="display: inline-block;">
<ng-container i18n="Label for lines select in logger view">Lines:</ng-container>&nbsp;
<mat-form-field style="width: 75px;">
<mat-select (selectionChange)="getLogs()" [(ngModel)]="requested_lines">
<mat-option [value]="10">10</mat-option>
<mat-option [value]="25">25</mat-option>
<mat-option [value]="50">50</mat-option>
<mat-option [value]="100">100</mat-option>
<mat-option [value]="0">All</mat-option>
</mat-select>
</mat-form-field>
</div>
<span class="spacer"></span>
<button style="float: right; margin-top: 12px;" (click)="clearLogs()" mat-stroked-button color="warn"><ng-container i18n="Clear logs button">Clear logs</ng-container></button>
</div>
<div>
<button style="position: absolute; right: 0px; top: 12px;" [cdkCopyToClipboard]="logs_text" (click)="copiedLogsToClipboard()" mat-mini-fab color="primary"><mat-icon style="font-size: 22px !important;">content_copy</mat-icon></button>
<div style="display: inline-block;">
<ng-container i18n="Label for lines select in logger view">Lines:</ng-container>&nbsp;
<mat-form-field style="width: 75px;">
<mat-select (selectionChange)="getLogs()" [(ngModel)]="requested_lines">
<mat-option [value]="10">10</mat-option>
<mat-option [value]="25">25</mat-option>
<mat-option [value]="50">50</mat-option>
<mat-option [value]="100">100</mat-option>
<mat-option [value]="0">All</mat-option>
</mat-select>
</mat-form-field>
</div>
<span class="spacer"></span>
<button style="float: right; margin-top: 12px;" (click)="clearLogs()" mat-stroked-button color="warn"><ng-container i18n="Clear logs button">Clear logs</ng-container></button>
</div>
</div>

View File

@@ -1,19 +1,17 @@
@if (role) {
<h4 mat-dialog-title><ng-container i18n="Manage role dialog title">Manage role</ng-container>&nbsp;-&nbsp;{{role.key}}</h4>
<mat-dialog-content>
@for (permission of available_permissions; track permission) {
<div>
<h4 *ngIf="role" mat-dialog-title><ng-container i18n="Manage role dialog title">Manage role</ng-container>&nbsp;-&nbsp;{{role.key}}</h4>
<mat-dialog-content *ngIf="role">
<div *ngFor="let permission of available_permissions">
<div matListItemTitle>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</div>
<div matListItemLine>
<mat-radio-group [disabled]="permission === 'settings' && role.key === 'admin'" (change)="changeRolePermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>
<mat-radio-group [disabled]="permission === 'settings' && role.key === 'admin'" (change)="changeRolePermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give role permission for ' + permission">
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>
</div>
</div>
}
</mat-dialog-content>
}
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,30 +1,30 @@
@if (user) {
<h4 mat-dialog-title><ng-container i18n="Manage user dialog title">Manage user</ng-container>&nbsp;-&nbsp;{{user.name}}</h4>
<mat-dialog-content>
<h4 *ngIf="user" mat-dialog-title><ng-container i18n="Manage user dialog title">Manage user</ng-container>&nbsp;-&nbsp;{{user.name}}</h4>
<mat-dialog-content *ngIf="user">
<p><ng-container i18n="User UID">User UID:</ng-container>&nbsp;{{user.uid}}</p>
<div>
<mat-form-field style="margin-right: 15px;">
<mat-label i18n="New password">New password</mat-label>
<input matInput [(ngModel)]="newPasswordInput" type="password">
</mat-form-field>
<button mat-raised-button color="accent" (click)="setNewPassword()" [disabled]="newPasswordInput.length === 0"><ng-container i18n="Set new password">Set new password</ng-container></button>
<mat-form-field style="margin-right: 15px;">
<mat-label i18n="New password">New password</mat-label>
<input matInput [(ngModel)]="newPasswordInput" type="password">
</mat-form-field>
<button mat-raised-button color="accent" (click)="setNewPassword()" [disabled]="newPasswordInput.length === 0"><ng-container i18n="Set new password">Set new password</ng-container></button>
</div>
<div>
@for (permission of available_permissions; track permission) {
<div>
<div matListItemTitle>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</div>
<div matListItemLine>
<mat-radio-group [disabled]="permission === 'settings' && postsService.user.uid === user.uid" (change)="changeUserPermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give user permission for ' + permission">
<mat-radio-button value="default"><ng-container i18n="Use role default">Use role default</ng-container></mat-radio-button>
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>
</div>
<div *ngFor="let permission of available_permissions">
<div matListItemTitle>{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}</div>
<div matListItemLine>
<mat-radio-group [disabled]="permission === 'settings' && postsService.user.uid === user.uid" (change)="changeUserPermissions($event, permission)" [(ngModel)]="permissions[permission]" [attr.aria-label]="'Give user permission for ' + permission">
<mat-radio-button value="default"><ng-container i18n="Use role default">Use role default</ng-container></mat-radio-button>
<mat-radio-button value="yes"><ng-container i18n="Yes">Yes</ng-container></mat-radio-button>
<mat-radio-button value="no"><ng-container i18n="No">No</ng-container></mat-radio-button>
</mat-radio-group>
</div>
</div>
}
</div>
</mat-dialog-content>
}
</mat-dialog-content>
<mat-dialog-actions>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,103 +1,108 @@
@if (dataSource) {
<div>
<div *ngIf="dataSource; else loading">
<div style="padding: 15px">
<div class="row">
<div class="row">
<div class="table table-responsive pb-4 pt-4">
<div class="example-header">
<div class="example-header">
<mat-form-field appearance="outline">
<mat-label i18n="Search">Search</mat-label>
<input matInput (keyup)="applyFilter($event)">
<mat-label i18n="Search">Search</mat-label>
<input matInput (keyup)="applyFilter($event)">
</mat-form-field>
</div>
<div class="mat-elevation-z8" style="margin-right: 15px;">
</div>
<div class="mat-elevation-z8" style="margin-right: 15px;">
<mat-table #table [dataSource]="dataSource" matSort>
<!-- Name Column -->
<ng-container matColumnDef="name">
<!-- Name Column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Username users table header"> User name </ng-container></mat-header-cell>
<mat-cell *matCellDef="let row">
@if (editObject && editObject.uid === row.uid) {
<span>
<span style="width: 80%;">
<mat-form-field>
<input matInput [(ngModel)]="constructedObject['name']" type="text" style="font-size: 12px">
</mat-form-field>
</span>
<span *ngIf="editObject && editObject.uid === row.uid; else noteditingname">
<span style="width: 80%;">
<mat-form-field>
<input matInput [(ngModel)]="constructedObject['name']" type="text" style="font-size: 12px">
</mat-form-field>
</span>
} @else {
{{row.name}}
}
</span>
<ng-template #noteditingname>
{{row.name}}
</ng-template>
</mat-cell>
</ng-container>
<!-- Email Column -->
<ng-container matColumnDef="role">
</ng-container>
<!-- Email Column -->
<ng-container matColumnDef="role">
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Role users table header"> Role </ng-container></mat-header-cell>
<mat-cell *matCellDef="let row">
@if (editObject && editObject.uid === row.uid) {
<span>
<span style="width: 80%;">
<span *ngIf="editObject && editObject.uid === row.uid; else noteditingemail">
<span style="width: 80%;">
<mat-form-field>
<mat-select [(ngModel)]="constructedObject['role']">
<mat-option value="admin">Admin</mat-option>
<mat-option value="user">User</mat-option>
</mat-select>
<mat-select [(ngModel)]="constructedObject['role']">
<mat-option value="admin">Admin</mat-option>
<mat-option value="user">User</mat-option>
</mat-select>
</mat-form-field>
</span>
</span>
} @else {
{{row.role}}
}
</span>
<ng-template #noteditingemail>
{{row.role}}
</ng-template>
</mat-cell>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef mat-sort-header><ng-container i18n="Actions users table header"> Actions </ng-container></mat-header-cell>
<mat-cell *matCellDef="let row">
@if (editObject && editObject.uid === row.uid) {
<span>
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Save" i18n-matTooltip="save user edit action button tooltip">
<mat-icon>done</mat-icon>
</button>
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel" i18n-matTooltip="cancel user edit action button tooltip">
<mat-icon>cancel</mat-icon>
</button>
</span>
} @else {
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user" i18n-matTooltip="edit user action button tooltip">
<mat-icon>edit</mat-icon>
<span *ngIf="editObject && editObject.uid === row.uid; else notediting">
<button mat-icon-button color="primary" (click)="finishEditing(row.uid)" matTooltip="Save" i18n-matTooltip="save user edit action button tooltip">
<mat-icon>done</mat-icon>
</button>
}
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user" i18n-matTooltip="manage user action button tooltip">
<button mat-icon-button (click)="disableEditMode()" matTooltip="Cancel" i18n-matTooltip="cancel user edit action button tooltip">
<mat-icon>cancel</mat-icon>
</button>
</span>
<ng-template #notediting>
<button mat-icon-button (click)="enableEditMode(row.uid)" matTooltip="Edit user" i18n-matTooltip="edit user action button tooltip">
<mat-icon>edit</mat-icon>
</button>
</ng-template>
<button (click)="manageUser(row.uid)" mat-icon-button [disabled]="editObject && editObject.uid === row.uid" matTooltip="Manage user" i18n-matTooltip="manage user action button tooltip">
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button [disabled]="editObject && editObject.uid === row.uid || row.uid === postsService.user.uid" (click)="removeUser(row.uid)" matTooltip="Delete user" i18n-matTooltip="delete user action button tooltip">
</button>
<button mat-icon-button [disabled]="editObject && editObject.uid === row.uid || row.uid === postsService.user.uid" (click)="removeUser(row.uid)" matTooltip="Delete user" i18n-matTooltip="delete user action button tooltip">
<mat-icon>delete</mat-icon>
</button>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;">
</mat-row>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;">
</mat-row>
</mat-table>
<mat-paginator #paginator [length]="length"
[pageSize]="pageSize"
[pageSizeOptions]="pageSizeOptions">
[pageSize]="pageSize"
[pageSizeOptions]="pageSizeOptions">
</mat-paginator>
<button color="primary" [disabled]="!this.users" mat-raised-button (click)="openAddUserDialog()" style="float: left; top: -45px; left: 15px">
<ng-container i18n="Add users button">Add Users</ng-container>
<ng-container i18n="Add users button">Add Users</ng-container>
</button>
</div>
</div>
</div>
<button color="primary" [matMenuTriggerFor]="edit_roles_menu" class="edit-role" mat-raised-button><ng-container i18n="Edit role">Edit Role</ng-container></button>
<mat-menu #edit_roles_menu="matMenu">
@for (role of roles; track role) {
<button (click)="openModifyRole(role)" mat-menu-item>{{role.key}}</button>
}
</mat-menu>
</div>
</div>
<button color="primary" [matMenuTriggerFor]="edit_roles_menu" class="edit-role" mat-raised-button><ng-container i18n="Edit role">Edit Role</ng-container></button>
<mat-menu #edit_roles_menu="matMenu">
<button (click)="openModifyRole(role)" mat-menu-item *ngFor="let role of roles">{{role.key}}</button>
</mat-menu>
</div>
</div>
} @else {
<div style="position: absolute" class="centered">
<mat-spinner></mat-spinner>
</div>
}
<div style="position: absolute" class="centered">
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>
</div>

View File

@@ -1,38 +1,32 @@
<cdk-virtual-scroll-viewport itemSize="50" class="viewport" minBufferPx="1200" maxBufferPx="1200">
<div #notification_parent class="notification-card-parent card-radius mat-elevation-z2" *cdkVirtualFor="let notification of notifications; let i = index;">
<mat-card class="notification-card card-radius">
<mat-card-header>
<mat-card-subtitle>
<div>
<span class="notification-timestamp">{{notification.timestamp * 1000 | date:'short'}}</span>
</div>
</mat-card-subtitle>
<mat-card-title>
@if (NOTIFICATION_PREFIX[notification.type]) {
{{NOTIFICATION_PREFIX[notification.type]}}
}
</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (NOTIFICATION_SUFFIX_KEY[notification.type]) {
<div style="word-break: break-word">
{{notification['data'][NOTIFICATION_SUFFIX_KEY[notification.type]]}}
</div>
}
</mat-card-content>
@if (notification.actions?.length > 0) {
<mat-card-actions class="notification-actions">
<button matTooltip="Remove" i18n-matTooltip="Remove" (click)="emitDeleteNotification(notification.uid)" mat-icon-button><mat-icon>close</mat-icon></button>
@for (action of notification.actions; track action) {
<span>
<button [matTooltip]="NOTIFICATION_ACTION_TO_STRING[action]" (click)="emitNotificationAction(notification, action)" mat-icon-button><mat-icon>{{NOTIFICATION_ICON[action]}}</mat-icon></button>
</span>
}
</mat-card-actions>
}
@if (!notification.read) {
<span class="dot"></span>
}
</mat-card>
</div>
<div #notification_parent class="notification-card-parent card-radius mat-elevation-z2" *cdkVirtualFor="let notification of notifications; let i = index;">
<mat-card class="notification-card card-radius">
<mat-card-header>
<mat-card-subtitle>
<div>
<span class="notification-timestamp">{{notification.timestamp * 1000 | date:'short'}}</span>
</div>
</mat-card-subtitle>
<mat-card-title>
<ng-container *ngIf="NOTIFICATION_PREFIX[notification.type]">
{{NOTIFICATION_PREFIX[notification.type]}}
</ng-container>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<ng-container *ngIf="NOTIFICATION_SUFFIX_KEY[notification.type]">
<div style="word-break: break-word">
{{notification['data'][NOTIFICATION_SUFFIX_KEY[notification.type]]}}
</div>
</ng-container>
</mat-card-content>
<mat-card-actions class="notification-actions" *ngIf="notification.actions?.length > 0">
<button matTooltip="Remove" i18n-matTooltip="Remove" (click)="emitDeleteNotification(notification.uid)" mat-icon-button><mat-icon>close</mat-icon></button>
<span *ngFor="let action of notification.actions">
<button [matTooltip]="NOTIFICATION_ACTION_TO_STRING[action]" (click)="emitNotificationAction(notification, action)" mat-icon-button><mat-icon>{{NOTIFICATION_ICON[action]}}</mat-icon></button>
</span>
</mat-card-actions>
<span *ngIf="!notification.read" class="dot"></span>
</mat-card>
</div>
</cdk-virtual-scroll-viewport>

View File

@@ -1,16 +1,10 @@
@if (notifications !== null && notifications.length === 0) {
<div style="text-align: center; margin: 10px;" i18n="No notifications available">No notifications available</div>
}
@if (notifications?.length > 0) {
<div>
<div *ngIf="notifications !== null && notifications.length === 0" style="text-align: center; margin: 10px;" i18n="No notifications available">No notifications available</div>
<div *ngIf="notifications?.length > 0">
<div class="notifications-list-parent">
<mat-chip-listbox [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
@for (filter of notificationFilters | keyvalue: originalOrder; track filter) {
<mat-chip-option [value]="filter.key" [selected]="selectedFilters.includes(filter.key)" color="accent">{{filter.value.label}}</mat-chip-option>
}
</mat-chip-listbox>
<app-notifications-list class="notifications-list" [style.height]="list_height" (notificationAction)="notificationAction($event)" (deleteNotification)="deleteNotification($event)" [notifications]="filtered_notifications"></app-notifications-list>
<mat-chip-listbox [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
<mat-chip-option *ngFor="let filter of notificationFilters | keyvalue: originalOrder" [value]="filter.key" [selected]="selectedFilters.includes(filter.key)" color="accent">{{filter.value.label}}</mat-chip-option>
</mat-chip-listbox>
<app-notifications-list class="notifications-list" [style.height]="list_height" (notificationAction)="notificationAction($event)" (deleteNotification)="deleteNotification($event)" [notifications]="filtered_notifications"></app-notifications-list>
</div>
<button style="margin: 10px 0px 2px 10px;" color="warn" (click)="deleteAllNotifications()" mat-stroked-button>Remove all</button>
</div>
}
<button style="margin: 10px 0px 2px 10px;" *ngIf="notifications?.length > 0" color="warn" (click)="deleteAllNotifications()" mat-stroked-button>Remove all</button>
</div>

View File

@@ -1,134 +1,105 @@
<div class="container-fluid" style="max-width: 941px;">
<div class="row">
<!-- Sorting -->
<div class="col-12 order-2 col-sm-4 order-sm-1 d-flex justify-content-center">
<app-sort-property [(sortProperty)]="sortProperty" [(descendingMode)]="descendingMode" (sortOptionChanged)="sortOptionChanged($event)"></app-sort-property>
<div class="row">
<!-- Sorting -->
<div class="col-12 order-2 col-sm-4 order-sm-1 d-flex justify-content-center">
<app-sort-property [(sortProperty)]="sortProperty" [(descendingMode)]="descendingMode" (sortOptionChanged)="sortOptionChanged($event)"></app-sort-property>
</div>
<!-- Files title -->
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
<h4 *ngIf="!customHeader" class="my-videos-title" i18n="My files title">My files</h4>
<h4 *ngIf="customHeader" class="my-videos-title">{{customHeader}}</h4>
</div>
<!-- Search -->
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
<mat-form-field appearance="outline" [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
<mat-label i18n="Search">Search</mat-label>
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
</div>
<!-- Files title -->
<div class="col-12 order-1 col-sm-4 order-sm-2 d-flex justify-content-center">
@if (!customHeader) {
<h4 class="my-videos-title" i18n="My files title">My files</h4>
} @else {
<h4 class="my-videos-title">{{customHeader}}</h4>
}
<!-- Filters -->
<div class="row justify-content-center">
<mat-chip-listbox class="filter-list" [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
<mat-chip-option *ngFor="let filter of fileFilters | keyvalue: originalOrder" [value]="filter.key" [selected]="selectedFilters.includes(filter.key)" color="accent">{{filter.value.label}}</mat-chip-option>
</mat-chip-listbox>
</div>
<!-- Search -->
<div class="col-12 order-3 col-sm-4 order-sm-3 d-flex justify-content-center">
<mat-form-field appearance="outline" [ngClass]="searchIsFocused ? 'search-bar-focused' : 'search-bar-unfocused'" class="search-bar" color="accent">
<mat-label i18n="Search">Search</mat-label>
<input (focus)="searchIsFocused = true" (blur)="searchIsFocused = false" class="search-input" type="text" [(ngModel)]="search_text" (ngModelChange)="onSearchInputChanged($event)" matInput>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
</div>
<!-- Filters -->
<div class="row justify-content-center">
<mat-chip-listbox class="filter-list" [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
@for (filter of fileFilters | keyvalue: originalOrder; track filter) {
<mat-chip-option [value]="filter.key" [selected]="selectedFilters.includes(filter.key)" color="accent">{{filter.value.label}}</mat-chip-option>
}
</mat-chip-listbox>
</div>
</div>
<div>
<!-- Files -->
@if (!selectMode) {
<div class="container" style="margin-bottom: 16px">
<div class="row justify-content-center">
<!-- Real cards -->
@if (normal_files_received && paged_data) {
@for (file of paged_data; track file; let i = $index) {
<div style="display: flex; align-items: center;" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [ngClass]="downloading_content[file.uid] ? 'blurred' : ''" [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" (toggleFavorite)="toggleFavorite($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card>
@if (downloading_content[file.uid]) {
<mat-spinner class="downloading-spinner" [diameter]="32"></mat-spinner>
}
</div>
} @empty {
<div>
<ng-container i18n="No files found">No files found.</ng-container>
</div>
}
}
<!-- Fake cards -->
<ng-container>
@for (file of loading_files; track file; let i = $index) {
<div class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[normal_files_received ? 'hide' : '', postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
</div>
}
</ng-container>
</div>
<!-- Files -->
<div *ngIf="!selectMode" class="container" style="margin-bottom: 16px">
<div class="row justify-content-center">
<!-- Real cards -->
<ng-container *ngIf="normal_files_received && paged_data">
<div style="display: flex; align-items: center;" *ngFor="let file of paged_data; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [ngClass]="downloading_content[file.uid] ? 'blurred' : ''" [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" (toggleFavorite)="toggleFavorite($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [availablePlaylists]="playlists" (addFileToPlaylist)="addFileToPlaylist($event)" [loading]="false" (deleteFile)="deleteFile($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''"></app-unified-file-card>
<mat-spinner *ngIf="downloading_content[file.uid]" class="downloading-spinner" [diameter]="32"></mat-spinner>
</div>
<div *ngIf="paged_data.length === 0">
<ng-container i18n="No files found">No files found.</ng-container>
</div>
</ng-container>
<!-- Fake cards -->
<ng-container>
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[normal_files_received ? 'hide' : '', postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
</div>
</ng-container>
</div>
</div>
} @else {
<div>
<!-- If selected files e.g. for creating a playlist -->
<mat-tab-group [(selectedIndex)]="selectedIndex">
<mat-tab label="Order" i18n-label="Order">
@if (selected_data.length) {
<div>
@if (reverse_order === false) {
<span i18n="Normal order">Normal order&nbsp;</span>
}
@if (reverse_order === true) {
<span i18n="Reverse order">Reverse order&nbsp;</span>
}
<button (click)="toggleSelectionOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
}
<!-- Selection order -->
@if (selected_data.length) {
<mat-button-toggle-group class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical #group="matButtonToggleGroup">
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
@for (file of (reverse_order ? selected_data_objs.slice().reverse() : selected_data_objs); track file; let i = $index) {
<mat-button-toggle class="media-box" cdkDrag [checked]="false"><div><div class="playlist-item-text">{{file.title}}</div> <button (click)="removeSelectedFile(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
}
</mat-button-toggle-group>
} @else {
<div style="margin-top: 20px;">
<h4 style="text-align: center;">No files selected!</h4>
</div>
}
</mat-tab>
<mat-tab label="Select files" i18n-label="Select files">
@if (normal_files_received) {
<mat-selection-list (selectionChange)="fileSelectionChanged($event)">
@for (file of paged_data; track file) {
<mat-list-option [selected]="selected_data.includes(file.uid)" [value]="file">
<div class="container">
<div class="row justify-content-center">
<div class="col-10 select-file-title">
<mat-icon class="audio-video-icon">{{file.isAudio ? 'audiotrack' : 'movie'}}</mat-icon>
{{file.title}}
</div>
<div class="col-2">{{file.registered | date:'shortDate'}}</div>
</div>
</div>
</mat-list-option>
}
</mat-selection-list>
}
@if (!normal_files_received && loading_files && loading_files.length > 0) {
@if (!normal_files_received) {
<mat-selection-list>
@for (file of paged_data; track file) {
<mat-list-option>
<content-loader class="list-ghosts" [backgroundColor]="postsService.theme.ghost_primary" [foregroundColor]="postsService.theme.ghost_secondary" viewBox="0 0 250 8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
</mat-list-option>
}
</mat-selection-list>
}
}
</mat-tab>
</mat-tab-group>
<div *ngIf="selectMode">
<!-- If selected files e.g. for creating a playlist -->
<mat-tab-group [(selectedIndex)]="selectedIndex">
<mat-tab label="Order" i18n-label="Order">
<div *ngIf="selected_data.length">
<span *ngIf="reverse_order === false" i18n="Normal order">Normal order&nbsp;</span>
<span *ngIf="reverse_order === true" i18n="Reverse order">Reverse order&nbsp;</span>
<button (click)="toggleSelectionOrder()" mat-icon-button><mat-icon>{{!reverse_order ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
<!-- Selection order -->
<mat-button-toggle-group *ngIf="selected_data.length" class="media-list" cdkDropList (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical #group="matButtonToggleGroup">
<!-- The following for loop can be optimized but it requires a pipe https://stackoverflow.com/a/35703364/8088021 -->
<mat-button-toggle class="media-box" cdkDrag *ngFor="let file of (reverse_order ? selected_data_objs.slice().reverse() : selected_data_objs); let i = index" [checked]="false"><div><div class="playlist-item-text">{{file.title}}</div> <button (click)="removeSelectedFile(i)" class="remove-item-button" mat-icon-button><mat-icon>cancel</mat-icon></button></div></mat-button-toggle>
</mat-button-toggle-group>
<div style="margin-top: 20px;" *ngIf="!selected_data.length">
<h4 style="text-align: center;">No files selected!</h4>
</div>
</mat-tab>
<mat-tab label="Select files" i18n-label="Select files">
<mat-selection-list *ngIf="normal_files_received" (selectionChange)="fileSelectionChanged($event)">
<mat-list-option [selected]="selected_data.includes(file.uid)" *ngFor="let file of paged_data" [value]="file">
<div class="container">
<div class="row justify-content-center">
<div class="col-10 select-file-title">
<mat-icon class="audio-video-icon">{{file.isAudio ? 'audiotrack' : 'movie'}}</mat-icon>
{{file.title}}
</div>
<div class="col-2">{{file.registered | date:'shortDate'}}</div>
</div>
</div>
</mat-list-option>
</mat-selection-list>
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<mat-selection-list *ngIf="!normal_files_received">
<mat-list-option *ngFor="let file of paged_data">
<content-loader class="list-ghosts" [backgroundColor]="postsService.theme.ghost_primary" [foregroundColor]="postsService.theme.ghost_secondary" viewBox="0 0 250 8"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="8" /></content-loader>
</mat-list-option>
</mat-selection-list>
</ng-container>
</mat-tab>
</mat-tab-group>
</div>
}
@if (usePaginator && selectedIndex > 0) {
<div style="position: relative;">
<mat-paginator class="paginator" #paginator (page)="pageChangeEvent($event)" [length]="file_count"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">
</mat-paginator>
<div style="position: relative;" *ngIf="usePaginator && selectedIndex > 0">
<mat-paginator class="paginator" #paginator (page)="pageChangeEvent($event)" [length]="file_count"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 100, this.paged_data && this.paged_data.length > 100 ? this.paged_data.length : 250]">
</mat-paginator>
</div>
}
</div>

View File

@@ -8,7 +8,6 @@ import { distinctUntilChanged } from 'rxjs/operators';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { MatChipListboxChange } from '@angular/material/chips';
import { MatSelectionListChange } from '@angular/material/list';
import { saveAs } from 'file-saver';
@Component({
selector: 'app-recent-videos',
@@ -381,8 +380,8 @@ export class RecentVideosComponent implements OnInit {
fileSelectionChanged(event: MatSelectionListChange): void {
// TODO: make sure below line is possible (_selected is private)
const adding = event.options['_selected'];
const value = adding.value;
const adding = event.option['_selected'];
const value = event.option.value;
if (adding) {
this.selected_data.push(value.uid);
this.selected_data_objs.push(value);

View File

@@ -1,14 +1,11 @@
<span class="text" [ngStyle]="{'-webkit-line-clamp': !see_more_active ? line_limit : null}" [innerHTML]="text | linkify"></span>
<span>
<a [routerLink]="[]" (click)="toggleSeeMore()">
@if (!see_more_active) {
<ng-container i18n="See more">
See more.
</ng-container>
} @else {
<ng-container i18n="See less">
See less.
</ng-container>
}
</a>
<a [routerLink]="[]" (click)="toggleSeeMore()">
<ng-container *ngIf="!see_more_active" i18n="See more">
See more.
</ng-container>
<ng-container *ngIf="see_more_active" i18n="See less">
See less.
</ng-container>
</a>
</span>

View File

@@ -1,3 +1 @@
@if (show_skip_ad_button) {
<button (click)="skipAdButtonClicked()" mat-flat-button><ng-container i18n="Skip ad button">Skip ad</ng-container></button>
}
<button *ngIf="show_skip_ad_button" (click)="skipAdButtonClicked()" mat-flat-button><ng-container i18n="Skip ad button">Skip ad</ng-container></button>

View File

@@ -1,16 +1,14 @@
<div>
<div style="display: inline-block;">
<mat-form-field appearance="outline" style="width: 165px;">
<mat-select [(ngModel)]="this.sortProperty" (selectionChange)="emitSortOptionChanged()">
@for (sortOption of sortProperties | keyvalue; track sortOption) {
<mat-option [value]="sortOption.key">
{{sortOption['value']['label']}}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="sort-dir-div">
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
<div style="display: inline-block;">
<mat-form-field appearance="outline" style="width: 165px;">
<mat-select [(ngModel)]="this.sortProperty" (selectionChange)="emitSortOptionChanged()">
<mat-option *ngFor="let sortOption of sortProperties | keyvalue" [value]="sortOption.key">
{{sortOption['value']['label']}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="sort-dir-div">
<button (click)="toggleModeChange()" mat-icon-button><mat-icon>{{descendingMode ? 'arrow_downward' : 'arrow_upward'}}</mat-icon></button>
</div>
</div>

View File

@@ -1,31 +1,29 @@
<h4 mat-dialog-title><ng-container i18n="Task settings">Task settings - {{task.title}}</ng-container></h4>
<mat-dialog-content>
@if (task_key === 'delete_old_files') {
<div>
<mat-form-field color="accent">
<mat-label i18n="Delete files older than">Delete files older than</mat-label>
<input [(ngModel)]="new_options['threshold_days']" matInput onlyNumber required>
<span matTextSuffix>days</span>
</mat-form-field>
<div>
<mat-checkbox [(ngModel)]="new_options['blacklist_files']" i18n="Blacklist deleted files" placeholder="Archive mode must be enabled" placeholder-i18n>Blacklist all files</mat-checkbox>
</div>
<div>
<mat-checkbox [disabled]="new_options['blacklist_files']" [(ngModel)]="new_options['blacklist_subscription_files']" i18n="Blacklist deleted subscription files" placeholder="Archive mode must be enabled" placeholder-i18n>Blacklist deleted subscription files</mat-checkbox>
</div>
<div *ngIf="task_key === 'delete_old_files'">
<mat-form-field color="accent">
<mat-label i18n="Delete files older than">Delete files older than</mat-label>
<input [(ngModel)]="new_options['threshold_days']" matInput onlyNumber required>
<span matTextSuffix>days</span>
</mat-form-field>
<div>
<mat-checkbox [(ngModel)]="new_options['blacklist_files']" i18n="Blacklist deleted files" placeholder="Archive mode must be enabled" placeholder-i18n>Blacklist all files</mat-checkbox>
</div>
<div>
<mat-checkbox [disabled]="new_options['blacklist_files']" [(ngModel)]="new_options['blacklist_subscription_files']" i18n="Blacklist deleted subscription files" placeholder="Archive mode must be enabled" placeholder-i18n>Blacklist deleted subscription files</mat-checkbox>
</div>
</div>
<div>
<mat-checkbox [(ngModel)]="new_options['auto_confirm']" i18n="Do not ask for confirmation">Do not ask for confirmation</mat-checkbox>
</div>
}
<div>
<mat-checkbox [(ngModel)]="new_options['auto_confirm']" i18n="Do not ask for confirmation">Do not ask for confirmation</mat-checkbox>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>
@if (optionsChanged()) {
<ng-container i18n="Task settings cancel button">Cancel</ng-container>
} @else {
<ng-container i18n="Task settings close button">Close</ng-container>
}
</button>
<button mat-button [disabled]="!optionsChanged()" (click)="saveSettings()"><ng-container i18n="Save button">Save</ng-container></button>
<button mat-button mat-dialog-close>
<ng-container *ngIf="optionsChanged()" i18n="Task settings cancel button">Cancel</ng-container>
<ng-container *ngIf="!optionsChanged()" i18n="Task settings close button">Close</ng-container>
</button>
<button mat-button [disabled]="!optionsChanged()" (click)="saveSettings()"><ng-container i18n="Save button">Save</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,114 +1,104 @@
<div [hidden]="!(tasks && tasks.length > 0)">
<div style="overflow: hidden;" [ngClass]="'mat-elevation-z8'">
<mat-table style="overflow: hidden" matSort [dataSource]="dataSource">
<!-- Title Column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span class="one-line" [matTooltip]="element.title ? element.title : null">
{{element.title}}
</span>
</mat-cell>
</ng-container>
<!-- Last Ran Column -->
<ng-container matColumnDef="last_ran">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last ran">Last ran</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
@if (element.last_ran) {
{{element.last_ran*1000 | date: 'short'}}
} @else {
<ng-container i18n="N/A">N/A</ng-container>
}
</mat-cell>
</ng-container>
<!-- Last Confirmed Column -->
<ng-container matColumnDef="last_confirmed">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last confirmed">Last confirmed</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
@if (element.last_confirmed) {
{{element.last_confirmed*1000 | date: 'short'}}
} @else {
<ng-container i18n="N/A">N/A</ng-container>
}
</mat-cell>
</ng-container>
<!-- Status Column -->
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Status">Status</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
@if (element.running || element.confirming) {
<span><mat-spinner matTooltip="Busy" i18n-matTooltip="Busy" [diameter]="25"></mat-spinner></span>
} @else if (element.schedule) {
<span style="display: flex">
<ng-container i18n="Scheduled">Scheduled for</ng-container>
{{element.next_invocation | date: 'short'}}
@if (element.schedule.type === 'recurring') {
<mat-icon style="font-size: 16px; display: inline-flex; align-items: center; padding-left: 5px; padding-bottom: 6px;">repeat</mat-icon>
}
</span>
} @else {
<span>
<ng-container i18n="Not scheduled">Not scheduled</ng-container>
</span>
}
</mat-cell>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<div class="container">
<div class="row justify-content-start">
@if (element.data?.uids?.length > 0 || (!element.data?.uids && element.data)) {
<div class="col-12 mt-2" style="display: flex; justify-content: center;">
<ng-container>
<button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button>
@switch(element.key) {
@case ('missing_files_check') {
<ng-container i18n="Clear missing files from DB">Clear missing files from DB:</ng-container>{{element.data.uids.length}}
} @case ('duplicate_files_check') {
<ng-container i18n="Clear duplicate files from DB">Clear duplicate files from DB:</ng-container>&nbsp;{{element.data.uids.length}}
} @case ('youtubedl_update_check') {
<ng-container i18n="Update binary to">Update binary to:</ng-container>&nbsp;{{element.data}}
} @case ('delete_old_files') {
<ng-container i18n="Delete old files">Delete old files:</ng-container>&nbsp;{{element.data.files_to_remove.length}}
}
}
</button>
</ng-container>
</div>
}
<div class="col-3">
<button (click)="runTask(element.key)" [disabled]="element.running || element.confirming" mat-icon-button matTooltip="Run" i18n-matTooltip="Run"><mat-icon>play_arrow</mat-icon></button>
</div>
<div class="col-3">
<button (click)="scheduleTask(element)" mat-icon-button matTooltip="Schedule" i18n-matTooltip="Schedule"><mat-icon>schedule</mat-icon></button>
</div>
<div class="col-3">
<button (click)="openTaskSettings(element)" mat-icon-button matTooltip="Settings" i18n-matTooltip="Settings"><mat-icon>settings</mat-icon></button>
</div>
@if (element.error) {
<div class="col-3">
<button (click)="showError(element)" mat-icon-button matTooltip="Show error" i18n-matTooltip="Show error"><mat-icon>warning</mat-icon></button>
</div>
}
</div>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<mat-paginator [pageSizeOptions]="[10, 20]"
showFirstLastButtons
aria-label="Select page of tasks">
</mat-paginator>
<div style="overflow: hidden;" [ngClass]="'mat-elevation-z8'">
<mat-table style="overflow: hidden" matSort [dataSource]="dataSource">
<!-- Title Column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span class="one-line" [matTooltip]="element.title ? element.title : null">
{{element.title}}
</span>
</mat-cell>
</ng-container>
<!-- Last Ran Column -->
<ng-container matColumnDef="last_ran">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last ran">Last ran</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<ng-container *ngIf="element.last_ran">{{element.last_ran*1000 | date: 'short'}}</ng-container>
<ng-container i18n="N/A" *ngIf="!element.last_ran">N/A</ng-container>
</mat-cell>
</ng-container>
<!-- Last Confirmed Column -->
<ng-container matColumnDef="last_confirmed">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Last confirmed">Last confirmed</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<ng-container *ngIf="element.last_confirmed">{{element.last_confirmed*1000 | date: 'short'}}</ng-container>
<ng-container i18n="N/A" *ngIf="!element.last_confirmed">N/A</ng-container>
</mat-cell>
</ng-container>
<!-- Status Column -->
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Status">Status</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<span *ngIf="element.running || element.confirming"><mat-spinner matTooltip="Busy" i18n-matTooltip="Busy" [diameter]="25"></mat-spinner></span>
<span *ngIf="!(element.running || element.confirming) && element.schedule" style="display: flex">
<ng-container i18n="Scheduled">Scheduled for</ng-container>
{{element.next_invocation | date: 'short'}}<mat-icon style="font-size: 16px; display: inline-flex; align-items: center; padding-left: 5px; padding-bottom: 6px;" *ngIf="element.schedule.type === 'recurring'">repeat</mat-icon>
</span>
<span *ngIf="!(element.running || element.confirming) && !element.schedule">
<ng-container i18n="Not scheduled">Not scheduled</ng-container>
</span>
</mat-cell>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
<mat-cell *matCellDef="let element">
<div class="container">
<div class="row justify-content-start">
<div *ngIf="element.data?.uids?.length > 0 || (!element.data?.uids && element.data)" class="col-12 mt-2" style="display: flex; justify-content: center;">
<ng-container>
<button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button>
<ng-container *ngIf="element.key == 'missing_files_check'">
<ng-container i18n="Clear missing files from DB">Clear missing files from DB:</ng-container>{{element.data.uids.length}}
</ng-container>
<ng-container *ngIf="element.key == 'duplicate_files_check'">
<ng-container i18n="Clear duplicate files from DB">Clear duplicate files from DB:</ng-container>&nbsp;{{element.data.uids.length}}
</ng-container>
<ng-container *ngIf="element.key == 'youtubedl_update_check'">
<ng-container i18n="Update binary to">Update binary to:</ng-container>&nbsp;{{element.data}}
</ng-container>
<ng-container *ngIf="element.key == 'delete_old_files'">
<ng-container i18n="Delete old files">Delete old files:</ng-container>&nbsp;{{element.data.files_to_remove.length}}
</ng-container>
</button>
</ng-container>
</div>
<div class="col-3">
<button (click)="runTask(element.key)" [disabled]="element.running || element.confirming" mat-icon-button matTooltip="Run" i18n-matTooltip="Run"><mat-icon>play_arrow</mat-icon></button>
</div>
<div class="col-3">
<button (click)="scheduleTask(element)" mat-icon-button matTooltip="Schedule" i18n-matTooltip="Schedule"><mat-icon>schedule</mat-icon></button>
</div>
<div class="col-3">
<button (click)="openTaskSettings(element)" mat-icon-button matTooltip="Settings" i18n-matTooltip="Settings"><mat-icon>settings</mat-icon></button>
</div>
<div *ngIf="element.error" class="col-3">
<button (click)="showError(element)" mat-icon-button matTooltip="Show error" i18n-matTooltip="Show error"><mat-icon>warning</mat-icon></button>
</div>
</div>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<mat-paginator [pageSizeOptions]="[10, 20]"
showFirstLastButtons
aria-label="Select page of tasks">
</mat-paginator>
</div>
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="openRestoreDBBackupDialog()" i18n="Restore DB from backup button">Restore DB from backup</button>
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="resetTasks()" color="warn" i18n="Reset tasks button">Reset tasks</button>
</div>
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="openRestoreDBBackupDialog()" i18n="Restore DB from backup button">Restore DB from backup</button>
<button style="margin-top: 10px; margin-left: 5px;" mat-stroked-button (click)="resetTasks()" color="warn" i18n="Reset tasks button">Reset tasks</button>
</div>
@if ((!tasks || tasks.length === 0) && tasks_retrieved) {
<div>
<h4 style="text-align: center; margin-top: 10px;" i18n="No tasks label">No tasks available!</h4>
</div>
}
<div *ngIf="(!tasks || tasks.length === 0) && tasks_retrieved">
<h4 style="text-align: center; margin-top: 10px;" i18n="No tasks label">No tasks available!</h4>
</div>

View File

@@ -1,17 +1,12 @@
@if (visible_chat) {
<div class="chat-container" #scrollContainer>
<div class="chat-container" #scrollContainer *ngIf="visible_chat">
<div style="width: 250px; text-align: center;"><strong>Twitch Chat</strong></div>
@for (chat of visible_chat; track chat; let last = $last) {
<div #chat style="max-width: 250px">
<div #chat style="max-width: 250px" *ngFor="let chat of visible_chat; let last = last">
{{chat.timestamp_str}} - <strong [style.color]="chat.user_color ? chat.user_color : null">{{chat.name}}</strong>: {{chat.message}}
{{last ? scrollToBottom() : ''}}
</div>
}
</div>
}
@if (chat_response_received && !full_chat) {
<button [disabled]="downloading_chat" (click)="downloadTwitchChat()" class="download-button" mat-raised-button color="accent"><ng-container i18n="Download Twitch Chat button">Download Twitch Chat</ng-container></button>
@if (downloading_chat) {
<mat-spinner class="downloading-spinner" [diameter]="30"></mat-spinner>
}
}
</div>
</div>
<ng-container *ngIf="chat_response_received && !full_chat">
<button [disabled]="downloading_chat" (click)="downloadTwitchChat()" class="download-button" mat-raised-button color="accent"><ng-container i18n="Download Twitch Chat button">Download Twitch Chat</ng-container></button>
<mat-spinner *ngIf="downloading_chat" class="downloading-spinner" [diameter]="30"></mat-spinner>
</ng-container>

View File

@@ -1,103 +1,73 @@
<div (mouseenter)="onMouseOver()" (mouseleave)="onMouseOut()" (contextmenu)="onRightClick($event)" style="position: relative; width: fit-content;">
@if (!loading) {
<div class="download-time">
<mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
&nbsp;&nbsp;
@if (file_obj.auto) {
<ng-container i18n="Auto-generated label">Auto-generated</ng-container>
}
@else {
{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}
}
<div *ngIf="!loading" class="download-time">
<mat-icon class="audio-video-icon">{{(file_obj.type === 'audio' || file_obj.isAudio) ? 'audiotrack' : 'movie'}}</mat-icon>
&nbsp;&nbsp;
<ng-container i18n="Auto-generated label" *ngIf="file_obj.auto">Auto-generated</ng-container>
<ng-container *ngIf="!file_obj.auto">{{file_obj.registered | date:'shortDate' : undefined : locale.ngID}}</ng-container>
</div>
}
@else {
<div class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 250 30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
}
<!-- The context menu trigger must be kept above the "more info" menu -->
<div style="visibility: hidden; position: fixed"
[style.left]="contextMenuPosition.x"
[style.top]="contextMenuPosition.y"
[matMenuTriggerFor]="context_menu">
</div>
@if (!file_obj || !file_obj.auto) {
<button [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
}
<mat-menu #context_menu>
@if (!loading) {
<button (click)="navigateToFile($event)" mat-menu-item><mat-icon>open_in_browser</mat-icon><ng-container i18n="Open file button">Open file</ng-container></button>
<button (click)="navigateToFile({ctrlKey: true})" mat-menu-item><mat-icon>open_in_new</mat-icon><ng-container i18n="Open file in new tab">Open file in new tab</ng-container></button>
}
</mat-menu>
<mat-menu #action_menu="matMenu">
@if (!is_playlist && !loading) {
<button (click)="emitToggleFavorite()" mat-menu-item>
<mat-icon>{{file_obj.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon>
@if (!file_obj.favorite) {
<ng-container i18n="Favorite button">Favorite</ng-container>
}
@else {
<ng-container i18n="Unfavorite button">Unfavorite</ng-container>
}
</button>
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
@if (file_obj.sub_id) {
<button (click)="navigateToSubscription()" mat-menu-item><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon>&nbsp;<ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
}
<button [disabled]="!availablePlaylists || availablePlaylists.length === 0" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon>&nbsp;<ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button>
<mat-menu #addtoplaylist="matMenu">
@for (playlist of availablePlaylists; track playlist) {
@if ((playlist.type === 'audio') === file_obj.isAudio) {
<button [disabled]="playlist.uids?.includes(file_obj.uid)" (click)="emitAddFileToPlaylist(playlist.id)" mat-menu-item>{{playlist.name}}</button>
}
}
</mat-menu>
<mat-divider></mat-divider>
@if (file_obj.sub_id) {
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container></button>
} @else {
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
}
@if (file_obj.sub_id || use_youtubedl_archive) {
<button (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button>
}
}
@if (is_playlist && !loading) {
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
<mat-divider></mat-divider>
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
} @else if (loading) {
<button mat-menu-item>Placeholder</button>
}
</mat-menu>
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px">
@if (!loading && file_obj.thumbnailURL) {
<div class="img-div">
<div [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" style="position: relative">
@if (!hide_image || is_playlist || (file_obj.type === 'audio' || file_obj.isAudio)) {
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailPath ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
}
@if (elevated && !is_playlist && !(file_obj.type === 'audio' || file_obj.isAudio)) {
<video autoplay loop muted [muted]="true" [ngClass]="{'video-small': card_size === 'small', 'video': card_size === 'medium', 'video-large': card_size === 'large'}" [src]="streamURL">
</video>
}
<div class="duration-time">
{{file_length}}
<div *ngIf="loading" class="download-time" style="width: 75%; margin-top: 5px;"><content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 250 30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></div>
<!-- The context menu trigger must be kept above the "more info" menu -->
<div style="visibility: hidden; position: fixed"
[style.left]="contextMenuPosition.x"
[style.top]="contextMenuPosition.y"
[matMenuTriggerFor]="context_menu">
</div>
<button *ngIf="!file_obj || !file_obj.auto" [disabled]="loading" [matMenuTriggerFor]="action_menu" class="menuButton" mat-icon-button><mat-icon>more_vert</mat-icon></button>
<mat-menu #context_menu>
<ng-container *ngIf="!loading">
<button (click)="navigateToFile($event)" mat-menu-item><mat-icon>open_in_browser</mat-icon><ng-container i18n="Open file button">Open file</ng-container></button>
<button (click)="navigateToFile({ctrlKey: true})" mat-menu-item><mat-icon>open_in_new</mat-icon><ng-container i18n="Open file in new tab">Open file in new tab</ng-container></button>
</ng-container>
</mat-menu>
<mat-menu #action_menu="matMenu">
<ng-container *ngIf="!is_playlist && !loading">
<button (click)="emitToggleFavorite()" mat-menu-item>
<mat-icon>{{file_obj.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon>
<ng-container *ngIf="!file_obj.favorite" i18n="Favorite button">Favorite</ng-container>
<ng-container *ngIf="file_obj.favorite" i18n="Unfavorite button">Unfavorite</ng-container>
</button>
<button (click)="openFileInfoDialog()" mat-menu-item><mat-icon>info</mat-icon><ng-container i18n="Video info button">Info</ng-container></button>
<button (click)="navigateToSubscription()" mat-menu-item *ngIf="file_obj.sub_id"><mat-icon>{{file_obj.isAudio ? 'library_music' : 'video_library'}}</mat-icon>&nbsp;<ng-container i18n="Go to subscription menu item">Go to subscription</ng-container></button>
<button [disabled]="!availablePlaylists || availablePlaylists.length === 0" [matMenuTriggerFor]="addtoplaylist" mat-menu-item><mat-icon>playlist_add</mat-icon>&nbsp;<ng-container i18n="Add to playlist menu item">Add to playlist</ng-container></button>
<mat-menu #addtoplaylist="matMenu">
<ng-container *ngFor="let playlist of availablePlaylists">
<button *ngIf="(playlist.type === 'audio') === file_obj.isAudio" [disabled]="playlist.uids?.includes(file_obj.uid)" (click)="emitAddFileToPlaylist(playlist.id)" mat-menu-item>{{playlist.name}}</button>
</ng-container>
</mat-menu>
<mat-divider></mat-divider>
<button *ngIf="file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>restore</mat-icon><ng-container i18n="Delete and redownload subscription video button">Delete and redownload</ng-container></button>
<button *ngIf="!file_obj.sub_id" (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete</mat-icon><ng-container i18n="Delete video button">Delete</ng-container></button>
<button *ngIf="file_obj.sub_id || use_youtubedl_archive" (click)="emitDeleteFile(true)" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete and don't download again">Delete and don't download again</ng-container></button>
</ng-container>
<ng-container *ngIf="is_playlist && !loading">
<button (click)="emitEditPlaylist()" mat-menu-item><mat-icon>edit</mat-icon><ng-container i18n="Playlist edit button">Edit</ng-container></button>
<mat-divider></mat-divider>
<button (click)="emitDeleteFile()" mat-menu-item><mat-icon>delete_forever</mat-icon><ng-container i18n="Delete playlist">Delete</ng-container></button>
</ng-container>
<ng-container *ngIf="loading">
<button mat-menu-item>Placeholder</button>
</ng-container>
</mat-menu>
<mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px">
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
<div [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" style="position: relative">
<img *ngIf="!hide_image || is_playlist || (file_obj.type === 'audio' || file_obj.isAudio)" [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailPath ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
<video *ngIf="elevated && !is_playlist && !(file_obj.type === 'audio' || file_obj.isAudio)" autoplay loop muted [muted]="true" [ngClass]="{'video-small': card_size === 'small', 'video': card_size === 'medium', 'video-large': card_size === 'large'}" [src]="streamURL">
</video>
<div class="duration-time">
{{file_length}}
</div>
</div>
</div>
</div>
<div *ngIf="loading" class="img-div">
<content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 100 55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
</div>
<span *ngIf="!loading" [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
<span *ngIf="loading" class="title-loading"><content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 250 30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
</div>
}
@if (loading) {
<div class="img-div">
<content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 100 55"><svg:rect x="0" y="0" rx="0" ry="0" width="100" height="55" /></content-loader>
</div>
} @else {
<span [ngClass]="{'max-two-lines': card_size !== 'small', 'max-one-line': card_size === 'small' }">{{card_size === 'large' && file_obj.uploader ? file_obj.uploader + ' - ' : ''}}<strong>{{!is_playlist ? file_obj.title : file_obj.name}}</strong></span>
}
@if (loading) {
<span class="title-loading"><content-loader [backgroundColor]="theme.ghost_primary" [foregroundColor]="theme.ghost_secondary" viewBox="0 0 250 30"><svg:rect x="0" y="0" rx="3" ry="3" width="250" height="30" /></content-loader></span>
}
</div>
</mat-card>
</mat-card>
</div>

View File

@@ -1,35 +1,28 @@
@if (create_mode) {
<h4 mat-dialog-title ><ng-container i18n="Create a playlist dialog title">Create a playlist</ng-container></h4>
} @else {
<h4 mat-dialog-title><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
}
<h4 mat-dialog-title *ngIf="create_mode" ><ng-container i18n="Create a playlist dialog title">Create a playlist</ng-container></h4>
<h4 mat-dialog-title *ngIf="!create_mode"><ng-container i18n="Modify playlist dialog title">Modify playlist</ng-container></h4>
<mat-dialog-content style="max-height: 85vh;">
<form>
@if (create_mode || playlist) {
<div>
<div>
<mat-form-field color="accent">
<mat-label i18n="Playlist name">Name</mat-label>
<input [(ngModel)]="name" matInput type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
<form>
<div *ngIf="create_mode || playlist">
<div>
<mat-form-field color="accent">
<mat-label i18n="Playlist name">Name</mat-label>
<input [(ngModel)]="name" matInput type="text" required aria-required [ngModelOptions]="{standalone: true}">
</mat-form-field>
</div>
<app-recent-videos [selectMode]="true" [defaultSelected]="preselected_files" [customHeader]="'Select files'" (fileSelectionEmitter)="fileSelectionChanged($event)" [selectedIndex]="create_mode ? 1 : 0"></app-recent-videos>
</div>
<app-recent-videos [selectMode]="true" [defaultSelected]="preselected_files" [customHeader]="'Select files'" (fileSelectionEmitter)="fileSelectionChanged($event)" [selectedIndex]="create_mode ? 1 : 0"></app-recent-videos>
</div>
}
</form>
</form>
</mat-dialog-content>
<div class="spacer"></div>
<mat-dialog-actions>
@if (create_mode) {
<button (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-button>
<ng-container i18n="Create button">Create</ng-container>
<button *ngIf="create_mode" (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-button>
<ng-container i18n="Create button">Create</ng-container>
</button>
} @else {
<button (click)="updatePlaylist()" [disabled]="!name || !playlistChanged()" color="primary" style="float: right" mat-button>
<ng-container i18n="Save button">Save</ng-container>
<button *ngIf="!create_mode" (click)="updatePlaylist()" [disabled]="!name || !playlistChanged()" color="primary" style="float: right" mat-button>
<ng-container i18n="Save button">Save</ng-container>
</button>
}
@if (create_in_progress) {
<div style="margin-left: 10px"><mat-spinner [diameter]="25"></mat-spinner></div>
}
<div *ngIf="create_in_progress" style="margin-left: 10px"><mat-spinner [diameter]="25"></mat-spinner></div>
</mat-dialog-actions>

View File

@@ -1,51 +1,43 @@
<h4 style="position: relative" mat-dialog-title><ng-container i18n="About dialog title">About YoutubeDL-Material</ng-container>
<span class="logo-image">
<a [href]="projectLink" target="_blank">
<img style="width: 32px;" src="assets/images/GitHub-64px.png">
</a>
<img style="width: 32px; margin-left: 15px;" src="assets/images/logo_128px.png">
</span>
<span class="logo-image">
<a [href]="projectLink" target="_blank">
<img style="width: 32px;" src="assets/images/GitHub-64px.png">
</a>
<img style="width: 32px; margin-left: 15px;" src="assets/images/logo_128px.png">
</span>
</h4>
<mat-dialog-content>
<div style="margin-bottom: 5px;">
<p>
<i>YoutubeDL-Material</i>&nbsp;<ng-container i18n="About first paragraph">is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos.</ng-container>
</p>
<p>
<i>YoutubeDL-Material</i>&nbsp;<ng-container i18n="About second paragraph">has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above.</ng-container>
</p>
<mat-divider></mat-divider>
<h5 style="margin-top: 10px;">Installation details:</h5>
<p>
<ng-container i18n="Version label">Installed version:</ng-container>&nbsp;{{current_version_tag}} -
@if (checking_for_updates) {
<span style="display: inline-block"><mat-spinner class="version-spinner" [diameter]="22"></mat-spinner>&nbsp;<ng-container i18n="Checking for updates text">Checking for updates...</ng-container></span>
} @else {
<mat-icon class="version-checked-icon">done</mat-icon>&nbsp;&nbsp;
@if (latestGithubRelease['tag_name'] !== current_version_tag) {
<a [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container>
} @else {
<span>You are up to date.</span>
}
}
</p>
<p>
<ng-container i18n="Installation type">Installation type:</ng-container>&nbsp;{{postsService.version_info.type}}
<br>
@if (postsService.version_info.type === 'docker') {
<ng-container i18n="Docker tag">Docker tag:</ng-container>&nbsp;{{postsService.version_info.tag}}
<br>
}
<ng-container i18n="Commit hash">Commit hash:</ng-container>&nbsp;{{postsService.version_info.commit}}
<br>
<ng-container i18n="Build date">Build date:</ng-container>&nbsp;{{postsService.version_info.date}}
<div style="margin-bottom: 5px;">
<p>
<i>YoutubeDL-Material</i>&nbsp;<ng-container i18n="About first paragraph">is an open-source YouTube downloader built under Google's Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos.</ng-container>
</p>
<p>
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container>&nbsp;<a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a>&nbsp;<ng-container i18n="About bug suffix">to create an issue!</ng-container>
<i>YoutubeDL-Material</i>&nbsp;<ng-container i18n="About second paragraph">has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above.</ng-container>
</p>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>
<mat-divider></mat-divider>
<h5 style="margin-top: 10px;">Installation details:</h5>
<p>
<ng-container i18n="Version label">Installed version:</ng-container>&nbsp;{{current_version_tag}} - <span style="display: inline-block" *ngIf="checking_for_updates"><mat-spinner class="version-spinner" [diameter]="22"></mat-spinner>&nbsp;<ng-container i18n="Checking for updates text">Checking for updates...</ng-container></span>
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon>&nbsp;&nbsp;<ng-container *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag"><a [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container></ng-container>
<span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
</p>
<p>
<ng-container i18n="Installation type">Installation type:</ng-container>&nbsp;{{postsService.version_info.type}}
<br>
<ng-container *ngIf="postsService.version_info.type === 'docker'">
<ng-container i18n="Docker tag">Docker tag:</ng-container>&nbsp;{{postsService.version_info.tag}}
<br>
</ng-container>
<ng-container i18n="Commit hash">Commit hash:</ng-container>&nbsp;{{postsService.version_info.commit}}
<br>
<ng-container i18n="Build date">Build date:</ng-container>&nbsp;{{postsService.version_info.date}}
</p>
<p>
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container>&nbsp;<a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a>&nbsp;<ng-container i18n="About bug suffix">to create an issue!</ng-container>
</p>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button style="margin-bottom: 5px;" mat-stroked-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,94 +1,92 @@
<h4 i18n="Modify args title" mat-dialog-title>Modify youtube-dl args</h4>
<mat-dialog-content>
<div class="container">
<div class="row">
<div class="col-12">
<mat-card class="mat-elevation-z6">
<mat-card-content>
<h6 i18n="Simulated args title">Simulated new args</h6>
<mat-chip-grid class="example-chip" #chipList aria-label="Args array" cdkDropList cdkDropListDisabled
cdkDropListOrientation="horizontal"
(cdkDropListDropped)="drop($event)">
@for (arg of args_array; track arg; let i = $index) {
<mat-chip-row [matTooltip]="argsByKey[arg] ? argsByKey[arg]['description'] : null" [removable]="removable" (removed)="remove(i)" cdkDrag>
{{arg}}
@if (removable) {
<mat-icon matChipRemove>cancel</mat-icon>
}
</mat-chip-row>
}
</mat-chip-grid>
<mat-form-field style="width: 100%" color="accent">
<input #chipper style="width: 100%;" [formControl]="chipCtrl" matInput [matAutocomplete]="autochip" [matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event)">
</mat-form-field>
<mat-autocomplete #autochip="matAutocomplete">
@for (arg of filteredChipOptions | async; track arg) {
<mat-option [value]="arg.key">
<span [innerHTML]="arg.key | highlight : chipCtrl.value"></span>
<button class="info-autocomplete-icon" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
</mat-option>
}
</mat-autocomplete>
</mat-card-content>
</mat-card>
<div class="container">
<div class="row">
<div class="col-12">
<mat-card class="mat-elevation-z6">
<mat-card-content>
<h6 i18n="Simulated args title">Simulated new args</h6>
<mat-chip-grid class="example-chip" #chipList aria-label="Args array" cdkDropList cdkDropListDisabled
cdkDropListOrientation="horizontal"
(cdkDropListDropped)="drop($event)">
<mat-chip-row [matTooltip]="argsByKey[arg] ? argsByKey[arg]['description'] : null" *ngFor="let arg of args_array; let i = index;" [removable]="removable" (removed)="remove(i)" cdkDrag>
{{arg}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip-row>
</mat-chip-grid>
<mat-form-field style="width: 100%" color="accent">
<input #chipper style="width: 100%;" [formControl]="chipCtrl" matInput [matAutocomplete]="autochip" [matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event)">
</mat-form-field>
<mat-autocomplete #autochip="matAutocomplete">
<mat-option *ngFor="let arg of filteredChipOptions | async" [value]="arg.key">
<span [innerHTML]="arg.key | highlight : chipCtrl.value"></span>
<button class="info-autocomplete-icon" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
</mat-option>
</mat-autocomplete>
</mat-card-content>
</mat-card>
</div>
<div class="col-12">
<mat-card class="mat-elevation-z6 my-2">
<mat-card-content>
<h6 i18n="Add arg card title">Add an arg</h6>
<form >
<div>
<mat-form-field style="width: 75%" color="accent">
<mat-label i18n="Arg">Arg</mat-label>
<input matInput [matAutocomplete]="auto" [formControl]="stateCtrl">
</mat-form-field>
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let arg of filteredOptions | async" [value]="arg.key">
<span [innerHTML]="arg.key | highlight : stateCtrl.value"></span>
<button class="info-autocomplete-icon" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
</mat-option>
</mat-autocomplete>
<div>
<mat-menu #argsByCategoryMenu="matMenu">
<ng-container *ngFor="let argsInCategory of argsByCategory | keyvalue">
<button mat-menu-item [matMenuTriggerFor]="subMenu">{{argsInfo[argsInCategory.key].label}}</button>
<mat-menu #subMenu="matMenu">
<button mat-menu-item *ngFor="let arg of argsInCategory.value" (click)="setFirstArg(arg.key)"><div style="display: inline-block;">{{arg.key}}</div>&nbsp;&nbsp;<div class="info-menu-icon"><mat-icon [matTooltip]="arg.description">info</mat-icon></div></button>
</mat-menu>
</ng-container>
</mat-menu>
<button style="margin-bottom: 15px" mat-stroked-button [matMenuTriggerFor]="argsByCategoryMenu"><ng-container i18n="Search args by category button">Search by category</ng-container></button>
</div>
</div>
<div>
<mat-checkbox color="accent" [ngModelOptions]="{standalone: true}" [(ngModel)]="secondArgEnabled"><ng-container i18n="Use arg value checkbox">Use arg value</ng-container></mat-checkbox>
</div>
<div *ngIf="secondArgEnabled">
<mat-form-field style="width: 75%" color="accent">
<mat-label i18n="Arg value">Arg value</mat-label>
<input [ngModelOptions]="{standalone: true}" matInput [disabled]="!secondArgEnabled" [(ngModel)]="secondArg">
</mat-form-field>
</div>
</form>
<div>
<button (click)="addArg()" [disabled]="!canAddArg()" mat-stroked-button color="accent"><ng-container i18n="Search args by category button">Add arg</ng-container></button>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
<div class="col-12">
<mat-card class="mat-elevation-z6 my-2">
<mat-card-content>
<h6 i18n="Add arg card title">Add an arg</h6>
<form >
<div>
<mat-form-field style="width: 75%" color="accent">
<mat-label i18n="Arg">Arg</mat-label>
<input matInput [matAutocomplete]="auto" [formControl]="stateCtrl">
</mat-form-field>
<mat-autocomplete #auto="matAutocomplete">
@for (arg of filteredOptions | async; track arg) {
<mat-option [value]="arg.key">
<span [innerHTML]="arg.key | highlight : stateCtrl.value"></span>
<button class="info-autocomplete-icon" [matTooltip]="arg.description" mat-icon-button><mat-icon>info</mat-icon></button>
</mat-option>
}
</mat-autocomplete>
<div>
<mat-menu #argsByCategoryMenu="matMenu">
@for (argsInCategory of argsByCategory | keyvalue; track argsInCategory) {
<button mat-menu-item [matMenuTriggerFor]="subMenu">{{argsInfo[argsInCategory.key].label}}</button>
<mat-menu #subMenu="matMenu">
@for (arg of argsInCategory.value; track arg) {
<button mat-menu-item (click)="setFirstArg(arg.key)"><div style="display: inline-block;">{{arg.key}}</div>&nbsp;&nbsp;<div class="info-menu-icon"><mat-icon [matTooltip]="arg.description">info</mat-icon></div></button>
}
</mat-menu>
}
</mat-menu>
<button style="margin-bottom: 15px" mat-stroked-button [matMenuTriggerFor]="argsByCategoryMenu"><ng-container i18n="Search args by category button">Search by category</ng-container></button>
</div>
</div>
<div>
<mat-checkbox color="accent" [ngModelOptions]="{standalone: true}" [(ngModel)]="secondArgEnabled"><ng-container i18n="Use arg value checkbox">Use arg value</ng-container></mat-checkbox>
</div>
@if (secondArgEnabled) {
<div>
<mat-form-field style="width: 75%" color="accent">
<mat-label i18n="Arg value">Arg value</mat-label>
<input [ngModelOptions]="{standalone: true}" matInput [disabled]="!secondArgEnabled" [(ngModel)]="secondArg">
</mat-form-field>
</div>
}
</form>
<div>
<button (click)="addArg()" [disabled]="!canAddArg()" mat-stroked-button color="accent"><ng-container i18n="Search args by category button">Add arg</ng-container></button>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button [mat-dialog-close]="null"><ng-container i18n="Arg modifier cancel button">Cancel</ng-container></button>
<button mat-button color="accent" [mat-dialog-close]="modified_args"><ng-container i18n="Arg modifier modify button">Modify</ng-container></button>
</mat-dialog-actions>
</mat-dialog-actions>

View File

@@ -2,27 +2,24 @@
<mat-dialog-content>
<div style="margin-bottom: 10px;">
<!-- We can support text dialogs or dialogs where users must select items from a list -->
@if (dialogType === 'text') {
<ng-container *ngIf="dialogType === 'text'">
{{dialogText}}
} @else if (dialogType === 'selection_list') {
</ng-container>
<ng-container *ngIf="dialogType === 'selection_list'">
<mat-selection-list [(ngModel)]="selected_items">
@for (item of list; track item) {
<mat-list-option [value]="item.key">
{{item.title}}
</mat-list-option>
}
<mat-list-option *ngFor="let item of list" [value]="item.key">
{{item.title}}
</mat-list-option>
</mat-selection-list>
}
</ng-container>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button [disabled]="dialogType === 'selection_list' && selected_items.length === 0" [color]="warnSubmitColor ? 'warn' : 'primary'" mat-flat-button type="submit" (click)="confirmClicked()">{{submitText}}</button>
@if (submitClicked) {
<div class="mat-spinner">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
}
<div class="mat-spinner" *ngIf="submitClicked">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
<span class="spacer"></span>
<button style="float: right;" mat-stroked-button mat-dialog-close>
{{cancelText}}

View File

@@ -1,44 +1,40 @@
<h4 mat-dialog-title i18n="Cookies uploader dialog title">Upload new cookies</h4>
<mat-dialog-content>
<div>
<div class="center">
<ngx-file-drop [multiple]="false" accept=".txt" dropZoneLabel="Drop files here" (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)">
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
<div>
<div>
<ng-container i18n="Drag and Drop">Drag and Drop</ng-container>
<div>
<div class="center">
<ngx-file-drop [multiple]="false" accept=".txt" dropZoneLabel="Drop files here" (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)">
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
<div>
<div>
<ng-container i18n="Drag and Drop">Drag and Drop</ng-container>
</div>
<div style="margin-top: 6px;">
<button mat-stroked-button (click)="openFileSelector()">Browse Files</button>
</div>
</div>
</ng-template>
</ngx-file-drop>
<div style="margin-top: 15px;">
<p style="font-size: 14px;" i18n="Cookies upload warning">NOTE: Uploading new cookies will override your previous cookies. Also note that cookies are instance-wide, not per-user.</p>
</div>
<div style="margin-top: 6px;">
<button mat-stroked-button (click)="openFileSelector()">Browse Files</button>
<div style="margin-top: 10px;">
<table class="table">
<tbody class="upload-name-style">
<tr *ngFor="let item of files; let i=index">
<td style="vertical-align: middle;">
<strong>{{ item.relativePath }}</strong>
</td>
<td>
<button [disabled]="uploading || uploaded" (click)="uploadFile()" style="float: right" matTooltip="Upload" i18n-matTooltip="Upload" mat-mini-fab><mat-icon>publish</mat-icon><mat-spinner *ngIf="uploading" class="spinner" [diameter]="38"></mat-spinner></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-template>
</ngx-file-drop>
<div style="margin-top: 15px;">
<p style="font-size: 14px;" i18n="Cookies upload warning">NOTE: Uploading new cookies will override your previous cookies. Also note that cookies are instance-wide, not per-user.</p>
</div>
<div style="margin-top: 10px;">
<table class="table">
<tbody class="upload-name-style">
@for (item of files; track item; let i = $index) {
<tr>
<td style="vertical-align: middle;">
<strong>{{ item.relativePath }}</strong>
</td>
<td>
<button [disabled]="uploading || uploaded" (click)="uploadFile()" style="float: right" matTooltip="Upload" i18n-matTooltip="Upload" mat-mini-fab><mat-icon>publish</mat-icon>
@if (uploading) {
<mat-spinner class="spinner" [diameter]="38"></mat-spinner>
}
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions><button style="margin-bottom: 5px;" mat-dialog-close mat-stroked-button><ng-container i18n="Close">Close</ng-container></button></mat-dialog-actions>

View File

@@ -1,63 +1,64 @@
<h4 mat-dialog-title><ng-container i18n="Editing category dialog title">Editing category</ng-container>&nbsp;{{category['name']}}</h4>
<mat-dialog-content style="max-height: 50vh">
<mat-form-field style="width: 250px; margin-bottom: 5px;">
<mat-label i18n="Category name">Name</mat-label>
<input matInput [(ngModel)]="category['name']" required>
</mat-form-field>
<mat-divider></mat-divider>
<h6 style="margin-top: 20px;" i18n="Rules">Rules</h6>
<mat-list>
@for (rule of category['rules']; track rule) {
<mat-list-item>
<mat-form-field [style.visibility]="$index === 0 ? 'hidden' : null" class="operator-select">
<mat-select [disabled]="$index === 0" [(ngModel)]="rule['preceding_operator']">
<mat-option value="or">OR</mat-option>
<mat-option value="and">AND</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="property-select">
<mat-select [(ngModel)]="rule['property']">
@for (propertyOption of propertyOptions; track propertyOption) {
<mat-option [value]="propertyOption.value">{{propertyOption.label}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="comparator-select">
<mat-select [(ngModel)]="rule['comparator']">
@for (comparatorOption of comparatorOptions; track comparatorOption) {
<mat-option [value]="comparatorOption.value">{{comparatorOption.label}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="value-input">
<input matInput [(ngModel)]="rule['value']">
</mat-form-field>
<span class="rule-buttons">
<button [disabled]="$index === category['rules'].length-1" (click)="swapRules($index, $index+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="$index === 0" (click)="swapRules($index, $index-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule($index)" mat-icon-button><mat-icon>cancel</mat-icon></button>
</span>
</mat-list-item>
}
</mat-list>
<button style="margin-bottom: 8px;" mat-icon-button (click)="addNewRule()" matTooltip="Add new rule" i18n-matTooltip="Add new rule tooltip"><mat-icon>add</mat-icon></button>
<mat-divider></mat-divider>
<mat-form-field style="width: 250px; margin-top: 10px;">
<mat-label i18n="Custom file output">Custom file output</mat-label>
<input matInput [(ngModel)]="category['custom_output']">
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
<mat-form-field style="width: 250px; margin-bottom: 5px;">
<mat-label i18n="Category name">Name</mat-label>
<input matInput [(ngModel)]="category['name']" required>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<mat-divider></mat-divider>
<h6 style="margin-top: 20px;" i18n="Rules">Rules</h6>
<mat-list>
<mat-list-item *ngFor="let rule of category['rules']; let i = index">
<mat-form-field [style.visibility]="i === 0 ? 'hidden' : null" class="operator-select">
<mat-select [disabled]="i === 0" [(ngModel)]="rule['preceding_operator']">
<mat-option value="or">OR</mat-option>
<mat-option value="and">AND</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="property-select">
<mat-select [(ngModel)]="rule['property']">
<mat-option *ngFor="let propertyOption of propertyOptions" [value]="propertyOption.value">{{propertyOption.label}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="comparator-select">
<mat-select [(ngModel)]="rule['comparator']">
<mat-option *ngFor="let comparatorOption of comparatorOptions" [value]="comparatorOption.value">{{comparatorOption.label}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="value-input">
<input matInput [(ngModel)]="rule['value']">
</mat-form-field>
<span class="rule-buttons">
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
</span>
</mat-list-item>
</mat-list>
<button style="margin-bottom: 8px;" mat-icon-button (click)="addNewRule()" matTooltip="Add new rule" i18n-matTooltip="Add new rule tooltip"><mat-icon>add</mat-icon></button>
<mat-divider></mat-divider>
<mat-form-field style="width: 250px; margin-top: 10px;">
<mat-label i18n="Custom file output">Custom file output</mat-label>
<input matInput [(ngModel)]="category['custom_output']">
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Cancel">Cancel</ng-container></button>
<button mat-button [disabled]="categoryChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
@if (updating) {
<div class="mat-spinner">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
}
</mat-dialog-actions>
<div class="mat-spinner" *ngIf="updating">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -1,80 +1,70 @@
<h4 mat-dialog-title><ng-container i18n="Edit subscription dialog title prefix">Editing</ng-container>&nbsp;{{sub.name}}
@if (sub.paused) {
&nbsp;<ng-container i18n="Paused suffix">(Paused)</ng-container>
}
</h4>
<h4 mat-dialog-title><ng-container i18n="Edit subscription dialog title prefix">Editing</ng-container>&nbsp;{{sub.name}}&nbsp;<ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
<mat-dialog-content>
<div class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox [(ngModel)]="new_sub.paused"><ng-container i18n="Paused subscription setting">Paused</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-3">
<mat-checkbox (change)="downloadAllToggled()" [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
</div>
@if (editor_initialized) {
<div class="col-12">
<ng-container i18n="Download time range prefix">Download videos uploaded in the last</ng-container>
<mat-form-field color="accent" class="amount-select">
<input type="number" matInput [(ngModel)]="timerange_amount" (ngModelChange)="timerangeChanged($event, false)" [disabled]="download_all">
</mat-form-field>
<mat-form-field class="unit-select">
<mat-select color="accent" [(ngModel)]="timerange_unit" (ngModelChange)="timerangeChanged($event, true)" [disabled]="download_all">
@for (time_unit of time_units; track time_unit) {
<mat-option [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox [(ngModel)]="new_sub.paused"><ng-container i18n="Paused subscription setting">Paused</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-3">
<mat-checkbox (change)="downloadAllToggled()" [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
</div>
<div class="col-12" *ngIf="editor_initialized">
<ng-container i18n="Download time range prefix">Download videos uploaded in the last</ng-container>
<mat-form-field color="accent" class="amount-select">
<input type="number" matInput [(ngModel)]="timerange_amount" (ngModelChange)="timerangeChanged($event, false)" [disabled]="download_all">
</mat-form-field>
<mat-form-field class="unit-select">
<mat-select color="accent" [(ngModel)]="timerange_unit" (ngModelChange)="timerangeChanged($event, true)" [disabled]="download_all">
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12 mt-2">
<mat-form-field>
<mat-label i18n="Max quality">Max quality</mat-label>
<mat-select [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.maxQuality">
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mb-3">
<mat-form-field color="accent">
<mat-label i18n="Custom args">Custom args</mat-label>
<input [(ngModel)]="new_sub.custom_args" matInput>
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-hint>
<ng-container i18n="Custom args hint">These are added after the standard args.</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-form-field color="accent">
<mat-label i18n="Custom file output">Custom file output</mat-label>
<input [(ngModel)]="new_sub.custom_output" matInput>
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
</div>
}
<div class="col-12">
<div>
<mat-checkbox [disabled]="true" [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12 mt-2">
<mat-form-field>
<mat-label i18n="Max quality">Max quality</mat-label>
<mat-select [disabled]="new_sub.type === 'audio'" [(ngModel)]="new_sub.maxQuality">
@for (available_quality of available_qualities; track available_quality) {
<mat-option [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mb-3">
<mat-form-field color="accent">
<mat-label i18n="Custom args">Custom args</mat-label>
<input [(ngModel)]="new_sub.custom_args" matInput>
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-hint>
<ng-container i18n="Custom args hint">These are added after the standard args.</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-2">
<mat-form-field color="accent">
<mat-label i18n="Custom file output">Custom file output</mat-label>
<input [(ngModel)]="new_sub.custom_output" matInput>
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Subscribe cancel button">Cancel</ng-container></button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button [disabled]="updating || !subChanged()" type="submit" (click)="saveClicked()"><ng-container i18n="Save button">Save</ng-container></button>
@if (updating) {
<div class="mat-spinner">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
}
</mat-dialog-actions>
<div class="mat-spinner" *ngIf="updating">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -1,68 +1,67 @@
<h4 mat-dialog-title><ng-container i18n="Generate RSS URL">Generate RSS URL</ng-container></h4>
<mat-dialog-content>
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label i18n="Title filter">Title filter</mat-label>
<input [(ngModel)]="titleFilter" matInput (input)="rebuildURL()">
<mat-hint i18n="Supports regex">Supports regex</mat-hint>
</mat-form-field>
</div>
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label><ng-container i18n="File type">File type</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="fileTypeFilter" (selectionChange)="rebuildURL()">
<mat-option value="both"><ng-container i18n="Both">Both</ng-container></mat-option>
<mat-option value="video_only"><ng-container i18n="Video only">Video only</ng-container></mat-option>
<mat-option value="audio_only"><ng-container i18n="Audio only">Audio only</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label><ng-container i18n="User">User</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="userFilter" (selectionChange)="rebuildURL()" [disabled]="!usersList">
<mat-option [value]="''"><ng-container i18n="None">None</ng-container></mat-option>
@for (user of usersList; track user) {
<mat-option [value]="user.uid"><ng-container>{{user.name}}</ng-container></mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label><ng-container i18n="Subscription">Subscription</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="subscriptionFilter" (selectionChange)="rebuildURL()">
<mat-option [value]="''"><ng-container i18n="None">None</ng-container></mat-option>
@for (sub of postsService.subscriptions; track sub) {
<mat-option [value]="sub.id"><ng-container>{{sub.name}}</ng-container></mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="col-12 col-md-6 mt-2">
<app-sort-property (sortOptionChanged)="sortOptionChanged($event)"></app-sort-property>
</div>
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label i18n="Item limit">Item limit</mat-label>
<input type="number" [(ngModel)]="itemLimit" (input)="rebuildURL()" matInput>
</mat-form-field>
</div>
<div class="col-12 mb-4">
<mat-checkbox (change)="rebuildURL()" [(ngModel)]="favoriteFilter"><ng-container i18n="Favorited">Favorited</ng-container></mat-checkbox>
</div>
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label i18n="Title filter">Title filter</mat-label>
<input [(ngModel)]="titleFilter" matInput (input)="rebuildURL()">
<mat-hint i18n="Supports regex">Supports regex</mat-hint>
</mat-form-field>
</div>
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label><ng-container i18n="File type">File type</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="fileTypeFilter" (selectionChange)="rebuildURL()">
<mat-option value="both"><ng-container i18n="Both">Both</ng-container></mat-option>
<mat-option value="video_only"><ng-container i18n="Video only">Video only</ng-container></mat-option>
<mat-option value="audio_only"><ng-container i18n="Audio only">Audio only</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label><ng-container i18n="User">User</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="userFilter" (selectionChange)="rebuildURL()" [disabled]="!usersList">
<mat-option [value]="''"><ng-container i18n="None">None</ng-container></mat-option>
<mat-option *ngFor="let user of usersList" [value]="user.uid"><ng-container>{{user.name}}</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label><ng-container i18n="Subscription">Subscription</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="subscriptionFilter" (selectionChange)="rebuildURL()">
<mat-option [value]="''"><ng-container i18n="None">None</ng-container></mat-option>
<mat-option *ngFor="let sub of postsService.subscriptions" [value]="sub.id"><ng-container>{{sub.name}}</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12 col-md-6 mt-2">
<app-sort-property (sortOptionChanged)="sortOptionChanged($event)"></app-sort-property>
</div>
<div class="col-12 col-md-6 mt-2">
<mat-form-field class="filter-field">
<mat-label i18n="Item limit">Item limit</mat-label>
<input type="number" [(ngModel)]="itemLimit" (input)="rebuildURL()" matInput>
</mat-form-field>
</div>
<div class="col-12 mb-4">
<mat-checkbox (change)="rebuildURL()" [(ngModel)]="favoriteFilter"><ng-container i18n="Favorited">Favorited</ng-container></mat-checkbox>
</div>
</div>
</div>
</div>
<mat-form-field style="width: 100%;">
<mat-label i18n="URL">URL</mat-label>
<input readonly [(ngModel)]="url" matInput>
<button mat-icon-button matSuffix (click)="copyURL()">
<mat-icon>content_copy</mat-icon>
</button>
</mat-form-field>
<mat-form-field style="width: 100%;">
<mat-label i18n="URL">URL</mat-label>
<input readonly [(ngModel)]="url" matInput>
<button mat-icon-button matSuffix (click)="copyURL()">
<mat-icon>content_copy</mat-icon>
</button>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
<button mat-button mat-dialog-close><ng-container i18n="Close">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,31 +1,29 @@
<h4 mat-dialog-title><ng-container i18n="Restore DB from backup">Restore DB from backup</ng-container></h4>
<mat-dialog-content>
<mat-selection-list [multiple]="false" [(ngModel)]="selected_backup">
@for (db_backup of db_backups; track db_backup) {
<mat-list-option [value]="db_backup.name" [matTooltip]="db_backup.name">
<div class="container-fluid">
<div class="row">
<div class="col-4">
{{db_backup.timestamp*1000 | date: 'short'}}
<mat-selection-list [multiple]="false" [(ngModel)]="selected_backup">
<mat-list-option *ngFor="let db_backup of db_backups" [value]="db_backup.name" [matTooltip]="db_backup.name">
<div class="container-fluid">
<div class="row">
<div class="col-4">
{{db_backup.timestamp*1000 | date: 'short'}}
</div>
<div class="col-4">
{{(db_backup.size/1000).toFixed(2)}} kB
</div>
<div class="col-4" style="text-transform: capitalize;">
{{db_backup.source}}
</div>
</div>
</div>
<div class="col-4">
{{(db_backup.size/1000).toFixed(2)}} kB
</div>
<div class="col-4" style="text-transform: capitalize;">
{{db_backup.source}}
</div>
</div>
</div>
</mat-list-option>
}
</mat-selection-list>
</mat-list-option>
</mat-selection-list>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Restore DB cancel button">Cancel</ng-container></button>
<button mat-button [disabled]="restoring" (click)="restoreClicked()" [disabled]="!selected_backup || selected_backup.length !== 1"><ng-container i18n="Restore button">Restore</ng-container></button>
@if (restoring) {
<div class="mat-spinner">
<button mat-button mat-dialog-close><ng-container i18n="Restore DB cancel button">Cancel</ng-container></button>
<button mat-button [disabled]="restoring" (click)="restoreClicked()" [disabled]="!selected_backup || selected_backup.length !== 1"><ng-container i18n="Restore button">Restore</ng-container></button>
<div class="mat-spinner" *ngIf="restoring">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
}
</mat-dialog-actions>

View File

@@ -1,22 +1,20 @@
<h4 mat-dialog-title><ng-container i18n="Create admin account dialog title">Create admin account</ng-container></h4>
<mat-dialog-content>
<div>
<p i18n="No default admin detected explanation">No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'.</p>
</div>
<div style="position: relative">
<div>
<mat-form-field color="accent">
<mat-label i18n="Password">Password</mat-label>
<input type="password" (keyup.enter)="create()" matInput [(ngModel)]="input">
</mat-form-field>
<p i18n="No default admin detected explanation">No default admin account detected. This will create and set the password for an admin account with the user name as 'admin'.</p>
</div>
<div style="position: relative">
<div>
<mat-form-field color="accent">
<mat-label i18n="Password">Password</mat-label>
<input type="password" (keyup.enter)="create()" matInput [(ngModel)]="input">
</mat-form-field>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button [disabled]="input.length === 0" color="accent" style="margin-bottom: 12px;" (click)="create()" mat-raised-button><ng-container i18n="Create">Create</ng-container></button>
<div class="spinner-div">
@if (creating) {
<mat-spinner [diameter]="25"></mat-spinner>
}
</div>
<button [disabled]="input.length === 0" color="accent" style="margin-bottom: 12px;" (click)="create()" mat-raised-button><ng-container i18n="Create">Create</ng-container></button>
<div class="spinner-div"><mat-spinner [diameter]="25" *ngIf="creating"></mat-spinner></div>
</mat-dialog-actions>

View File

@@ -1,32 +1,31 @@
<h4 mat-dialog-title>
@if (is_playlist) {
<ng-container i18n="Share playlist dialog title">Share playlist</ng-container>
} @else {
<ng-container i18n="Share video dialog title">Share file</ng-container>
}
<ng-container *ngIf="is_playlist" i18n="Share playlist dialog title">Share playlist</ng-container>
<ng-container *ngIf="!is_playlist" i18n="Share video dialog title">Share file</ng-container>
</h4>
<mat-dialog-content>
<div>
<div>
<mat-checkbox [checked]="sharing_enabled" (change)="sharingChanged($event)" [disabled]="uuid && (!postsService.isLoggedIn || postsService.user?.uid !== uuid)"><ng-container i18n="Enable sharing checkbox">Enable sharing</ng-container></mat-checkbox>
<div>
<mat-checkbox [checked]="sharing_enabled" (change)="sharingChanged($event)" [disabled]="uuid && (!postsService.isLoggedIn || postsService.user?.uid !== uuid)"><ng-container i18n="Enable sharing checkbox">Enable sharing</ng-container></mat-checkbox>
</div>
<div>
<mat-checkbox style="margin-right: 15px;" (change)="useTimestampChanged()" [(ngModel)]="timestamp_enabled"><ng-container i18n="Use timestamp">Use timestamp</ng-container></mat-checkbox>
<mat-form-field>
<mat-label i18n="Seconds">Seconds</mat-label>
<input matInput type="number" [(ngModel)]="current_timestamp" [disabled]="!timestamp_enabled" (change)="timestampInputChanged($event)">
</mat-form-field>
</div>
<div>
<mat-form-field style="width: 100%">
<input matInput [disabled]="!sharing_enabled" [readonly]="true" [value]="share_url">
</mat-form-field>
</div>
<div style="margin-bottom: 10px;">
<button color="accent" (click)="copiedToClipboard()" [disabled]="!sharing_enabled" [cdkCopyToClipboard]="share_url" mat-raised-button><ng-container i18n="Copy to clipboard button">Copy to clipboard</ng-container></button>
</div>
</div>
<div>
<mat-checkbox style="margin-right: 15px;" (change)="useTimestampChanged()" [(ngModel)]="timestamp_enabled"><ng-container i18n="Use timestamp">Use timestamp</ng-container></mat-checkbox>
<mat-form-field>
<mat-label i18n="Seconds">Seconds</mat-label>
<input matInput type="number" [(ngModel)]="current_timestamp" [disabled]="!timestamp_enabled" (change)="timestampInputChanged($event)">
</mat-form-field>
</div>
<div>
<mat-form-field style="width: 100%">
<input matInput [disabled]="!sharing_enabled" [readonly]="true" [value]="share_url">
</mat-form-field>
</div>
<div style="margin-bottom: 10px;">
<button color="accent" (click)="copiedToClipboard()" [disabled]="!sharing_enabled" [cdkCopyToClipboard]="share_url" mat-raised-button><ng-container i18n="Copy to clipboard button">Copy to clipboard</ng-container></button>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close button">Close</ng-container></button>
<button mat-button mat-dialog-close><ng-container i18n="Close button">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,91 +1,87 @@
<h4 mat-dialog-title i18n="Subscribe dialog title">Subscribe to playlist or channel</h4>
<mat-dialog-content>
<div class="container-fluid">
<div class="row">
<div class="col-12 mb-3">
<mat-form-field color="accent">
<mat-label i18n="URL">URL</mat-label>
<input [(ngModel)]="url" matInput required aria-required="true">
<mat-hint><ng-container i18n="Subscription URL input hint">The playlist or channel URL</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-12 mb-3">
<mat-form-field color="accent">
<mat-label i18n="URL">URL</mat-label>
<input [(ngModel)]="url" matInput required aria-required="true">
<mat-hint><ng-container i18n="Subscription URL input hint">The playlist or channel URL</ng-container></mat-hint>
</mat-form-field>
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="container-fluid">
<div class="row">
<div class="col-12 mt-2">
<mat-form-field color="accent">
<mat-label i18n="Custom name">Custom name</mat-label>
<input [(ngModel)]="name" matInput>
</mat-form-field>
</div>
<div class="col-12">
<mat-checkbox [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
</div>
<div class="col-12">
<span i18n="Download time range prefix">Download videos uploaded in the last</span>
<div>
<mat-form-field color="accent" style="width: 100px; text-align: center;">
<input type="number" matInput [(ngModel)]="timerange_amount" [disabled]="download_all">
</mat-form-field>
<mat-form-field class="unit-select">
<mat-select color="accent" [(ngModel)]="timerange_unit" [disabled]="download_all">
@for (time_unit of time_units; track time_unit) {
<mat-option [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-divider></mat-divider>
<div class="container-fluid">
<div class="row">
<div class="col-12 mt-2">
<mat-form-field color="accent">
<mat-label i18n="Custom name">Custom name</mat-label>
<input [(ngModel)]="name" matInput>
</mat-form-field>
</div>
<div class="col-12">
<mat-checkbox [(ngModel)]="download_all"><ng-container i18n="Download all uploads subscription setting">Download all uploads</ng-container></mat-checkbox>
</div>
<div class="col-12">
<span i18n="Download time range prefix">Download videos uploaded in the last</span>
<div>
<mat-form-field color="accent" style="width: 100px; text-align: center;">
<input type="number" matInput [(ngModel)]="timerange_amount" [disabled]="download_all">
</mat-form-field>
<mat-form-field class="unit-select">
<mat-select color="accent" [(ngModel)]="timerange_unit" [disabled]="download_all">
<mat-option *ngFor="let time_unit of time_units" [value]="time_unit + (timerange_amount === 1 ? '' : 's')">
{{time_unit + (timerange_amount === 1 ? '' : 's')}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="col-12 mt-2">
<mat-form-field>
<mat-label i18n="Max quality">Max quality</mat-label>
<mat-select [disabled]="audioOnlyMode" [(ngModel)]="maxQuality">
<mat-option *ngFor="let available_quality of available_qualities" [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12 mb-4">
<mat-form-field color="accent">
<mat-label i18n="Custom args">Custom args</mat-label>
<input [(ngModel)]="customArgs" matInput>
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-hint>
<ng-container i18n="Custom args hint">These are added after the standard args.</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div class="col-12">
<mat-form-field color="accent">
<mat-label i18n="Custom file output">Custom file output</mat-label>
<input [(ngModel)]="customFileOutput" matInput>
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
</div>
</div>
<div class="col-12 mt-2">
<mat-form-field>
<mat-label i18n="Max quality">Max quality</mat-label>
<mat-select [disabled]="audioOnlyMode" [(ngModel)]="maxQuality">
@for (available_quality of available_qualities; track available_quality) {
<mat-option [value]="available_quality['value']">{{available_quality['label']}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="col-12">
<div>
<mat-checkbox [(ngModel)]="audioOnlyMode"><ng-container i18n="Streaming-only mode">Audio-only mode</ng-container></mat-checkbox>
</div>
</div>
<div class="col-12 mb-4">
<mat-form-field color="accent">
<mat-label i18n="Custom args">Custom args</mat-label>
<input [(ngModel)]="customArgs" matInput>
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-hint>
<ng-container i18n="Custom args hint">These are added after the standard args.</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div class="col-12">
<mat-form-field color="accent">
<mat-label i18n="Custom file output">Custom file output</mat-label>
<input [(ngModel)]="customFileOutput" matInput>
<mat-hint>
<a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Custom output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Subscribe cancel button">Cancel</ng-container></button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button [disabled]="!url" type="submit" (click)="subscribeClicked()"><ng-container i18n="Subscribe button">Subscribe</ng-container></button>
@if (subscribing) {
<div class="mat-spinner">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
}
</mat-dialog-actions>
<div class="mat-spinner" *ngIf="subscribing">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -1,31 +1,27 @@
<h4 mat-dialog-title>{{sub.name}}
@if (sub.paused) {
&nbsp;<ng-container i18n="Paused suffix">(Paused)</ng-container>
}
</h4>
<h4 mat-dialog-title>{{sub.name}}&nbsp;<ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></h4>
<mat-dialog-content>
<div class="info-item">
<strong><ng-container i18n="Subscription type property">Type:</ng-container>&nbsp;</strong>
<span class="info-item-value">{{(sub.isPlaylist ? 'Playlist' : 'Channel')}}</span>
</div>
<div class="info-item">
<strong><ng-container i18n="Subscription URL property">URL:</ng-container>&nbsp;</strong>
<span class="info-item-value">{{sub.url}}</span>
</div>
<div class="info-item">
<strong><ng-container i18n="Subscription ID property">ID:</ng-container>&nbsp;</strong>
<span class="info-item-value">{{sub.id}}</span>
</div>
@if (sub.archive) {
<div class="info-item">
<strong><ng-container i18n="Subscription ID property">Archive:</ng-container>&nbsp;</strong>
<span class="info-item-value">{{sub.archive}}</span>
<strong><ng-container i18n="Subscription type property">Type:</ng-container>&nbsp;</strong>
<span class="info-item-value">{{(sub.isPlaylist ? 'Playlist' : 'Channel')}}</span>
</div>
<div class="info-item">
<strong><ng-container i18n="Subscription URL property">URL:</ng-container>&nbsp;</strong>
<span class="info-item-value">{{sub.url}}</span>
</div>
<div class="info-item">
<strong><ng-container i18n="Subscription ID property">ID:</ng-container>&nbsp;</strong>
<span class="info-item-value">{{sub.id}}</span>
</div>
<div class="info-item" *ngIf="sub.archive">
<strong><ng-container i18n="Subscription ID property">Archive:</ng-container>&nbsp;</strong>
<span class="info-item-value">{{sub.archive}}</span>
</div>
}
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close subscription info button">Close</ng-container></button>
<button mat-stroked-button (click)="downloadArchive()" color="accent"><ng-container i18n="Export Archive button">Export Archive</ng-container></button>
<span class="spacer"></span>
<button mat-button (click)="confirmUnsubscribe()" color="warn"><ng-container i18n="Unsubscribe button">Unsubscribe</ng-container></button>
<button mat-button mat-dialog-close><ng-container i18n="Close subscription info button">Close</ng-container></button>
<button mat-stroked-button (click)="downloadArchive()" color="accent"><ng-container i18n="Export Archive button">Export Archive</ng-container></button>
<span class="spacer"></span>
<button mat-button (click)="confirmUnsubscribe()" color="warn"><ng-container i18n="Unsubscribe button">Unsubscribe</ng-container></button>
</mat-dialog-actions>

View File

@@ -3,7 +3,6 @@ import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dial
import { PostsService } from 'app/posts.services';
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component';
import { Subscription } from 'api-types';
import { saveAs } from 'file-saver';
@Component({
selector: 'app-subscription-info-dialog',

View File

@@ -1,26 +1,18 @@
<h4 i18n="Update progress dialog title" mat-dialog-title>Updater</h4>
<mat-dialog-content>
@if (updateStatus) {
<div>
<div style="margin-bottom: 8px;">
@if (updateStatus['updating']) {
<h6>Update in progress</h6>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
} @else {
@if (updateStatus['error']) {
<h6>Update failed</h6>
} @else {
<h6>Update succeeded!</h6>
}
<mat-progress-bar mode="determinate" value="100"></mat-progress-bar>
}
</div>
@if (updateStatus['details']) {
<p style="margin-top: 4px; font-size: 13px;">{{updateStatus['details']}}</p>
}
<div *ngIf="updateStatus">
<div style="margin-bottom: 8px;">
<h6 *ngIf="updateStatus['updating']">Update in progress</h6>
<h6 *ngIf="!updateStatus['updating'] && updateStatus['error']">Update failed</h6>
<h6 *ngIf="!updateStatus['updating'] && !updateStatus['error']">Update succeeded!</h6>
</div>
<mat-progress-bar *ngIf="updateStatus['updating']" mode="indeterminate"></mat-progress-bar>
<mat-progress-bar *ngIf="!updateStatus['updating']" mode="determinate" value="100"></mat-progress-bar>
<p style="margin-top: 4px; font-size: 13px;" *ngIf="updateStatus['details']">{{updateStatus['details']}}</p>
</div>
}
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close update progress dialog">Close</ng-container></button>
<button mat-button mat-dialog-close><ng-container i18n="Close update progress dialog">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,59 +1,54 @@
<h4 mat-dialog-title><ng-container i18n="Update task schedule">Update task schedule</ng-container></h4>
<mat-dialog-content>
<div class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox [(ngModel)]="enabled"><ng-container i18n="Enabled">Enabled</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2">
<mat-checkbox [(ngModel)]="recurring" [disabled]="!enabled"><ng-container i18n="Recurring">Recurring</ng-container></mat-checkbox>
</div>
@if (recurring) {
<div class="col-12 mt-2">
<mat-form-field>
<mat-select placeholder="Interval" [(ngModel)]="interval" [disabled]="!enabled">
<mat-option value="weekly">Weekly</mat-option>
<mat-option value="daily">Daily</mat-option>
</mat-select>
</mat-form-field>
<div class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox [(ngModel)]="enabled"><ng-container i18n="Enabled">Enabled</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2">
<mat-checkbox [(ngModel)]="recurring" [disabled]="!enabled"><ng-container i18n="Recurring">Recurring</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2" *ngIf="recurring">
<mat-form-field>
<mat-select placeholder="Interval" [(ngModel)]="interval" [disabled]="!enabled">
<mat-option value="weekly">Weekly</mat-option>
<mat-option value="daily">Daily</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="!recurring" class="col-12 mt-2">
<mat-form-field>
<mat-label i18n="Choose a date">Choose a date</mat-label>
<input [(ngModel)]="date" [min]="today" matInput [matDatepicker]="picker" [disabled]="!enabled">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
<div *ngIf="recurring && interval === 'weekly'" class="col-12 mt-2">
<mat-button-toggle-group [(ngModel)]="days_of_week" [multiple]="true" [disabled]="!enabled" aria-label="Week day">
<!-- TODO: support translation -->
<mat-button-toggle [value]="0">M</mat-button-toggle>
<mat-button-toggle [value]="1">T</mat-button-toggle>
<mat-button-toggle [value]="2">W</mat-button-toggle>
<mat-button-toggle [value]="3">T</mat-button-toggle>
<mat-button-toggle [value]="4">F</mat-button-toggle>
<mat-button-toggle [value]="5">S</mat-button-toggle>
<mat-button-toggle [value]="6">S</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div class="col-12 mt-2">
<mat-form-field>
<mat-label>Time</mat-label>
<input type="time" matInput [(ngModel)]="time" [disabled]="!enabled">
<mat-hint *ngIf="Intl?.DateTimeFormat().resolvedOptions().timeZone">{{Intl.DateTimeFormat().resolvedOptions().timeZone}}</mat-hint>
</mat-form-field>
</div>
</div>
@if (interval === 'weekly') {
<div class="col-12 mt-2">
<mat-button-toggle-group [(ngModel)]="days_of_week" [multiple]="true" [disabled]="!enabled" aria-label="Week day">
<!-- TODO: support translation -->
<mat-button-toggle [value]="0">M</mat-button-toggle>
<mat-button-toggle [value]="1">T</mat-button-toggle>
<mat-button-toggle [value]="2">W</mat-button-toggle>
<mat-button-toggle [value]="3">T</mat-button-toggle>
<mat-button-toggle [value]="4">F</mat-button-toggle>
<mat-button-toggle [value]="5">S</mat-button-toggle>
<mat-button-toggle [value]="6">S</mat-button-toggle>
</mat-button-toggle-group>
</div>
}
} @else {
<div class="col-12 mt-2">
<mat-form-field>
<mat-label i18n="Choose a date">Choose a date</mat-label>
<input [(ngModel)]="date" [min]="today" matInput [matDatepicker]="picker" [disabled]="!enabled">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
}
<div class="col-12 mt-2">
<mat-form-field>
<mat-label>Time</mat-label>
<input type="time" matInput [(ngModel)]="time" [disabled]="!enabled">
@if (Intl?.DateTimeFormat().resolvedOptions().timeZone) {
<mat-hint>{{Intl.DateTimeFormat().resolvedOptions().timeZone}}</mat-hint>
}
</mat-form-field>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Update task schedule cancel button">Cancel</ng-container></button>
<button mat-button (click)="updateTaskSchedule()"><ng-container i18n="Update button">Update</ng-container></button>
<button mat-button mat-dialog-close><ng-container i18n="Update task schedule cancel button">Cancel</ng-container></button>
<button mat-button (click)="updateTaskSchedule()"><ng-container i18n="Update button">Update</ng-container></button>
</mat-dialog-actions>

View File

@@ -1,68 +1,64 @@
<h4 mat-dialog-title i18n="User profile dialog title">Your Profile</h4>
<mat-dialog-content>
@if (postsService.isLoggedIn && postsService.user) {
<div>
<div>
<strong><ng-container i18n="Name">Name:</ng-container></strong>&nbsp;{{postsService.user.name}}
</div>
<div>
<strong><ng-container i18n="UID">UID:</ng-container></strong>&nbsp;{{postsService.user.uid}}
</div>
<div>
<strong><ng-container i18n="Created">Created:</ng-container></strong>&nbsp;{{postsService.user.created ? (postsService.user.created | date) : 'N/A'}}
</div>
<div style="margin-top: 20px;">
</div>
<mat-divider style="margin-bottom: 20px"></mat-divider>
<div *ngIf="postsService.isLoggedIn && postsService.user">
<div>
<strong><ng-container i18n="Name">Name:</ng-container></strong>&nbsp;{{postsService.user.name}}
</div>
<div>
<strong><ng-container i18n="UID">UID:</ng-container></strong>&nbsp;{{postsService.user.uid}}
</div>
<div>
<strong><ng-container i18n="Created">Created:</ng-container></strong>&nbsp;{{postsService.user.created ? (postsService.user.created | date) : 'N/A'}}
</div>
<div style="margin-top: 20px;">
</div>
<mat-divider style="margin-bottom: 20px"></mat-divider>
</div>
}
<mat-form-field color="accent">
<mat-label><ng-container i18n="Language select label">Language</ng-container></mat-label>
<mat-select (selectionChange)="localeSelectChanged($event.value)" [(value)]="initialLocale">
@for (locale of supported_locales; track locale) {
<mat-option [value]="locale">
@if (all_locales[locale]) {
{{all_locales[locale]['nativeName']}}
}
</mat-option>
}
</mat-select>
</mat-form-field>
<br/>
<mat-form-field>
<mat-label i18n="Sidepanel mode">Sidepanel mode</mat-label>
<mat-select [(ngModel)]="sidepanel_mode" (selectionChange)="sidePanelModeChanged($event.value)">
<mat-option i18n="Over" value="over">
Over
</mat-option>
<mat-option i18n="Side" value="side">
Side
</mat-option>
</mat-select>
</mat-form-field>
<br/>
<mat-form-field>
<mat-label i18n="File card size">File card size</mat-label>
<mat-select [(ngModel)]="card_size" (selectionChange)="cardSizeOptionChanged($event.value)">
<mat-option i18n="Large" value="large">
Large
</mat-option>
<mat-option i18n="Medium" value="medium">
Medium
</mat-option>
<mat-option i18n="Small" value="small">
Small
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field color="accent">
<mat-label><ng-container i18n="Language select label">Language</ng-container></mat-label>
<mat-select (selectionChange)="localeSelectChanged($event.value)" [(value)]="initialLocale">
<mat-option *ngFor="let locale of supported_locales" [value]="locale">
<ng-container *ngIf="all_locales[locale]">
{{all_locales[locale]['nativeName']}}
</ng-container>
</mat-option>
</mat-select>
</mat-form-field>
<br/>
<mat-form-field>
<mat-label i18n="Sidepanel mode">Sidepanel mode</mat-label>
<mat-select [(ngModel)]="sidepanel_mode" (selectionChange)="sidePanelModeChanged($event.value)">
<mat-option i18n="Over" value="over">
Over
</mat-option>
<mat-option i18n="Side" value="side">
Side
</mat-option>
</mat-select>
</mat-form-field>
<br/>
<mat-form-field>
<mat-label i18n="File card size">File card size</mat-label>
<mat-select [(ngModel)]="card_size" (selectionChange)="cardSizeOptionChanged($event.value)">
<mat-option i18n="Large" value="large">
Large
</mat-option>
<mat-option i18n="Medium" value="medium">
Medium
</mat-option>
<mat-option i18n="Small" value="small">
Small
</mat-option>
</mat-select>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<div style="width: 100%">
<div style="position: relative">
<button mat-stroked-button mat-dialog-close color="primary"><ng-container i18n="Close">Close</ng-container></button>
@if (postsService.isLoggedIn) {
<button style="position: absolute; right: 0px;" (click)="logoutClicked()" mat-stroked-button color="warn"><ng-container i18n="Logout">Logout</ng-container></button>
}
<div style="width: 100%">
<div style="position: relative">
<button mat-stroked-button mat-dialog-close color="primary"><ng-container i18n="Close">Close</ng-container></button>
<button *ngIf="postsService.isLoggedIn" style="position: absolute; right: 0px;" (click)="logoutClicked()" mat-stroked-button color="warn"><ng-container i18n="Logout">Logout</ng-container></button>
</div>
</div>
</div>
</mat-dialog-actions>

View File

@@ -1,84 +1,83 @@
<h4 mat-dialog-title>
{{file.title}}
<div class="favorite-button">
<button [disabled]="!initialized || retrieving_file || !write_access" mat-icon-button (click)="editing = !editing"><mat-icon>edit</mat-icon></button>
<button [disabled]="!initialized || retrieving_file || !write_access" (click)="toggleFavorite()" mat-icon-button ><mat-icon>{{file.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon></button>
</div>
</h4>
<mat-dialog-content>
<mat-form-field class="info-field">
<mat-label i18n="Name">Name</mat-label>
<input [(ngModel)]="new_file.title" matInput [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="URL">URL</mat-label>
<input [(ngModel)]="new_file.url" matInput [disabled]="!editing">
<button mat-icon-button matSuffix (click)="window.open(new_file.url, '_blank')">
<mat-icon>link</mat-icon>
</button>
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Uploader">Uploader</mat-label>
<input [(ngModel)]="new_file.uploader" matInput [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Upload date">Upload date</mat-label>
<input [value]="upload_date" matInput [matDatepicker]="picker" (dateChange)="uploadDateChanged($event)" [disabled]="!editing">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Thumbnail path">Thumbnail path</mat-label>
<input [(ngModel)]="new_file.thumbnailPath" matInput [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Thumbnail URL">Thumbnail URL</mat-label>
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
</mat-form-field>
@if (initialized && postsService.categories) {
<mat-form-field class="info-field">
<mat-label i18n="Category">Category</mat-label>
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-option [value]="{}">
N/A
</mat-option>
@for (available_category of postsService.categories | keyvalue; track available_category) {
<mat-option [value]="available_category.value">
{{available_category.value['name']}}
</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field class="info-field">
<mat-label i18n="View count">View count</mat-label>
<input type="number" [(ngModel)]="new_file.view_count" matInput [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Local view count">Local view count</mat-label>
<input type="number" [(ngModel)]="new_file.local_view_count" matInput [disabled]="!editing">
</mat-form-field>
<mat-divider style="margin-bottom: 16px;"></mat-divider>
@if (!new_file.isAudio) {
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video resolution property">Resolution:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.height ? new_file.height + 'p' : 'N/A'}}</div>
{{file.title}}
<div class="favorite-button">
<button [disabled]="!initialized || retrieving_file || !write_access" mat-icon-button (click)="editing = !editing"><mat-icon>edit</mat-icon></button>
<button [disabled]="!initialized || retrieving_file || !write_access" (click)="toggleFavorite()" mat-icon-button ><mat-icon>{{file.favorite ? 'favorite_filled' : 'favorite_outline'}}</mat-icon></button>
</div>
}
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video audio bitrate property">Audio bitrate:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.abr ? new_file.abr + ' Kbps' : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video file size property">File size:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.size ? filesize(new_file.size) : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video path property">Path:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.path ? new_file.path : 'N/A'}}</div>
</div>
</h4>
<mat-dialog-content>
<mat-form-field class="info-field">
<mat-label i18n="Name">Name</mat-label>
<input [(ngModel)]="new_file.title" matInput [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="URL">URL</mat-label>
<input [(ngModel)]="new_file.url" matInput [disabled]="!editing">
<button mat-icon-button matSuffix (click)="window.open(new_file.url, '_blank')">
<mat-icon>link</mat-icon>
</button>
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Uploader">Uploader</mat-label>
<input [(ngModel)]="new_file.uploader" matInput [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Upload date">Upload date</mat-label>
<input [value]="upload_date" matInput [matDatepicker]="picker" (dateChange)="uploadDateChanged($event)" [disabled]="!editing">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Thumbnail path">Thumbnail path</mat-label>
<input [(ngModel)]="new_file.thumbnailPath" matInput [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Thumbnail URL">Thumbnail URL</mat-label>
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
</mat-form-field>
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
<mat-label i18n="Category">Category</mat-label>
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-option [value]="{}">
N/A
</mat-option>
<mat-option *ngFor="let available_category of postsService.categories | keyvalue" [value]="available_category.value">
{{available_category.value['name']}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="View count">View count</mat-label>
<input type="number" [(ngModel)]="new_file.view_count" matInput [disabled]="!editing">
</mat-form-field>
<mat-form-field class="info-field">
<mat-label i18n="Local view count">Local view count</mat-label>
<input type="number" [(ngModel)]="new_file.local_view_count" matInput [disabled]="!editing">
</mat-form-field>
<mat-divider style="margin-bottom: 16px;"></mat-divider>
<div *ngIf="!new_file.isAudio" class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video resolution property">Resolution:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.height ? new_file.height + 'p' : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video audio bitrate property">Audio bitrate:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.abr ? new_file.abr + ' Kbps' : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video file size property">File size:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.size ? filesize(new_file.size) : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video path property">Path:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{new_file.path ? new_file.path : 'N/A'}}</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close video info button">Close</ng-container></button>
<button mat-button [disabled]="!metadataChanged()" (click)="saveChanges()"><ng-container i18n="Save video info button">Save</ng-container></button>
<button mat-button mat-dialog-close><ng-container i18n="Close video info button">Close</ng-container></button>
<button mat-button [disabled]="!metadataChanged()" (click)="saveChanges()"><ng-container i18n="Save video info button">Save</ng-container></button>
</mat-dialog-actions>

View File

@@ -11,9 +11,7 @@
<button mat-button mat-dialog-close>Cancel</button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button [disabled]="!inputText" type="submit" (click)="enterPressed()">{{submitText}}</button>
@if (inputSubmitted) {
<div class="mat-spinner">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
}
<div class="mat-spinner" *ngIf="inputSubmitted">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -1,249 +1,216 @@
<br/>
<div class="big demo-basic">
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;" [ngClass]="(allowAdvancedDownload) ? 'no-border-radius-bottom' : 'border-radius-both'">
<mat-card-content style="padding: 0px 8px 0px 8px;">
<div style="position: relative; margin-right: 15px; margin-top: 20px;">
<form class="example-form">
<div class="container-fluid">
<div class="row">
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
<mat-form-field color="accent" class="example-full-width">
<textarea class="url-input" cdkTextareaAutosize cdkAutosizeMinRows="1" wrap="off" matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" #urlinput></textarea>
</mat-form-field>
<!--<button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>-->
</div>
@if (allowQualitySelect) {
<div class="col-7 col-sm-3">
<mat-form-field color="accent" style="display: inline-block; width: inherit; min-width: 120px;">
<mat-label>
<ng-container i18n="Quality select label">
Quality
</ng-container>
</mat-label>
<mat-select [disabled]="url === '' || cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']" [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality" (ngModelChange)="argsChanged()">
<mat-option i18n="Best" [value]="''">
Best
</mat-option>
@if (url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats && !cachedAvailableFormats[url]?.formats_failed) {
@for (option of cachedAvailableFormats[url]['formats'][audioOnly ? 'audio' : 'video']; track option) {
@if (option.key !== 'best_audio_format') {
<mat-option [matTooltip]="option.expected_filesize ? humanFileSize(option.expected_filesize) : null" [value]="option">
{{option.key}}
</mat-option>
}
}
}
@if (url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats_failed) {
@for (option of qualityOptions[audioOnly ? 'audio' : 'video']; track option) {
<mat-option [value]="option.value">
{{option.label}}
</mat-option>
}
}
</mat-select>
</mat-form-field>
@if (url !== '' && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']) {
<div class="spinner-div">
<mat-spinner [diameter]="25"></mat-spinner>
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;" [ngClass]="(allowAdvancedDownload) ? 'no-border-radius-bottom' : 'border-radius-both'">
<mat-card-content style="padding: 0px 8px 0px 8px;">
<div style="position: relative; margin-right: 15px; margin-top: 20px;">
<form class="example-form">
<div class="container-fluid">
<div class="row">
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
<mat-form-field color="accent" class="example-full-width">
<textarea class="url-input" cdkTextareaAutosize cdkAutosizeMinRows="1" wrap="off" matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" #urlinput></textarea>
</mat-form-field>
<!--<button type="button" class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>-->
</div>
<div *ngIf="allowQualitySelect" class="col-7 col-sm-3">
<mat-form-field color="accent" style="display: inline-block; width: inherit; min-width: 120px;">
<mat-label>
<ng-container i18n="Quality select label">
Quality
</ng-container>
</mat-label>
<mat-select [disabled]="url === '' || cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']" [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality" (ngModelChange)="argsChanged()">
<mat-option i18n="Best" [value]="''">
Best
</mat-option>
<ng-container *ngIf="url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats && !cachedAvailableFormats[url]?.formats_failed">
<ng-container *ngFor="let option of cachedAvailableFormats[url]['formats'][audioOnly ? 'audio' : 'video']">
<mat-option [matTooltip]="option.expected_filesize ? humanFileSize(option.expected_filesize) : null" *ngIf="option.key !== 'best_audio_format'" [value]="option">
{{option.key}}
</mat-option>
</ng-container>
</ng-container>
<ng-container *ngIf="url && cachedAvailableFormats && cachedAvailableFormats[url]?.formats_failed">
<ng-container *ngFor="let option of qualityOptions[audioOnly ? 'audio' : 'video']">
<mat-option [value]="option.value">
{{option.label}}
</mat-option>
</ng-container>
</ng-container>
</mat-select>
</mat-form-field>
<div class="spinner-div" *ngIf="url !== '' && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</div>
</div>
</div>
}
</div>
}
<div class="results-div" *ngIf="results_showing">
<span *ngFor="let result of results; let i = index">
<mat-card appearance="outlined" class="result-card mat-elevation-z7" [ngClass]="[(i === 0 && results.length > 1) ? 'first-result-card' : '', ((i === results.length-1) && results.length > 1) ? 'last-result-card' : '', (results.length === 1) ? 'only-result-card' : '']">
<div class="search-card-title">
{{result.title}}
</div>
<div style="font-size: 12px; margin-bottom: 10px;">
{{result.uploaded}}
</div>
<button mat-flat-button color="primary" style="float: left;" (click)="useURL(result.videoUrl)">
<ng-container i18n="YT search Use URL button for searched video">Use URL</ng-container>
</button>
<button mat-stroked-button color="primary" (click)="visitURL(result.videoUrl)" style="float: right">
<ng-container i18n="YT search View button for searched video">
View
</ng-container>
</button>
</mat-card>
</span>
</div>
</form>
<br/>
<mat-checkbox [disabled]="autoplay && !!current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px; margin-left: 4px;">
<ng-container i18n="Only Audio checkbox">
Only Audio
</ng-container>
</mat-checkbox>
<mat-checkbox *ngIf="!forceAutoplay" [disabled]="getURLArray(url).length > 1" (change)="autoplayChanged($event)" [(ngModel)]="autoplay" style="float: right; margin-top: -12px">
<ng-container i18n="Autoplay checkbox">
Autoplay
</ng-container>
</mat-checkbox>
</div>
</div>
@if (results_showing) {
<div class="results-div">
@for (result of results; track result; let i = $index) {
<span>
<mat-card appearance="outlined" class="result-card mat-elevation-z7" [ngClass]="[(i === 0 && results.length > 1) ? 'first-result-card' : '', ((i === results.length-1) && results.length > 1) ? 'last-result-card' : '', (results.length === 1) ? 'only-result-card' : '']">
<div class="search-card-title">
{{result.title}}
</div>
<div style="font-size: 12px; margin-bottom: 10px;">
{{result.uploaded}}
</div>
<button mat-flat-button color="primary" style="float: left;" (click)="useURL(result.videoUrl)">
<ng-container i18n="YT search Use URL button for searched video">Use URL</ng-container>
</button>
<button mat-stroked-button color="primary" (click)="visitURL(result.videoUrl)" style="float: right">
<ng-container i18n="YT search View button for searched video">
View
</ng-container>
</button>
</mat-card>
</span>
}
</div>
}
</form>
<br/>
<mat-checkbox [disabled]="autoplay && !!current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px; margin-left: 4px;">
<ng-container i18n="Only Audio checkbox">
Only Audio
</ng-container>
</mat-checkbox>
@if (!forceAutoplay) {
<mat-checkbox [disabled]="getURLArray(url).length > 1" (change)="autoplayChanged($event)" [(ngModel)]="autoplay" style="float: right; margin-top: -12px">
<ng-container i18n="Autoplay checkbox">
Autoplay
</ng-container>
</mat-checkbox>
}
</div>
</mat-card-content>
<mat-card-actions>
<button style="margin-left: 8px; margin-bottom: 8px" (click)="downloadClicked()" [disabled]="downloadingfile || url === ''" type="submit" mat-stroked-button color="accent">
<ng-container i18n="Main download button">
Download
</ng-container>
</button>
@if (!!current_download) {
<button (click)="cancelDownload()" style="margin-left: 8px; margin-bottom: 8px" mat-stroked-button color="warn">
<ng-container i18n="Cancel download button">
Cancel
</ng-container>
</button>
}
</mat-card-actions>
</mat-card>
</mat-card-content>
<mat-card-actions>
<button style="margin-left: 8px; margin-bottom: 8px" (click)="downloadClicked()" [disabled]="downloadingfile || url === ''" type="submit" mat-stroked-button color="accent">
<ng-container i18n="Main download button">
Download
</ng-container>
</button>
<button (click)="cancelDownload()" style="margin-left: 8px; margin-bottom: 8px" *ngIf="!!current_download" mat-stroked-button color="warn">
<ng-container i18n="Cancel download button">
Cancel
</ng-container>
</button>
</mat-card-actions>
</mat-card>
</div>
@if (allowAdvancedDownload) {
<div class="big demo-basic">
<div *ngIf="allowAdvancedDownload" class="big demo-basic">
<form style="margin-left: 20px; margin-right: 20px;">
<mat-expansion-panel class="big no-border-radius-top">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Advanced download mode panel">
Advanced
</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
@if (this.simulatedOutput) {
<p>
<ng-container i18n="Simulated command label">
Simulated command:
</ng-container>
&nbsp;<i>{{this.simulatedOutput}}</i></p>
}
<div class="container" style="padding-bottom: 20px;">
<div class="row">
<div class="col-12 col-sm-6">
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="customArgsEnabledChanged($event)" [(ngModel)]="customArgsEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use custom args checkbox">
Use custom args
</ng-container>
</mat-checkbox>
<button class="edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-checkbox color="accent" [disabled]="!customArgsEnabled || !!current_download" (change)="replaceArgsChanged($event)" [(ngModel)]="replaceArgs" style="z-index: 999; margin-left: 10px" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Replace args">
Replace args
</ng-container>
</mat-checkbox>
<mat-form-field color="accent" style="margin-bottom: 42px;" class="advanced-input">
<mat-label i18n="Custom args">Custom args</mat-label>
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput (ngModelChange)="argsChanged()">
<mat-hint>
<ng-container i18n="Custom Args input hint">
No need to include URL, just everything after. Args are delimited using two commas like so: ,,
<mat-expansion-panel class="big no-border-radius-top">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Advanced download mode panel">
Advanced
</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div class="col-12 col-sm-6">
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="customOutputEnabledChanged($event)" [(ngModel)]="customOutputEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use custom output checkbox">
Use custom output
</ng-container>
</mat-checkbox>
<mat-form-field style="margin-bottom: 42px;" color="accent" class="advanced-input">
<mat-label i18n="Custom output">Custom output</mat-label>
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput (ngModelChange)="argsChanged()">
<mat-hint><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Youtube-dl output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
@if (!youtubeAuthDisabledOverride) {
<div class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use authentication checkbox">
Use authentication
</ng-container>
</mat-checkbox>
@if (youtubeAuthEnabled) {
<mat-form-field color="accent" class="advanced-input">
<mat-label i18n="Username">Username</mat-label>
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()">
</mat-form-field>
}
</mat-panel-title>
</mat-expansion-panel-header>
<p *ngIf="this.simulatedOutput">
<ng-container i18n="Simulated command label">
Simulated command:
</ng-container>
&nbsp;<i>{{this.simulatedOutput}}</i></p>
<div class="container" style="padding-bottom: 20px;">
<div class="row">
<div class="col-12 col-sm-6">
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="customArgsEnabledChanged($event)" [(ngModel)]="customArgsEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use custom args checkbox">
Use custom args
</ng-container>
</mat-checkbox>
<button class="edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
<mat-checkbox color="accent" [disabled]="!customArgsEnabled || !!current_download" (change)="replaceArgsChanged($event)" [(ngModel)]="replaceArgs" style="z-index: 999; margin-left: 10px" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Replace args">
Replace args
</ng-container>
</mat-checkbox>
<mat-form-field color="accent" style="margin-bottom: 42px;" class="advanced-input">
<mat-label i18n="Custom args">Custom args</mat-label>
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput (ngModelChange)="argsChanged()">
<mat-hint>
<ng-container i18n="Custom Args input hint">
No need to include URL, just everything after. Args are delimited using two commas like so: ,,
</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div class="col-12 col-sm-6">
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="customOutputEnabledChanged($event)" [(ngModel)]="customOutputEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use custom output checkbox">
Use custom output
</ng-container>
</mat-checkbox>
<mat-form-field style="margin-bottom: 42px;" color="accent" class="advanced-input">
<mat-label i18n="Custom output">Custom output</mat-label>
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput (ngModelChange)="argsChanged()">
<mat-hint><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">
<ng-container i18n="Youtube-dl output template documentation link">Documentation</ng-container></a>.
<ng-container i18n="Custom Output input hint">Path is relative to the config download path. Don't include extension.</ng-container>
</mat-hint>
</mat-form-field>
</div>
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="!!current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Use authentication checkbox">
Use authentication
</ng-container>
</mat-checkbox>
<mat-form-field *ngIf="youtubeAuthEnabled" color="accent" class="advanced-input">
<mat-label i18n="Username">Username</mat-label>
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()">
</mat-form-field>
</div>
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-3">
<mat-form-field *ngIf="youtubeAuthEnabled" style="margin-top: 40px;" color="accent" class="advanced-input">
<mat-label i18n="Password">Password</mat-label>
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()">
</mat-form-field>
</div>
<div class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="!!current_download" [(ngModel)]="cropFile" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Crop video checkbox">
Crop file
</ng-container>
</mat-checkbox>
<mat-form-field *ngIf="cropFile" color="accent" class="advanced-input">
<mat-label i18n="Crop from (seconds)">Crop from (seconds)</mat-label>
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" matInput>
</mat-form-field>
</div>
<div class="col-12 col-sm-6 mt-3">
<mat-form-field *ngIf="cropFile" style="margin-top: 40px;" color="accent" class="advanced-input">
<mat-label i18n="Crop to (seconds)">Crop to (seconds)</mat-label>
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" matInput>
</mat-form-field>
</div>
</div>
<div class="col-12 col-sm-6 mt-3">
@if (youtubeAuthEnabled) {
<mat-form-field style="margin-top: 40px;" color="accent" class="advanced-input">
<mat-label i18n="Password">Password</mat-label>
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" matInput (ngModelChange)="argsChanged()">
</mat-form-field>
}
</div>
}
<div class="col-12 col-sm-6 mt-3">
<mat-checkbox color="accent" [disabled]="!!current_download" [(ngModel)]="cropFile" style="z-index: 999" [ngModelOptions]="{standalone: true}">
<ng-container i18n="Crop video checkbox">
Crop file
</ng-container>
</mat-checkbox>
@if (cropFile) {
<mat-form-field color="accent" class="advanced-input">
<mat-label i18n="Crop from (seconds)">Crop from (seconds)</mat-label>
<input [(ngModel)]="cropFileStart" type="number" [ngModelOptions]="{standalone: true}" matInput>
</mat-form-field>
}
</div>
<div class="col-12 col-sm-6 mt-3">
@if (cropFile) {
<mat-form-field style="margin-top: 40px;" color="accent" class="advanced-input">
<mat-label i18n="Crop to (seconds)">Crop to (seconds)</mat-label>
<input [(ngModel)]="cropFileEnd" type="number" [ngModelOptions]="{standalone: true}" matInput>
</mat-form-field>
}
</div>
</div>
</div>
</mat-expansion-panel>
</form>
</div>
}
<br/>
@if (current_download && autoplay) {
<div class="centered big" id="bar_div">
<div class="margined">
@if (current_download.percent_complete && current_download.percent_complete > 1) {
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px">
</form>
</div>
<br/>
<div class="centered big" id="bar_div" *ngIf="current_download && autoplay">
<div class="margined">
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
<br/>
</div>
} @else {
<mat-progress-bar style="border-radius: 5px;" mode="indeterminate"></mat-progress-bar>
}
@if (+percentDownloaded > 99) {
<div class="spinner">
</div>
<div *ngIf="+percentDownloaded > 99" class="spinner">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
}
</div>
<br/>
</div>
<ng-template #indeterminateprogress>
<mat-progress-bar style="border-radius: 5px;" mode="indeterminate"></mat-progress-bar>
</ng-template>
</div>
}
@if (downloads && downloads.length > 0 && !autoplay) {
<div style="display: flex; justify-content: center;">
<app-downloads style="width: 80%; min-width: 350px; margin-bottom: 10px" [uids]="download_uids"></app-downloads>
</div>
}
@if (cachedFileManagerEnabled || fileManagerEnabled) {
<br/>
</div>
<div style="display: flex; justify-content: center;" *ngIf="downloads && downloads.length > 0 && !autoplay">
<app-downloads style="width: 80%; min-width: 350px; margin-bottom: 10px" [uids]="download_uids"></app-downloads>
</div>
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
<app-recent-videos #recentVideos></app-recent-videos>
<br/>
<h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
}
</ng-container>

View File

@@ -1,86 +1,64 @@
@if (playlist.length > 0 && show_player) {
<div style="height: 100%">
<div style="height: 100%" *ngIf="playlist.length > 0 && show_player">
<div style="height: 100%" [ngClass]="(currentItem.type === 'audio/mp3') ? null : 'container-video'">
<div style="max-width: 100%; margin-left: 0px; height: 100%">
<mat-drawer-container style="height: 100%" class="example-container" autosize>
<div style="height: fit-content" [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(currentItem.type === 'audio/mp3') ? postsService.theme.drawer_color : 'black'">
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="$any(media)" [src]="currentItem.src" id="singleVideo" preload="auto" controls playsinline>
</video>
@if (postsService['config']['API']['use_sponsorblock_API'] && api && playlist?.length > 0 && playlist[currentIndex]['type'] === 'video/mp4') {
<app-skip-ad-button (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [current_video]="playlist[currentIndex]" [playback_timestamp]="api.currentTime" class="skip-ad-button"></app-skip-ad-button>
}
</vg-player>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<div class="container">
<div class="row">
<div class="col-2 col-lg-1">
@if (db_file) {
{{db_file['local_view_count'] ? db_file['local_view_count']+1 : 1}}&nbsp;<ng-container i18n="View count label">views</ng-container>
}
</div>
<div style="white-space: pre-line;" class="col-8 col-lg-9">
@if (db_file && db_file['description']) {
<p>
<app-see-more [text]="db_file['description']"></app-see-more>
</p>
} @else {
<p i18n="No description" style="text-align: center;">
No description available.
</p>
}
</div>
<div class="col-2">
@if (db_playlist) {
<span class="buttons">
<button (click)="downloadContent()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon></button>
@if (downloading) {
<mat-spinner class="spinner" [diameter]="35"></mat-spinner>
}
@if ((!postsService.isLoggedIn || postsService.permissions.includes('sharing')) && !auto) {
<button (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
}
</span>
}
@if (db_file) {
<span class="buttons">
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>cloud_download</mat-icon></button>
@if (downloading) {
<mat-spinner class="spinner" [diameter]="35"></mat-spinner>
}
@if (!postsService.isLoggedIn || postsService.permissions.includes('sharing')) {
<button (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
}
</span>
}
@if (db_file || db_playlist) {
<button (click)="openFileInfoDialog()" mat-icon-button><mat-icon>info</mat-icon></button>
}
@if (db_file && db_file.url.includes('twitch.tv')) {
<button (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
}
</div>
</div>
<div style="max-width: 100%; margin-left: 0px; height: 100%">
<mat-drawer-container style="height: 100%" class="example-container" autosize>
<div style="height: fit-content" [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-col' : 'video-col'">
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(currentItem.type === 'audio/mp3') ? postsService.theme.drawer_color : 'black'">
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="$any(media)" [src]="currentItem.src" id="singleVideo" preload="auto" controls playsinline>
</video>
<app-skip-ad-button *ngIf="postsService['config']['API']['use_sponsorblock_API'] && api && playlist?.length > 0 && playlist[currentIndex]['type'] === 'video/mp4'" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [current_video]="playlist[currentIndex]" [playback_timestamp]="api.currentTime" class="skip-ad-button"></app-skip-ad-button>
</vg-player>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<div class="container">
<div class="row">
<div class="col-2 col-lg-1">
<ng-container *ngIf="db_file">{{db_file['local_view_count'] ? db_file['local_view_count']+1 : 1}}&nbsp;<ng-container i18n="View count label">views</ng-container></ng-container>
</div>
<div style="white-space: pre-line;" class="col-8 col-lg-9">
<ng-container *ngIf="db_file && db_file['description']">
<p>
<app-see-more [text]="db_file['description']"></app-see-more>
</p>
</ng-container>
<ng-container *ngIf="!db_file || !db_file['description']">
<p i18n="No description" style="text-align: center;">
No description available.
</p>
</ng-container>
</div>
<div class="col-2">
<span class="buttons" *ngIf="db_playlist">
<button (click)="downloadContent()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon></button>
<mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner>
<button *ngIf="(!postsService.isLoggedIn || postsService.permissions.includes('sharing')) && !auto" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
</span>
<span class="buttons" *ngIf="db_file">
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>cloud_download</mat-icon></button>
<mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner>
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('sharing')" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
</span>
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
<button (click)="openFileInfoDialog()" *ngIf="db_file || db_playlist" mat-icon-button><mat-icon>info</mat-icon></button>
<button *ngIf="db_file && db_file.url.includes('twitch.tv')" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
</div>
</div>
</div>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="true" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<app-concurrent-stream *ngIf="db_file && api && postsService.config" (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists']">
<ng-container *ngIf="api_ready && db_file && db_file.url.includes('twitch.tv')">
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
</ng-container>
</mat-drawer>
</mat-drawer-container>
</div>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="true" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
@for (playlist_item of playlist; track playlist_item; let i = $index) {
<mat-button-toggle cdkDrag [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
}
</mat-button-toggle-group>
</div>
@if (db_file && api && postsService.config) {
<app-concurrent-stream (setPlaybackRate)="setPlaybackRate($event)" (togglePlayback)="togglePlayback($event)" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [playing]="api.state === 'playing'" [uid]="uid" [playback_timestamp]="api.time.current/1000" [server_mode]="!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn"></app-concurrent-stream>
}
<mat-drawer #drawer class="example-sidenav" mode="side" position="end" [opened]="db_file && db_file['chat_exists']">
@if (api_ready && db_file && db_file.url.includes('twitch.tv')) {
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
}
</mat-drawer>
</mat-drawer-container>
</div>
</div>
</div>
}
</div>

View File

@@ -8,7 +8,6 @@ import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-m
import { DatabaseFile, FileType, Playlist } from '../../api-types';
import { TwitchChatComponent } from 'app/components/twitch-chat/twitch-chat.component';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { saveAs } from 'file-saver';
export interface IMedia {
title: string;

View File

@@ -4,7 +4,7 @@ import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { THEMES_CONFIG } from '../themes';
import { Router, ActivatedRouteSnapshot } from '@angular/router';
import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { DOCUMENT } from '@angular/common';
import { BehaviorSubject, Observable } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
@@ -122,7 +122,7 @@ import { MatDrawerMode } from '@angular/material/sidenav';
import { environment } from '../environments/environment';
@Injectable()
export class PostsService {
export class PostsService implements CanActivate {
path = '';
// local settings

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,27 @@
<div style="margin-top: 14px;">
<button class="back-button" (click)="goBack()" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
<div style="margin-bottom: 15px;">
@if (subscription) {
<h2 style="text-align: center;">
{{subscription.name}}
@if (subscription.paused) {
&nbsp;<ng-container i18n="Paused suffix">(Paused)</ng-container>
}
<button class="edit-button" (click)="editSubscription()" [disabled]="downloading" matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button><mat-icon class="save-icon">edit</mat-icon></button>
</h2>
}
@if (subscription && subscription.downloading) {
<mat-progress-bar style="width: 80%; margin: 0 auto; margin-top: 15px;" mode="indeterminate"></mat-progress-bar>
}
</div>
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
<br/>
<!-- Extra margin added for floating buttons to have room -->
@if (subscription) {
<div style="margin-bottom: 100px;">
<app-recent-videos #recentVideos [sub_id]="subscription.id"></app-recent-videos>
</div>
}
<div class="check-button">
@if (subscription.downloading) {
<button color="primary" (click)="cancelCheckSubscription()" [disabled]="cancel_clicked" matTooltip="Cancel subscription check" i18n-matTooltip="Cancel subscription check" mat-fab><mat-icon class="save-icon">cancel</mat-icon></button>
} @else {
<button color="primary" (click)="checkSubscription()" [disabled]="check_clicked" matTooltip="Check subscription" i18n-matTooltip="Check subscription" mat-fab><mat-icon class="save-icon">youtube_searched_for</mat-icon></button>
}
</div>
<button class="watch-button" color="primary" (click)="watchSubscription()" matTooltip="Play all" i18n-matTooltip="Play all" mat-fab><mat-icon class="save-icon">video_library</mat-icon></button>
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" matTooltip="Download zip" i18n-matTooltip="Download zip" mat-fab><mat-icon class="save-icon">save</mat-icon>
@if (downloading) {
<mat-spinner class="spinner" [diameter]="50"></mat-spinner>
}
</button>
<button class="back-button" (click)="goBack()" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
<div style="margin-bottom: 15px;">
<h2 style="text-align: center;" *ngIf="subscription">
{{subscription.name}}&nbsp;<ng-container *ngIf="subscription.paused" i18n="Paused suffix">(Paused)</ng-container>
<button class="edit-button" (click)="editSubscription()" [disabled]="downloading" matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button><mat-icon class="save-icon">edit</mat-icon></button>
</h2>
<mat-progress-bar style="width: 80%; margin: 0 auto; margin-top: 15px;" *ngIf="subscription && subscription.downloading" mode="indeterminate"></mat-progress-bar>
</div>
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
<br/>
<!-- Extra margin added for floating buttons to have room -->
<div style="margin-bottom: 100px;" *ngIf="subscription">
<app-recent-videos #recentVideos [sub_id]="subscription.id"></app-recent-videos>
</div>
<div class="check-button">
<ng-container *ngIf="subscription.downloading">
<button color="primary" (click)="cancelCheckSubscription()" [disabled]="cancel_clicked" matTooltip="Cancel subscription check" i18n-matTooltip="Cancel subscription check" mat-fab><mat-icon class="save-icon">cancel</mat-icon></button>
</ng-container>
<ng-container *ngIf="!subscription.downloading">
<button color="primary" (click)="checkSubscription()" [disabled]="check_clicked" matTooltip="Check subscription" i18n-matTooltip="Check subscription" mat-fab><mat-icon class="save-icon">youtube_searched_for</mat-icon></button>
</ng-container>
</div>
<button class="watch-button" color="primary" (click)="watchSubscription()" matTooltip="Play all" i18n-matTooltip="Play all" mat-fab><mat-icon class="save-icon">video_library</mat-icon></button>
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" matTooltip="Download zip" i18n-matTooltip="Download zip" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
</div>

View File

@@ -4,7 +4,6 @@ import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { EditSubscriptionDialogComponent } from 'app/dialogs/edit-subscription-dialog/edit-subscription-dialog.component';
import { Subscription } from 'api-types';
import { saveAs } from 'file-saver';
@Component({
selector: 'app-subscription',

View File

@@ -1,76 +1,60 @@
<br/>
<h2 i18n="Subscriptions title" style="text-align: center; margin-bottom: 15px;">Your subscriptions</h2>
<mat-divider style="width: 80%; margin: 0 auto"></mat-divider>
<br/>
<h4 i18n="Subscriptions channels title" style="text-align: center;">Channels</h4>
<mat-nav-list class="sub-nav-list">
@for (sub of channel_subscriptions; track sub) {
<mat-list-item style="pointer-events: none">
<a style="pointer-events: auto;" class="a-list-item" matListItemTitle (click)="goToSubscription(sub)">
@if (sub.name) {
<strong>{{ sub.name }}
@if (sub.paused) {
&nbsp;<ng-container i18n="Paused suffix">(Paused)</ng-container>
}
</strong>
} @else {
<div>
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
</div>
}
</a>
<div style="pointer-events: auto; color: unset" matListItemMeta>
<button matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button (click)="editSubscription(sub)">
<mat-icon>edit</mat-icon>
</button>
<button matTooltip="Info" i18n-matTooltip="Info" mat-icon-button (click)="showSubInfo(sub)">
<mat-icon>info</mat-icon>
</button>
</div>
<mat-list-item *ngFor="let sub of channel_subscriptions" style="pointer-events: none">
<a style="pointer-events: auto;" class="a-list-item" matListItemTitle (click)="goToSubscription(sub)">
<strong *ngIf="sub.name">{{ sub.name }}&nbsp;<ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></strong>
<div *ngIf="!sub.name">
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
</div>
</a>
<div style="pointer-events: auto; color: unset" matListItemMeta>
<button matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button (click)="editSubscription(sub)">
<mat-icon>edit</mat-icon>
</button>
<button matTooltip="Info" i18n-matTooltip="Info" mat-icon-button (click)="showSubInfo(sub)">
<mat-icon>info</mat-icon>
</button>
</div>
</mat-list-item>
}
</mat-nav-list>
@if (channel_subscriptions.length === 0 && subscriptions) {
<div style="width: 80%; margin: 0 auto; padding-left: 15px;">
</mat-nav-list>
<div style="width: 80%; margin: 0 auto; padding-left: 15px;" *ngIf="channel_subscriptions.length === 0 && subscriptions">
<p i18n="No channel subscriptions text">You have no channel subscriptions.</p>
</div>
}
</div>
<h4 i18n="Subscriptions playlists title" style="text-align: center; margin-top: 10px;">Playlists</h4>
<mat-nav-list class="sub-nav-list">
@for (sub of playlist_subscriptions; track sub) {
<mat-list-item style="pointer-events: none">
<a style="pointer-events: auto;" class="a-list-item" matListItemTitle (click)="goToSubscription(sub)">
@if (sub.name) {
<strong>{{ sub.name }}
@if (sub.paused) {
&nbsp;<ng-container i18n="Paused suffix">(Paused)</ng-container>
}
</strong>
} @else {
<div>
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
</div>
}
</a>
<div style="pointer-events: auto; color: unset" matListItemMeta>
<button matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button (click)="editSubscription(sub)">
<mat-icon>edit</mat-icon>
</button>
<button matTooltip="Info" i18n-matTooltip="Info" mat-icon-button (click)="showSubInfo(sub)">
<mat-icon>info</mat-icon>
</button>
</div>
<mat-list-item *ngFor="let sub of playlist_subscriptions" style="pointer-events: none">
<a style="pointer-events: auto;" class="a-list-item" matListItemTitle (click)="goToSubscription(sub)">
<strong *ngIf="sub.name">{{ sub.name }}&nbsp;<ng-container *ngIf="sub.paused" i18n="Paused suffix">(Paused)</ng-container></strong>
<div *ngIf="!sub.name">
<ng-container i18n="Subscription playlist not available text">Name not available. Channel retrieval in progress.</ng-container>
</div>
</a>
<div style="pointer-events: auto; color: unset" matListItemMeta>
<button matTooltip="Edit" i18n-matTooltip="Edit" mat-icon-button (click)="editSubscription(sub)">
<mat-icon>edit</mat-icon>
</button>
<button matTooltip="Info" i18n-matTooltip="Info" mat-icon-button (click)="showSubInfo(sub)">
<mat-icon>info</mat-icon>
</button>
</div>
</mat-list-item>
}
</mat-nav-list>
@if (playlist_subscriptions.length === 0 && subscriptions) {
<div style="width: 80%; margin: 0 auto; padding-left: 15px;">
<div style="width: 80%; margin: 0 auto; padding-left: 15px;" *ngIf="playlist_subscriptions.length === 0 && subscriptions">
<p i18n="No playlist subscriptions text">You have no playlist subscriptions.</p>
</div>
}
@if (subscriptions_loading) {
<div style="margin: 0 auto; width: 80%">
</div>
<div style="margin: 0 auto; width: 80%" *ngIf="subscriptions_loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
}
</div>
<button class="add-subscription-button" (click)="openSubscribeDialog()" matTooltip="Add subscription" i18n-matTooltip="Add subscription" mat-fab><mat-icon>add</mat-icon></button>

View File

@@ -1,28 +1,18 @@
<div style="display: block">
<div style="display: inline-block">
<ng-container i18n="Select a version">Select a version:</ng-container>
</div>
@if (availableVersions) {
<div style="display: inline-block; margin-left: 15px;">
<mat-form-field>
<mat-select [(ngModel)]="selectedVersion">
@for (version of availableVersionsFiltered; track version) {
<mat-option [value]="version['tag_name']">
{{version['tag_name'] + (version === latestStableRelease ? ' - Latest Stable' : '') + (version['tag_name'] === CURRENT_VERSION ? ' - Current Version' : '')}}
</mat-option>
}
</mat-select>
</mat-form-field>
<div style="display: inline-block">
<ng-container i18n="Select a version">Select a version:</ng-container>
</div>
}
@if (selectedVersion && selectedVersion !== CURRENT_VERSION) {
<div style="display: inline-block; margin-left: 15px;">
<button (click)="updateServer()" color="accent" mat-raised-button><mat-icon>update</mat-icon>&nbsp;&nbsp;
@if (selectedVersion > CURRENT_VERSION) {
<ng-container i18n="Upgrade to">Upgrade to</ng-container>
} @else {
<ng-container i18n="Downgrade to">Downgrade to</ng-container>
}&nbsp;{{selectedVersion}}</button>
<div *ngIf="availableVersions" style="display: inline-block; margin-left: 15px;">
<mat-form-field>
<mat-select [(ngModel)]="selectedVersion">
<mat-option *ngFor="let version of availableVersionsFiltered" [value]="version['tag_name']">
{{version['tag_name'] + (version === latestStableRelease ? ' - Latest Stable' : '') + (version['tag_name'] === CURRENT_VERSION ? ' - Current Version' : '')}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="selectedVersion && selectedVersion !== CURRENT_VERSION" style="display: inline-block; margin-left: 15px;">
<button (click)="updateServer()" color="accent" mat-raised-button><mat-icon>update</mat-icon>&nbsp;&nbsp;
<ng-container *ngIf="selectedVersion > CURRENT_VERSION">Upgrade to</ng-container><ng-container *ngIf="selectedVersion < CURRENT_VERSION">Downgrade to</ng-container>&nbsp;{{selectedVersion}}</button>
</div>
}
</div>

View File

@@ -224,7 +224,7 @@
</trans-unit>
<trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716">
<source>Settings</source>
<target state="translated" state-qualifier="leveraged-glossary">Configuraciones</target>
<target state="translated" state-qualifier="leveraged-glossary">Ajustes</target>
</trans-unit>
<trans-unit id="801b98c6f02fe3b32f6afa3ee854c99ed83474e6">
<source>URL</source>
@@ -532,7 +532,7 @@
</trans-unit>
<trans-unit id="6765b4c916060f6bc42d9bb69e80377dbcb5e4e9">
<source>Login</source>
<target state="translated">Identificarse</target>
<target state="translated">Iniciar Sesión</target>
</trans-unit>
<trans-unit id="bb694b49d408265c91c62799c2b3a7e3151c824d">
<source>Logout</source>
@@ -1800,7 +1800,7 @@
</trans-unit>
<trans-unit id="4027175717527633324" datatype="html">
<source>Getting info</source>
<target>Cogiendo info</target>
<target state="translated">Obteniendo info</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">59</context>
@@ -2530,7 +2530,7 @@
</trans-unit>
<trans-unit id="3961621815065792326" datatype="html">
<source>Failed to clear finished downloads!</source>
<target state="translated">¡Error al limpiar las descargas finalizadas!</target>
<target state="translated">¡No se pudo limpiar las descargas finalizadas!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">157</context>
@@ -2546,7 +2546,7 @@
</trans-unit>
<trans-unit id="5223827577229167333" datatype="html">
<source>Failed to pause all downloads! See server logs for more info.</source>
<target state="translated">¡No se pudieron pausar todas las descargas! Vea el registro del servidor para más información.</target>
<target state="translated">¡No se pudo pausar todas las descargas! Vea el registro del servidor para más información.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">178</context>
@@ -2734,7 +2734,7 @@
</trans-unit>
<trans-unit id="39921032161993566" datatype="html">
<source>Successfully created playlist!</source>
<target state="translated">¡Lista de reproducción creada con éxito!</target>
<target state="translated">¡Lista de reproducción creada exitosamente!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-playlists/custom-playlists.component.ts</context>
<context context-type="linenumber">56</context>
@@ -2742,7 +2742,7 @@
</trans-unit>
<trans-unit id="7114033980971410157" datatype="html">
<source>Failed to pause download! See server logs for more info.</source>
<target state="translated">¡Error al pausar las descargas! Vea el registro del servidor para más información.</target>
<target state="translated">¡No se pudo pausar las descargas! Vea el registro del servidor para más información.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">170</context>
@@ -3169,7 +3169,7 @@
</trans-unit>
<trans-unit id="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html">
<source>Upload</source>
<target state="translated">Cargado</target>
<target state="translated">Subir</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">137</context>
@@ -3206,7 +3206,7 @@
</trans-unit>
<trans-unit id="3159807825117518005" datatype="html">
<source>Delete archives</source>
<target state="translated">Borrar los archivos</target>
<target state="translated">Borrar archivos</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">152</context>
@@ -3829,7 +3829,7 @@
</trans-unit>
<trans-unit id="8425787787095143143" datatype="html">
<source>Would you like to delete <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archive(s)?</source>
<target state="translated">¿ Quieres borrar el(los) archivo(s) de <x id="selected archives amount" equiv-text="this.selection.selected.length"/> ?</target>
<target state="translated">¿Quieres borrar el(los) archivo(s) de <x id="selected archives amount" equiv-text="this.selection.selected.length"/> ?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">153</context>
@@ -3962,7 +3962,7 @@
</trans-unit>
<trans-unit id="8456659390937171831" datatype="html">
<source>Show error</source>
<target state="translated">Mostrar el error</target>
<target state="translated">Mostrar error</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">56</context>

View File

@@ -2172,16 +2172,18 @@
</context-group>
<note priority="1" from="description">Simulated args title</note>
</trans-unit>
<trans-unit id="0b71824ae71972f236039bed43f8d2323e8fd570" datatype="html">
<trans-unit id="0b71824ae71972f236039bed43f8d2323e8fd570" datatype="html" xml:space="preserve">
<source>Add an arg</source>
<target state="translated">新增參數</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
<note priority="1" from="description">Add arg card title</note>
</trans-unit>
<trans-unit id="d618f383a0ea2458eeb945a85190d4a002ea394b" datatype="html">
<trans-unit id="d618f383a0ea2458eeb945a85190d4a002ea394b" datatype="html" xml:space="preserve">
<source>Arg</source>
<target state="translated">參數</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html</context>
<context context-type="linenumber">41</context>

View File

@@ -100,9 +100,9 @@
</context-group>
<note priority="1" from="description">Arg value placeholder</note>
</trans-unit>
<trans-unit id="7de2451ed3fb8d8b847979bd3f0c740b970f167b" datatype="html">
<trans-unit id="7de2451ed3fb8d8b847979bd3f0c740b970f167b" datatype="html" xml:space="preserve">
<source>Add arg</source>
<target>Ajouter</target>
<target state="translated">Ajouter un argument</target>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html</context>
<context context-type="linenumber">73</context>
@@ -1332,9 +1332,9 @@
</context-group>
<note priority="1" from="description">About bug prefix</note>
</trans-unit>
<trans-unit id="e1f398f38ff1534303d4bb80bd6cece245f24016" datatype="html">
<trans-unit id="e1f398f38ff1534303d4bb80bd6cece245f24016" datatype="html" xml:space="preserve">
<source>to create an issue!</source>
<target>pour signaler un problème !</target>
<target state="translated">pour signaler un problème!</target>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">25</context>
@@ -1995,9 +1995,9 @@
</context-group>
<note priority="1" from="description">Actions users table header</note>
</trans-unit>
<trans-unit id="4d92a0395dd66778a931460118626c5794a3fc7a" datatype="html">
<trans-unit id="4d92a0395dd66778a931460118626c5794a3fc7a" datatype="html" xml:space="preserve">
<source>Add Users</source>
<target>Ajouter</target>
<target state="translated">Ajout d'utilisateurs</target>
<context-group purpose="location">
<context context-type="sourcefile">app/components/modify-users/modify-users.component.html</context>
<context context-type="linenumber">90</context>
@@ -3010,9 +3010,9 @@
</context-group>
<note priority="1" from="description">Title</note>
</trans-unit>
<trans-unit id="47bbc861efa59ba4135e6aa8f63213420e3f3b91" datatype="html">
<trans-unit id="47bbc861efa59ba4135e6aa8f63213420e3f3b91" datatype="html" xml:space="preserve">
<source>Subscription</source>
<target>Souscription</target>
<target state="translated">Abonnement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">23</context>
@@ -4006,9 +4006,9 @@
<context context-type="linenumber">189</context>
</context-group>
</trans-unit>
<trans-unit id="8314249599019746316" datatype="html">
<trans-unit id="8314249599019746316" datatype="html" xml:space="preserve">
<source>Download failed!</source>
<target state="translated">Échec du téléchargement!</target>
<target state="translated">Le téléchargement a échoué!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">387</context>
@@ -4203,6 +4203,898 @@
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html" xml:space="preserve">
<source>Archives</source>
<target state="translated">Archives</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
</trans-unit>
<trans-unit id="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html" xml:space="preserve">
<source>Download archive</source>
<target state="translated">Télécharger l'archive</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">80</context>
</context-group>
</trans-unit>
<trans-unit id="8953483585652369683" datatype="html" xml:space="preserve">
<source>Archive successfully imported!</source>
<target state="translated">Archive importée avec succès!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit id="2525880134753073592" datatype="html" xml:space="preserve">
<source>Successfully deleted archive items!</source>
<target state="translated">Les archives ont bien été supprimées!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">172</context>
</context-group>
</trans-unit>
<trans-unit id="7022070615528435141" datatype="html" xml:space="preserve">
<source>Delete</source>
<target state="translated">Supprimer</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">154</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">160</context>
</context-group>
</trans-unit>
<trans-unit id="5a105e7bd7e7db6ea211fe950fc9f317379acceb" datatype="html" xml:space="preserve">
<source>No notifications available</source>
<target state="translated">Aucune notification</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html" xml:space="preserve">
<source>Name</source>
<target state="translated">Nom</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html" xml:space="preserve">
<source>Delete old files:</source>
<target state="translated">Supprimer les anciens fichiers :</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.html</context>
<context context-type="linenumber">66</context>
</context-group>
</trans-unit>
<trans-unit id="378c072ce05889c9771718d05106e7685fcd3507" datatype="html" xml:space="preserve">
<source>Medium</source>
<target state="translated">Moyenne</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">47,49</context>
</context-group>
</trans-unit>
<trans-unit id="9a865c2922f5c01899d06c472dba2e5bd63bcff9" datatype="html" xml:space="preserve">
<source>Small</source>
<target state="translated">Petite</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">50,52</context>
</context-group>
</trans-unit>
<trans-unit id="3371159074051387771" datatype="html" xml:space="preserve">
<source>Failed to delete <x id="category name" equiv-text="category['name']"/>!</source>
<target state="translated">Échec de la suppression de <x id="category name" equiv-text="category['name']"/>!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">172</context>
</context-group>
</trans-unit>
<trans-unit id="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html" xml:space="preserve">
<source>Filter</source>
<target state="translated">Filtres</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html" xml:space="preserve">
<source>ID</source>
<target state="translated">ID</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
</trans-unit>
<trans-unit id="51a161ce175abcd44f6c6cbd0e996681bf553ac3" datatype="html" xml:space="preserve">
<source>Delete selected</source>
<target state="translated">Supprimer la sélection</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">77</context>
</context-group>
</trans-unit>
<trans-unit id="28da11220a3377ddce3c7948825d33101f142782" datatype="html" xml:space="preserve">
<source>Extractor</source>
<target state="translated">Extracteur</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">57</context>
</context-group>
</trans-unit>
<trans-unit id="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html" xml:space="preserve">
<source>Archives empty</source>
<target state="translated">Archives vides</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html" xml:space="preserve">
<source>None</source>
<target state="translated">Aucun</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">84</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">126</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html" xml:space="preserve">
<source>Upload</source>
<target state="translated">Charger</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">137</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="6549265851868599441" datatype="html" xml:space="preserve">
<source>Video</source>
<target state="translated">Vidéo</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="347407180135731058" datatype="html" xml:space="preserve">
<source>Audio</source>
<target state="translated">Audio</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">44</context>
</context-group>
</trans-unit>
<trans-unit id="3159807825117518005" datatype="html" xml:space="preserve">
<source>Delete archives</source>
<target state="translated">Supprimer les archives</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">152</context>
</context-group>
</trans-unit>
<trans-unit id="8425787787095143143" datatype="html" xml:space="preserve">
<source>Would you like to delete <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archive(s)?</source>
<target state="translated">Voulez-vous supprimer <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archive(s)?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">153</context>
</context-group>
</trans-unit>
<trans-unit id="8224301330941792118" datatype="html" xml:space="preserve">
<source>Failed to delete archive items!</source>
<target state="translated">Échec de la suppression des archives!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="019d4bd6a5690f0cfa0ecf346a4e6bf7f0d8debb" datatype="html" xml:space="preserve">
<source>Remove</source>
<target state="translated">Reprendre</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="8564202903947049539" datatype="html" xml:space="preserve">
<source>Play</source>
<target state="translated">Lecture</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="8643601595923420698" datatype="html" xml:space="preserve">
<source>Retry download</source>
<target state="translated">Réessayer le téléchargement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="6876310993601590130" datatype="html" xml:space="preserve">
<source>Download completed</source>
<target state="translated">Téléchargement terminé</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="1879058637439215882" datatype="html" xml:space="preserve">
<source>Download error</source>
<target state="translated">Erreur de téléchargement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="4578192247039196794" datatype="html" xml:space="preserve">
<source>Task</source>
<target state="translated">Tâche</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="5000203534763292992" datatype="html" xml:space="preserve">
<source>Download restarted!</source>
<target state="translated">Le téléchargement a redémarré!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="3533826530554274875" datatype="html" xml:space="preserve">
<source>Upload Date</source>
<target state="translated">Date de chargement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="c65dd978b3c7566551c0ebefb234c2d41942b847" datatype="html" xml:space="preserve">
<source>Delete files older than</source>
<target state="translated">Supprimer les fichiers antérieurs à</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">6</context>
</context-group>
</trans-unit>
<trans-unit id="56b1a3c93fb95fed1805005c561a5e431d57ffae" datatype="html" xml:space="preserve">
<source>Blacklist all files</source>
<target state="translated">Liste noire pour tous les fichiers</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
</trans-unit>
<trans-unit id="d618f383a0ea2458eeb945a85190d4a002ea394b" datatype="html" xml:space="preserve">
<source>Arg</source>
<target state="translated">Argument</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="d57c023a4cf63b2f12c10328c15b636ff18929aa" datatype="html" xml:space="preserve">
<source>Best</source>
<target state="translated">Meilleure</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.html</context>
<context context-type="linenumber">24,25</context>
</context-group>
</trans-unit>
<trans-unit id="c40370dc182b5e4333828b70f7478bde58bb5cfe" datatype="html" xml:space="preserve">
<source>Enable notifications</source>
<target state="translated">Activer les notifications</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">349</context>
</context-group>
</trans-unit>
<trans-unit id="2361a4f76caaa4574803fbcdca8b0a47c91cc7ed" datatype="html" xml:space="preserve">
<source>Task finished</source>
<target state="translated">Tâche terminée</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">360</context>
</context-group>
</trans-unit>
<trans-unit id="6785427850041119037" datatype="html" xml:space="preserve">
<source>Delete category</source>
<target state="translated">Supprimer la catégorie</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">158</context>
</context-group>
</trans-unit>
<trans-unit id="0ed98b4c6ec1db6708a963e8a2699478ac97f55c" datatype="html" xml:space="preserve">
<source>Add subscription</source>
<target state="translated">Ajouter un abonnement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscriptions/subscriptions.component.html</context>
<context context-type="linenumber">60</context>
</context-group>
</trans-unit>
<trans-unit id="8c6e24eab969d9f63a8a0e9d617aee3b99e28ae6" datatype="html" xml:space="preserve">
<source>Play all</source>
<target state="translated">Lire tout</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="6080b77234e92ad41bb52653b239c4c4f851317d" datatype="html" xml:space="preserve">
<source>Error</source>
<target state="translated">Erreur</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">39</context>
</context-group>
</trans-unit>
<trans-unit id="3640026747176198246" datatype="html" xml:space="preserve">
<source>Watch content</source>
<target state="translated">Visionner le contenu</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit id="8456659390937171831" datatype="html" xml:space="preserve">
<source>Show error</source>
<target state="translated">Afficher l'erreur</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="1236604279860679031" datatype="html" xml:space="preserve">
<source>Restart</source>
<target state="translated">Redémarrer</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="9042260521669277115" datatype="html" xml:space="preserve">
<source>Pause</source>
<target state="translated">Mettre en pause</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="7182974689040833178" datatype="html" xml:space="preserve">
<source>Resume</source>
<target state="translated">Reprendre</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">74</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">80</context>
</context-group>
</trans-unit>
<trans-unit id="6219551536751479443" datatype="html" xml:space="preserve">
<source>Finished downloading</source>
<target state="translated">Fin du téléchargement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="5947241266456580665" datatype="html" xml:space="preserve">
<source>Download failed</source>
<target state="translated">Téléchargement échoué</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="8443034725057696949" datatype="html" xml:space="preserve">
<source>Task finished</source>
<target state="translated">Tâche terminée</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="8571838164752006148" datatype="html" xml:space="preserve">
<source>View error</source>
<target state="translated">Voir l'erreur</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="5709555629190115111" datatype="html" xml:space="preserve">
<source>View task</source>
<target state="translated">Voir la tâche</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="7911845622864460134" datatype="html" xml:space="preserve">
<source>Video only</source>
<target state="translated">Vidéo seulement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">55</context>
</context-group>
</trans-unit>
<trans-unit id="6437411876967154040" datatype="html" xml:space="preserve">
<source>Audio only</source>
<target state="translated">Audio seulement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">60</context>
</context-group>
</trans-unit>
<trans-unit id="4665451070906079743" datatype="html" xml:space="preserve">
<source>Favorited</source>
<target state="translated">Favoris</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">65</context>
</context-group>
</trans-unit>
<trans-unit id="6268070779441507380" datatype="html" xml:space="preserve">
<source>Download Date</source>
<target state="translated">Date de téléchargement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="2492098975665776610" datatype="html" xml:space="preserve">
<source>File Size</source>
<target state="translated">Taille de fichier</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="7410432243549869948" datatype="html" xml:space="preserve">
<source>Duration</source>
<target state="translated">Durée</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
<trans-unit id="cdf5297d8d080a78e8b10debc5c38b7845a3cbe7" datatype="html" xml:space="preserve">
<source>Do not ask for confirmation</source>
<target state="translated">Ne pas demander de confirmation</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="ea2b65121b93921fe54692025da9b9e3ce779ad5" datatype="html" xml:space="preserve">
<source>Task settings - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></source>
<target state="translated">Paramètres de la tâche <x id="INTERPOLATION" equiv-text="{{task.title}}"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="9aa1b4779a515170b297d2c0507e6ff9d2e3e0e0" datatype="html" xml:space="preserve">
<source>Blacklist deleted subscription files</source>
<target state="translated">Liste noire pour les fichiers d'abonnement supprimés</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
</trans-unit>
<trans-unit id="9176960997786930103" datatype="html" xml:space="preserve">
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
<target state="translated">Erreur pour : <x id="PH" equiv-text="task['title']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="11a0771f88158a540a54e0e4ec5d25733d65fc0e" datatype="html" xml:space="preserve">
<source>Favorite</source>
<target state="translated">Favori</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
</trans-unit>
<trans-unit id="1d4fa01d25990f60abf21c3a451fa8ba262b7912" datatype="html" xml:space="preserve">
<source>Unfavorite</source>
<target state="translated">Supprimer des favoris</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="c35ef0f03a863d33b04aae6807f140397a50f491" datatype="html" xml:space="preserve">
<source>Generate RSS URL</source>
<target state="translated">Générer une URL RSS</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">273</context>
</context-group>
</trans-unit>
<trans-unit id="1a9b415816364f554ee411020e65219092655271" datatype="html" xml:space="preserve">
<source>Title filter</source>
<target state="translated">Filtre de titre</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
</trans-unit>
<trans-unit id="9aa62bf1a535a97a4d752bbc5cf1c31af0f0c1f7" datatype="html" xml:space="preserve">
<source>Supports regex</source>
<target state="translated">Prend en charge les expressions rationnelles</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
</trans-unit>
<trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html" xml:space="preserve">
<source>User</source>
<target state="translated">Utilisateur</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="c2faa86201eab08b5b39b5437f96ab9432e125e7" datatype="html" xml:space="preserve">
<source>Item limit</source>
<target state="translated">Limite d'élément</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit id="b78a98bc54259a29cf6250dbaeab5fe11fae91cf" datatype="html" xml:space="preserve">
<source>Favorited</source>
<target state="translated">Favoris</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
</trans-unit>
<trans-unit id="8336047719608684263" datatype="html" xml:space="preserve">
<source>Unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/></source>
<target state="translated">Se désabonner de <x id="subscription name" equiv-text="this.sub['name']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="784837056777689544" datatype="html" xml:space="preserve">
<source>Would you like to unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/>?</source>
<target state="translated">Souhaitez-vous vous désabonner de <x id="subscription name" equiv-text="this.sub['name']"/>?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="1698114086921246480" datatype="html" xml:space="preserve">
<source>Unsubscribe</source>
<target state="translated">Se désabonner</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="1091872159779006651" datatype="html" xml:space="preserve">
<source>You must input a time!</source>
<target state="translated">Vous devez indiquer une heure!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.ts</context>
<context context-type="linenumber">70</context>
</context-group>
</trans-unit>
<trans-unit id="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html" xml:space="preserve">
<source>Sidepanel mode</source>
<target state="translated">Mode panneau latéral</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="338b44701a53ce3ef2f36abfb56f89c3edfa9eab" datatype="html" xml:space="preserve">
<source>Over</source>
<target state="translated">Au-dessus</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">32,34</context>
</context-group>
</trans-unit>
<trans-unit id="b6399391e706e2d7b7b7880eb5630e4e6f49728c" datatype="html" xml:space="preserve">
<source>Side</source>
<target state="translated">Sur le côté</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">35,37</context>
</context-group>
</trans-unit>
<trans-unit id="1f2809e6a99d511fdb6eaf041d785fe54d0680cc" datatype="html" xml:space="preserve">
<source>File card size</source>
<target state="translated">Taille des vignettes</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="e0a11fbea353b1ce1131161774e4a3e10bcb99b1" datatype="html" xml:space="preserve">
<source>Large</source>
<target state="translated">Grande</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">44,46</context>
</context-group>
</trans-unit>
<trans-unit id="af30e51aa8b67e1133a341ec28359be05150e65c" datatype="html" xml:space="preserve">
<source>No description available.</source>
<target state="translated">Aucune description disponible.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.html</context>
<context context-type="linenumber">25,27</context>
</context-group>
</trans-unit>
<trans-unit id="37469c9f3e31d95087fa22b6c9c3bc64adf1692d" datatype="html" xml:space="preserve">
<source>Enable RSS Feed</source>
<target state="translated">Activer le flux RSS</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">271</context>
</context-group>
</trans-unit>
<trans-unit id="35cf4cdcedc8ef3f94b6100e0d86836e31dbb908" datatype="html" xml:space="preserve">
<source>Force autoplay</source>
<target state="translated">Forcer la lecture automatique</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">218</context>
</context-group>
</trans-unit>
<trans-unit id="61d6b5fa4311b1c617b66dad72496f9dd43b07b4" datatype="html" xml:space="preserve">
<source>Be careful enabling this with multi-user mode! User data may be exposed.</source>
<target state="translated">Soyez prudent lorsque vous activez cette fonction en mode multi-utilisateurs! Les données de l'utilisateur peuvent être exposées.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">272</context>
</context-group>
</trans-unit>
<trans-unit id="fd467148a18f0921c10d116d4e0174fe29452be4" datatype="html" xml:space="preserve">
<source>See documentation here.</source>
<target state="translated">Voir la documentation ici.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">274</context>
</context-group>
</trans-unit>
<trans-unit id="8bcabdf6b16cad0313a86c7e940c5e3ad7f9f8ab" datatype="html" xml:space="preserve">
<source>Notifications</source>
<target state="translated">Notifications</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">343</context>
</context-group>
</trans-unit>
<trans-unit id="33a7c6d5ff3515fa237f1fd4e43df8b65373954d" datatype="html" xml:space="preserve">
<source>Enable all notifications</source>
<target state="translated">Activer toutes les notifications</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">352</context>
</context-group>
</trans-unit>
<trans-unit id="9c562d26e041390ecc3f49dabc51cc50ebba7469" datatype="html" xml:space="preserve">
<source>Allowed notification types</source>
<target state="translated">Types de notification autorisés</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">356</context>
</context-group>
</trans-unit>
<trans-unit id="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html" xml:space="preserve">
<source>Download complete</source>
<target state="translated">Téléchargement terminé</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">358</context>
</context-group>
</trans-unit>
<trans-unit id="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html" xml:space="preserve">
<source>Download error</source>
<target state="translated">Erreur de téléchargement</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">359</context>
</context-group>
</trans-unit>
<trans-unit id="9e766e11a9de375907aaf566897ecc6dac393ebc" datatype="html" xml:space="preserve">
<source>Webhook URL</source>
<target state="translated">URL du webhook</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">366</context>
</context-group>
</trans-unit>
<trans-unit id="3264d82792954815be755b3da01e2625458711dc" datatype="html" xml:space="preserve">
<source>Discord Webhook URL</source>
<target state="translated">URL du Webhook Discord</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">373</context>
</context-group>
</trans-unit>
<trans-unit id="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html" xml:space="preserve">
<source>See docs here.</source>
<target state="translated">Voir la documentation ici.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">375</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">382</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">392</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">402</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">409</context>
</context-group>
</trans-unit>
<trans-unit id="7cedb649779673568447b994463b2882c4e0436a" datatype="html" xml:space="preserve">
<source>Slack Webhook URL</source>
<target state="translated">URL Webhook de Slack</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">380</context>
</context-group>
</trans-unit>
<trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html" xml:space="preserve">
<source>Use ntfy API</source>
<target state="translated">Utiliser l'API ntfy</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">386</context>
</context-group>
</trans-unit>
<trans-unit id="06f503e492d6dbcf59e7b9c412ca86913d718689" datatype="html" xml:space="preserve">
<source>ntfy topic URL</source>
<target state="translated">URL du sujet ntfy</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">390</context>
</context-group>
</trans-unit>
<trans-unit id="5827fde8fcafdd55ae80921ad3ad4aa01012e203" datatype="html" xml:space="preserve">
<source>Use gotify API</source>
<target state="translated">Utiliser l'API gotify</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">396</context>
</context-group>
</trans-unit>
<trans-unit id="55f559d6f666b945479f534b0c182f70cd0a8a69" datatype="html" xml:space="preserve">
<source>Gotify server URL</source>
<target state="translated">URL du serveur Gotify</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">400</context>
</context-group>
</trans-unit>
<trans-unit id="b770c48628d98cb4633d6a17e3f0ba0265376af5" datatype="html" xml:space="preserve">
<source>Gotify app token</source>
<target state="translated">Jeton d'application Gotify</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">407</context>
</context-group>
</trans-unit>
<trans-unit id="38992954440d6afb54aeb58af12ca0123ee5e26e" datatype="html" xml:space="preserve">
<source>Use Telegram API</source>
<target state="translated">Utiliser l'API Telegram</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">413</context>
</context-group>
</trans-unit>
<trans-unit id="2e076ff9866213d0815961c494aa48b177046b9d" datatype="html" xml:space="preserve">
<source>Telegram bot token</source>
<target state="translated">Jeton du bot Telegram</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">417</context>
</context-group>
</trans-unit>
<trans-unit id="eeb0ba2e4743901d8f5eebd9a3529aa1f236c608" datatype="html" xml:space="preserve">
<source>Create bot here.</source>
<target state="translated">Créer un bot ici.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">419</context>
</context-group>
</trans-unit>
<trans-unit id="144e1a21ebe8fa238f88d2ac27515ed711cfc9a0" datatype="html" xml:space="preserve">
<source>Telegram chat ID</source>
<target state="translated">ID du chat Telegram</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">424</context>
</context-group>
</trans-unit>
<trans-unit id="3e420c675b8f3f3702576d52e8bb6e8e1d3feda0" datatype="html" xml:space="preserve">
<source>How do I get the chat ID?</source>
<target state="translated">Comment obtenir l'identifiant du chat?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">426</context>
</context-group>
</trans-unit>
<trans-unit id="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html" xml:space="preserve">
<source>Restart required.</source>
<target state="translated">Redémarrage nécessaire.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">446</context>
</context-group>
</trans-unit>
<trans-unit id="2481374649045841364" datatype="html" xml:space="preserve">
<source>Would you like to delete <x id="category name" equiv-text="category['name']"/>?</source>
<target state="translated">Souhaitez-vous supprimer <x id="category name" equiv-text="category['name']"/>?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="7332320960988475089" datatype="html" xml:space="preserve">
<source>Successfully deleted <x id="category name" equiv-text="category['name']"/>!</source>
<target state="translated">Suppression réussie <x id="category name" equiv-text="category['name']"/>!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">168</context>
</context-group>
</trans-unit>
<trans-unit id="674a999dd48d7da565ffdd105602261b8a4761ea" datatype="html" xml:space="preserve">
<source>Download zip</source>
<target state="translated">Télécharger le zip</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3325,6 +3325,42 @@
</context-group>
<note priority="1" from="description">Update button</note>
</trans-unit>
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html" xml:space="preserve">
<source>Archives</source>
<target state="translated">Ахриви</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Archives menu label</note>
</trans-unit>
<trans-unit id="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html" xml:space="preserve">
<source>Filter</source>
<target state="translated">Филтер</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<note priority="1" from="description">Filter</note>
</trans-unit>
<trans-unit id="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html" xml:space="preserve">
<source>Archives empty</source>
<target state="translated">Архивите се празни</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">72</context>
</context-group>
<note priority="1" from="description">Archives empty</note>
</trans-unit>
<trans-unit id="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html" xml:space="preserve">
<source>Download archive</source>
<target state="translated">Симни архива</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">80</context>
</context-group>
<note priority="1" from="description">Download archive</note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -4505,6 +4505,241 @@
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="3159807825117518005" datatype="html">
<source>Delete archives</source>
<target state="translated">Usuń archiwa</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">152</context>
</context-group>
</trans-unit>
<trans-unit id="8224301330941792118" datatype="html">
<source>Failed to delete archive items!</source>
<target state="translated">Nie udało się usunąć elementów archiwum!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="6219551536751479443" datatype="html">
<source>Finished downloading</source>
<target state="translated">Zakończono pobieranie</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="5947241266456580665" datatype="html">
<source>Download failed</source>
<target state="translated">Pobieranie nie udane</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="11a0771f88158a540a54e0e4ec5d25733d65fc0e" datatype="html">
<source>Favorite</source>
<target state="translated">Ulubione</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Favorite button</note>
</trans-unit>
<trans-unit id="5000203534763292992" datatype="html">
<source>Download restarted!</source>
<target state="translated">Pobieranie wznowione!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="56b1a3c93fb95fed1805005c561a5e431d57ffae" datatype="html" xml:space="preserve">
<source>Blacklist all files</source>
<target state="translated">Dodaj do listy blokowanie wszystkie pliki</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Blacklist deleted files</note>
</trans-unit>
<trans-unit id="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html" xml:space="preserve">
<source>Sidepanel mode</source>
<target state="translated">Tryb panelu bocznego</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
<note priority="1" from="description">Sidepanel mode</note>
</trans-unit>
<trans-unit id="c2faa86201eab08b5b39b5437f96ab9432e125e7" datatype="html" xml:space="preserve">
<source>Item limit</source>
<target state="translated">Lista elementów</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">46</context>
</context-group>
<note priority="1" from="description">Item limit</note>
</trans-unit>
<trans-unit id="8336047719608684263" datatype="html" xml:space="preserve">
<source>Unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/></source>
<target state="translated">Anuluj subskrypcję <x id="subscription name" equiv-text="this.sub['name']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="784837056777689544" datatype="html" xml:space="preserve">
<source>Would you like to unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/>?</source>
<target state="translated">Czy na pewno chcesz cofnąć subskrypcję dla <x id="subscription name" equiv-text="this.sub['name']"/>?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="c40370dc182b5e4333828b70f7478bde58bb5cfe" datatype="html" xml:space="preserve">
<source>Enable notifications</source>
<target state="translated">Włącz powiadomienia</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">349</context>
</context-group>
<note priority="1" from="description">Enable notifications setting</note>
</trans-unit>
<trans-unit id="3371159074051387771" datatype="html" xml:space="preserve">
<source>Failed to delete <x id="category name" equiv-text="category['name']"/>!</source>
<target state="translated">Nie udało się usunąć <x id="category name" equiv-text="category['name']"/>!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">172</context>
</context-group>
</trans-unit>
<trans-unit id="7332320960988475089" datatype="html" xml:space="preserve">
<source>Successfully deleted <x id="category name" equiv-text="category['name']"/>!</source>
<target state="translated">Usunięto subskrypcję <x id="category name" equiv-text="category['name']"/>!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">168</context>
</context-group>
</trans-unit>
<trans-unit id="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html" xml:space="preserve">
<source>Restart required.</source>
<target state="translated">Wymagane jest ponowne uruchomienie.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">446</context>
</context-group>
<note priority="1" from="description">Restart required hint</note>
</trans-unit>
<trans-unit id="674a999dd48d7da565ffdd105602261b8a4761ea" datatype="html" xml:space="preserve">
<source>Download zip</source>
<target state="translated">Pobierz zip</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/subscription/subscription/subscription.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
<note priority="1" from="description">Download zip</note>
</trans-unit>
<trans-unit id="c35ef0f03a863d33b04aae6807f140397a50f491" datatype="html" xml:space="preserve">
<source>Generate RSS URL</source>
<target state="translated">Generuj adres URL kanału RSS</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">273</context>
</context-group>
<note priority="1" from="description">Generate RSS URL</note>
</trans-unit>
<trans-unit id="b6399391e706e2d7b7b7880eb5630e4e6f49728c" datatype="html" xml:space="preserve">
<source>Side</source>
<target state="translated">Strona</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">35,37</context>
</context-group>
<note priority="1" from="description">Side</note>
</trans-unit>
<trans-unit id="8c1bf02206fbc371ff69ab1b7e35a17ba29d169d" datatype="html" xml:space="preserve">
<source>Use ntfy API</source>
<target state="translated">Użyj API ntfy</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">386</context>
</context-group>
<note priority="1" from="description">Use ntfy API setting</note>
</trans-unit>
<trans-unit id="eeb0ba2e4743901d8f5eebd9a3529aa1f236c608" datatype="html" xml:space="preserve">
<source>Create bot here.</source>
<target state="translated">Stwórz bota.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">419</context>
</context-group>
<note priority="1" from="description">Telegram bot create link</note>
</trans-unit>
<trans-unit id="6785427850041119037" datatype="html" xml:space="preserve">
<source>Delete category</source>
<target state="translated">Usuń kategorię</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">158</context>
</context-group>
</trans-unit>
<trans-unit id="9e766e11a9de375907aaf566897ecc6dac393ebc" datatype="html" xml:space="preserve">
<source>Webhook URL</source>
<target state="translated">Adres URL Webhook</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">366</context>
</context-group>
<note priority="1" from="description">webhook URL</note>
</trans-unit>
<trans-unit id="3264d82792954815be755b3da01e2625458711dc" datatype="html" xml:space="preserve">
<source>Discord Webhook URL</source>
<target state="translated">Adres URL Webhook Discord</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">373</context>
</context-group>
<note priority="1" from="description">Discord Webhook URL</note>
</trans-unit>
<trans-unit id="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html" xml:space="preserve">
<source>See docs here.</source>
<target state="translated">Zobacz dokumentacje tutaj.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">375</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">382</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">392</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">402</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">409</context>
</context-group>
<note priority="1" from="description">Discord API setting hint</note>
</trans-unit>
<trans-unit id="378c072ce05889c9771718d05106e7685fcd3507" datatype="html" xml:space="preserve">
<source>Medium</source>
<target state="translated">Średnia</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">47,49</context>
</context-group>
<note priority="1" from="description">Medium</note>
</trans-unit>
</body>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

View File

@@ -81,9 +81,9 @@
</context-group>
<note priority="1" from="description">Navigation menu Subscriptions Page title</note>
</trans-unit>
<trans-unit id="822fab38216f64e8166d368b59fe756ca39d301b" datatype="html">
<trans-unit id="822fab38216f64e8166d368b59fe756ca39d301b" datatype="html" xml:space="preserve">
<source>Downloads</source>
<target>Baixados</target>
<target state="translated">Descarregados</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">46</context>
@@ -99,9 +99,9 @@
</context-group>
<note priority="1" from="description">Only Audio checkbox</note>
</trans-unit>
<trans-unit id="6a21ba5fb0ac804a525bf9ab168038c3ee88e661" datatype="html">
<trans-unit id="6a21ba5fb0ac804a525bf9ab168038c3ee88e661" datatype="html" xml:space="preserve">
<source>Download</source>
<target>Baixar</target>
<target state="translated">Descarregar</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.html</context>
<context context-type="linenumber">74,75</context>
@@ -240,9 +240,9 @@
</context-group>
<note priority="1" from="description">Youtube-dl output template documentation link</note>
</trans-unit>
<trans-unit id="19d1ae64d94d28a29b2c57ae8671aace906b5401" datatype="html">
<trans-unit id="19d1ae64d94d28a29b2c57ae8671aace906b5401" datatype="html" xml:space="preserve">
<source>Path is relative to the config download path. Don't include extension.</source>
<target>Caminho é relativo a configuração do caminho de download. Não inclua a extensão.</target>
<target state="translated">Caminho é relativo à configuração do caminho de descargas. Não inclui a extensão.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.html</context>
<context context-type="linenumber">128</context>
@@ -420,9 +420,9 @@
</context-group>
<note priority="1" from="description">Subscription custom name placeholder</note>
</trans-unit>
<trans-unit id="ea30873bd3f0d5e4fb2378eec3f0a1db77634a28" datatype="html">
<trans-unit id="ea30873bd3f0d5e4fb2378eec3f0a1db77634a28" datatype="html" xml:space="preserve">
<source>Download all uploads</source>
<target>Baixar todos uploads</target>
<target state="translated">Descarregar todos envios</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">23</context>
@@ -536,9 +536,9 @@
</context-group>
<note priority="1" from="description">Subscribe button</note>
</trans-unit>
<trans-unit id="28a678e9cabf86e44c32594c43fa0e890135c20f" datatype="html">
<trans-unit id="28a678e9cabf86e44c32594c43fa0e890135c20f" datatype="html" xml:space="preserve">
<source>Download videos uploaded in the last</source>
<target>Baixe vídeos enviados no último</target>
<target state="translated">Descarregar vídeos enviados no último</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">26</context>
@@ -1023,9 +1023,9 @@
</context-group>
<note priority="1" from="description">Add new rule tooltip</note>
</trans-unit>
<trans-unit id="792dc6a57f28a1066db283f2e736484f066005fd" datatype="html">
<trans-unit id="792dc6a57f28a1066db283f2e736484f066005fd" datatype="html" xml:space="preserve">
<source>Download Twitch Chat</source>
<target>Baixar Twitch Chat</target>
<target state="translated">Descarregar Twitch Chat</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/twitch-chat/twitch-chat.component.html</context>
<context context-type="linenumber">10</context>
@@ -1266,9 +1266,9 @@
</context-group>
<note priority="1" from="description">Main settings label</note>
</trans-unit>
<trans-unit id="0ba25ad86a240576c4f20a2fada4722ebba77b1e" datatype="html">
<trans-unit id="0ba25ad86a240576c4f20a2fada4722ebba77b1e" datatype="html" xml:space="preserve">
<source>Downloader</source>
<target>Downloader</target>
<target state="translated">Descarregador</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">102</context>
@@ -1373,9 +1373,9 @@
</context-group>
<note priority="1" from="description">Users base path placeholder</note>
</trans-unit>
<trans-unit id="a64505c41150663968e277ec9b3ddaa5f4838798" datatype="html">
<trans-unit id="a64505c41150663968e277ec9b3ddaa5f4838798" datatype="html" xml:space="preserve">
<source>Base path for users and their downloaded videos.</source>
<target>Diretório base para usuários e seus vídeos baixados.</target>
<target state="translated">Diretório base para utilizadores e os seus vídeos descarregados.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">39</context>
@@ -1427,18 +1427,18 @@
</context-group>
<note priority="1" from="description">Check interval setting input hint</note>
</trans-unit>
<trans-unit id="13759b09a7f4074ceee8fa2f968f9815fdf63295" datatype="html">
<trans-unit id="13759b09a7f4074ceee8fa2f968f9815fdf63295" datatype="html" xml:space="preserve">
<source>Sometimes new videos are downloaded before being fully processed. This setting will mean new videos will be checked for a higher quality version the following day.</source>
<target>Algumas vezes vídeos são baixados antes de serem totalmente processados. Esta configuração faz com que vídeos novos sejam verificados por uma qualidade mais alta no dia seguinte.</target>
<target state="translated">Algumas vezes vídeos são descarregados antes de serem totalmente processados. Esta configuração faz com que vídeos novos sejam verificados por uma qualidade mais alta no dia seguinte.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">63</context>
</context-group>
<note priority="1" from="description">Redownload fresh uploads tooltip</note>
</trans-unit>
<trans-unit id="3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513" datatype="html">
<trans-unit id="3d1a47dc18b7bd8b5d9e1eb44b235ed9c4a2b513" datatype="html" xml:space="preserve">
<source>Redownload fresh uploads</source>
<target>Baixar novamente uploads recentes</target>
<target state="translated">Descarregar envios recentes novamente</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">63</context>
@@ -1490,9 +1490,9 @@
</context-group>
<note priority="1" from="description">Audio folder path input placeholder</note>
</trans-unit>
<trans-unit id="c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca" datatype="html">
<trans-unit id="c2c89cdf45d46ea64d2ed2f9ac15dfa4d77e26ca" datatype="html" xml:space="preserve">
<source>Path for audio only downloads. It is relative to YTDL-Material's root folder.</source>
<target>Diretório para downloads de 'áudio apenas'. Relativo ao diretório raiz do YTDL-Material.</target>
<target state="translated">Diretório para descargas de 'apenas áudio'. Relativo ao diretório raiz do YTDL-Material.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">110</context>
@@ -1508,9 +1508,9 @@
</context-group>
<note priority="1" from="description">Video folder path input placeholder</note>
</trans-unit>
<trans-unit id="17c92e6d47a213fa95b5aa344b3f258147123f93" datatype="html">
<trans-unit id="17c92e6d47a213fa95b5aa344b3f258147123f93" datatype="html" xml:space="preserve">
<source>Path for video downloads. It is relative to YTDL-Material's root folder.</source>
<target>Diretório para download de vídeos. Relativo ao diretório raiz do YTDL-Material.</target>
<target state="translated">Diretório para descargas de vídeos. Relativo ao diretório raiz do YTDL-Material.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">117</context>
@@ -1526,9 +1526,9 @@
</context-group>
<note priority="1" from="description">Default file output placeholder</note>
</trans-unit>
<trans-unit id="1148fd45287ff09955b938756bc302042bcb29c7" datatype="html">
<trans-unit id="1148fd45287ff09955b938756bc302042bcb29c7" datatype="html" xml:space="preserve">
<source>Path is relative to the above download paths. Don't include extension.</source>
<target>O caminho é relativo ao diretório de download acima. Não inclua extensão.</target>
<target state="translated">O caminho é relativo ao diretório de descargas acima. Não inclua extensão.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">126</context>
@@ -1589,9 +1589,9 @@
</context-group>
<note priority="1" from="description">Include metadata setting</note>
</trans-unit>
<trans-unit id="fb35145bfb84521e21b6385363d59221f436a573" datatype="html">
<trans-unit id="fb35145bfb84521e21b6385363d59221f436a573" datatype="html" xml:space="preserve">
<source>Kill all downloads</source>
<target>Parar todos os downloads</target>
<target state="translated">Parar todas as descargas</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">175</context>
@@ -1616,9 +1616,9 @@
</context-group>
<note priority="1" from="description">File manager enabled setting</note>
</trans-unit>
<trans-unit id="a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a" datatype="html">
<trans-unit id="a5a1be0a5df07de9eec57f5d2a86ed0204b2e75a" datatype="html" xml:space="preserve">
<source>Downloads manager enabled</source>
<target>Habilitar gerenciador de downloads</target>
<target state="translated">Ativar gestor de descargas</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">196</context>
@@ -1634,9 +1634,9 @@
</context-group>
<note priority="1" from="description">Allow quality seelct setting</note>
</trans-unit>
<trans-unit id="bda5508e24e0d77debb28bcd9194d8fefb1cfb92" datatype="html">
<trans-unit id="bda5508e24e0d77debb28bcd9194d8fefb1cfb92" datatype="html" xml:space="preserve">
<source>Download only mode</source>
<target>Modo Apenas Download</target>
<target state="translated">Modo apenas descargas</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">202</context>
@@ -1755,9 +1755,9 @@
</context-group>
<note priority="1" from="description">Twitch API Key setting hint AKA preamble</note>
</trans-unit>
<trans-unit id="5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9" datatype="html">
<trans-unit id="5fb1e0083c9b2a40ac8ae7dcb2618311c291b8b9" datatype="html" xml:space="preserve">
<source>Auto-download Twitch Chat</source>
<target>Baixar Twitch Chat automaticamente</target>
<target state="translated">Descarregar Twitch Chat automaticamente</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">244</context>
@@ -1781,9 +1781,9 @@
</context-group>
<note priority="1" from="description">Chrome ext click here</note>
</trans-unit>
<trans-unit id="7f09776373995003161235c0c8d02b7f91dbc4df" datatype="html">
<trans-unit id="7f09776373995003161235c0c8d02b7f91dbc4df" datatype="html" xml:space="preserve">
<source>to download the official YoutubeDL-Material Chrome extension manually.</source>
<target>para baixar a extensão do YoutubeDL-Material para o Chrome manualmente.</target>
<target state="translated">para descarregar a extensão do YoutubeDL-Material para o Chrome manualmente.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">259</context>
@@ -1826,9 +1826,9 @@
</context-group>
<note priority="1" from="description">Firefox setup suffix</note>
</trans-unit>
<trans-unit id="61b81b11aad0b9d970ece2fce18405f07eac69c2" datatype="html">
<trans-unit id="61b81b11aad0b9d970ece2fce18405f07eac69c2" datatype="html" xml:space="preserve">
<source>Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark.</source>
<target>Arraste o link abaixo para seus Favoritos, e pronto! Navegue para o vídeo do Youtube que deseja baixar e clique no link favoritado.</target>
<target state="translated">Arraste a ligação abaixo para os seus favoritos, e pronto! Navegue para o vídeo do Youtube que deseja descarregar e clique na ligação favorita.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">271</context>
@@ -1844,27 +1844,27 @@
</context-group>
<note priority="1" from="description">Generate audio only bookmarklet checkbox</note>
</trans-unit>
<trans-unit id="ec71e08aee647ea4a71fd6b7510c54d84a797ca6" datatype="html">
<trans-unit id="ec71e08aee647ea4a71fd6b7510c54d84a797ca6" datatype="html" xml:space="preserve">
<source>Select a downloader</source>
<target>Selecione um downloader</target>
<target state="translated">Selecione um descarregador</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">287</context>
</context-group>
<note priority="1" from="description">Default downloader select label</note>
</trans-unit>
<trans-unit id="5fab47f146b0a4b809dcebf3db9da94df6299ea1" datatype="html">
<trans-unit id="5fab47f146b0a4b809dcebf3db9da94df6299ea1" datatype="html" xml:space="preserve">
<source>Use default downloading agent</source>
<target>Usar agente de download padrão</target>
<target state="translated">Usar agente de descargas padrão</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">295</context>
</context-group>
<note priority="1" from="description">Use default downloading agent setting</note>
</trans-unit>
<trans-unit id="c776eb4992b6c98f58cd89b20c1ea8ac37888521" datatype="html">
<trans-unit id="c776eb4992b6c98f58cd89b20c1ea8ac37888521" datatype="html" xml:space="preserve">
<source>Select a download agent</source>
<target>Selecionar um agente de download</target>
<target state="translated">Selecionar um agente de descargas</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">299</context>
@@ -1889,9 +1889,9 @@
</context-group>
<note priority="1" from="description">Login expiration select label</note>
</trans-unit>
<trans-unit id="dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8" datatype="html">
<trans-unit id="dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8" datatype="html" xml:space="preserve">
<source>Allow advanced download</source>
<target>Permitir Download avançado</target>
<target state="translated">Permitir descarga avançada</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">336</context>
@@ -2177,9 +2177,9 @@
</context-group>
<note priority="1" from="description">Current session</note>
</trans-unit>
<trans-unit id="7117fc42f860e86d983bfccfcf2654e5750f3406" datatype="html">
<trans-unit id="7117fc42f860e86d983bfccfcf2654e5750f3406" datatype="html" xml:space="preserve">
<source>No downloads available!</source>
<target>Não há downloads disponíveis!</target>
<target state="translated">Não há descargas disponíveis!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">25</context>
@@ -2459,9 +2459,9 @@
</context-group>
<note priority="1" from="description">Go to subscription menu item</note>
</trans-unit>
<trans-unit id="94e01842dcee90531caa52e4147f70679bac87fe" datatype="html">
<trans-unit id="94e01842dcee90531caa52e4147f70679bac87fe" datatype="html" xml:space="preserve">
<source>Delete and redownload</source>
<target>Excluir e baixar novamente</target>
<target state="translated">Apagar e descarregar novamente</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">28</context>
@@ -2565,18 +2565,18 @@
<context context-type="linenumber">48</context>
</context-group>
</trans-unit>
<trans-unit id="4f9f174dc9939283b3192acc9e87d6c1e4cca118" datatype="html">
<trans-unit id="4f9f174dc9939283b3192acc9e87d6c1e4cca118" datatype="html" xml:space="preserve">
<source>Limits the amount of downloads that can be simultaneously downloaded. Use -1 for no limit.</source>
<target>Limita a quantidade de downloads simultâneos. Use -1 para ilimitado.</target>
<target state="translated">Limita a quantidade de descargas simultâneas. Use -1 para ilimitado.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">176</context>
</context-group>
<note priority="1" from="description">Max concurrent downloads input hint</note>
</trans-unit>
<trans-unit id="6c2714fbfa525868fea90cc7a8f8de62458fbecf" datatype="html">
<trans-unit id="6c2714fbfa525868fea90cc7a8f8de62458fbecf" datatype="html" xml:space="preserve">
<source>Max concurrent downloads</source>
<target>Número máximo de downloads simultâneos</target>
<target state="translated">Número máximo de descargas simultâneas</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">175</context>
@@ -2636,9 +2636,9 @@
</context-group>
<note priority="1" from="description">Title</note>
</trans-unit>
<trans-unit id="b36b7458192b833592e13029fa8a0b3555e0d9bd" datatype="html">
<trans-unit id="b36b7458192b833592e13029fa8a0b3555e0d9bd" datatype="html" xml:space="preserve">
<source>Pause all downloads</source>
<target>Pausar todos os downloads</target>
<target state="translated">Pausar todas as descargas</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">83</context>
@@ -2680,9 +2680,9 @@
</context-group>
<note priority="1" from="description">Clear</note>
</trans-unit>
<trans-unit id="7220285196408439810" datatype="html">
<source>Download for <x id="url" equiv-text="d, cropF"/> has been queued!</source>
<target>Download de <x id="url" equiv-text="d, cropF"/> foi adicionado à fila!</target>
<trans-unit id="7220285196408439810" datatype="html" xml:space="preserve">
<source>Download for <x id="url" equiv-text="url"/> has been queued!</source>
<target state="translated">A descarga de <x id="url" equiv-text="url"/> foi adicionado à fila!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.ts</context>
<context context-type="linenumber">469</context>
@@ -2773,18 +2773,18 @@
</context-group>
<note priority="1" from="description">Allow playlist categorization setting label</note>
</trans-unit>
<trans-unit id="a30fc9944a494022ba67b3046ad3d27c62dd7fee" datatype="html">
<trans-unit id="a30fc9944a494022ba67b3046ad3d27c62dd7fee" datatype="html" xml:space="preserve">
<source>Download rate limit</source>
<target>Taxa limite do download</target>
<target state="translated">Taxa limite de descargas</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">181</context>
</context-group>
<note priority="1" from="description">Download rate limit input placeholder</note>
</trans-unit>
<trans-unit id="0af5ee1867be592a6cd35a94faba8833b52c740f" datatype="html">
<trans-unit id="0af5ee1867be592a6cd35a94faba8833b52c740f" datatype="html" xml:space="preserve">
<source>Rate limits your downloads to the specified amount. Ex: 200K</source>
<target>Limita a taxa de seus downloads na quantidade especificada. Ex: 200K</target>
<target state="translated">Limita a taxa das suas descargas na quantidade especificada. Ex: 200K</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">182</context>
@@ -2930,9 +2930,9 @@
</context-group>
<note priority="1" from="description">Share video dialog title</note>
</trans-unit>
<trans-unit id="2827589726081052618" datatype="html">
<trans-unit id="2827589726081052618" datatype="html" xml:space="preserve">
<source>Creating download</source>
<target>Criando download</target>
<target state="translated">A criar descarga</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">58</context>
@@ -3068,9 +3068,9 @@
</context-group>
<note priority="1" from="description">Stage</note>
</trans-unit>
<trans-unit id="9b2084f9aea764292cf0978cb083907d8be51bf7" datatype="html">
<trans-unit id="9b2084f9aea764292cf0978cb083907d8be51bf7" datatype="html" xml:space="preserve">
<source>Resume all downloads</source>
<target>Resumir todos os downloads</target>
<target state="translated">Resumir todos as descargas</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">84</context>
@@ -3113,6 +3113,42 @@
</context-group>
<note priority="1" from="description">Navigation menu Tasks Page title</note>
</trans-unit>
<trans-unit id="45cc8ca94b5a50842a9a8ef804a5ab089a38ae5c" datatype="html" xml:space="preserve">
<source>ID</source>
<target state="translated">Identidade/identificação</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
<note priority="1" from="description">ID</note>
</trans-unit>
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html" xml:space="preserve">
<source>Archives</source>
<target state="translated">Arquivos</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<note priority="1" from="description">Archives menu label</note>
</trans-unit>
<trans-unit id="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html" xml:space="preserve">
<source>Filter</source>
<target state="translated">Filtrar</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
<note priority="1" from="description">Filter</note>
</trans-unit>
<trans-unit id="28da11220a3377ddce3c7948825d33101f142782" datatype="html" xml:space="preserve">
<source>Extractor</source>
<target state="translated">Extrator</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">57</context>
</context-group>
<note priority="1" from="description">Extractor</note>
</trans-unit>
</body>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3757,6 +3757,794 @@
</context-group>
<note priority="1" from="description">Select a version</note>
</trans-unit>
<trans-unit id="c748ac656af9f13998206ef2c52018dd418b0483" datatype="html" xml:space="preserve">
<source>Archives</source>
<target state="translated">Arşivler</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
</trans-unit>
<trans-unit id="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html" xml:space="preserve">
<source>Filter</source>
<target state="translated">Filtre</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="28da11220a3377ddce3c7948825d33101f142782" datatype="html" xml:space="preserve">
<source>Extractor</source>
<target state="translated">Dışa Çıkarıcı</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">57</context>
</context-group>
</trans-unit>
<trans-unit id="c150a30bbbdb175b4d08820196a9acb66b167653" datatype="html" xml:space="preserve">
<source>Archives empty</source>
<target state="translated">Arşivler boş</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="51a161ce175abcd44f6c6cbd0e996681bf553ac3" datatype="html" xml:space="preserve">
<source>Delete selected</source>
<target state="translated">Seçilenleri sil</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">77</context>
</context-group>
</trans-unit>
<trans-unit id="a2f14a73f7a6e94479f67423cc51102da8d6f524" datatype="html" xml:space="preserve">
<source>None</source>
<target state="translated">Hiçbiri</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">84</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">126</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="2d1ea268a6a9f483dbc2cbfe19bf4256a57a6af4" datatype="html" xml:space="preserve">
<source>Video</source>
<target state="translated">Video</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">92</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">133</context>
</context-group>
</trans-unit>
<trans-unit id="f0baeb8b69d120073b6d60d34785889b0c3232c8" datatype="html" xml:space="preserve">
<source>Audio</source>
<target state="translated">Ses</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">93</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">134</context>
</context-group>
</trans-unit>
<trans-unit id="4b3972c3e9485218508a95f7a4ce7758e3f09ced" datatype="html" xml:space="preserve">
<source>Upload</source>
<target state="translated">Yükle</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">137</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/cookies-uploader-dialog/cookies-uploader-dialog.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="6549265851868599441" datatype="html" xml:space="preserve">
<source>Video</source>
<target state="translated">Video</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="347407180135731058" datatype="html" xml:space="preserve">
<source>Audio</source>
<target state="translated">Ses</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">44</context>
</context-group>
</trans-unit>
<trans-unit id="8953483585652369683" datatype="html" xml:space="preserve">
<source>Archive successfully imported!</source>
<target state="translated">Arşiv başarıyla içe aktarıldı!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit id="3159807825117518005" datatype="html" xml:space="preserve">
<source>Delete archives</source>
<target state="translated">Arşivleri sil</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">152</context>
</context-group>
</trans-unit>
<trans-unit id="8425787787095143143" datatype="html" xml:space="preserve">
<source>Would you like to delete <x id="selected archives amount" equiv-text="this.selection.selected.length"/> archive(s)?</source>
<target state="translated"><x id="selected archives amount" equiv-text="this.selection.selected.length"/> arşivi silmek istiyor musunuz?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">153</context>
</context-group>
</trans-unit>
<trans-unit id="7022070615528435141" datatype="html" xml:space="preserve">
<source>Delete</source>
<target state="translated">Sil</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">154</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">160</context>
</context-group>
</trans-unit>
<trans-unit id="2525880134753073592" datatype="html" xml:space="preserve">
<source>Successfully deleted archive items!</source>
<target state="translated">Arşiv öğeleri başarıyla silindi!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">172</context>
</context-group>
</trans-unit>
<trans-unit id="8224301330941792118" datatype="html" xml:space="preserve">
<source>Failed to delete archive items!</source>
<target state="translated">Arşiv öğelerini silmek başarısız oldu!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="8443034725057696949" datatype="html" xml:space="preserve">
<source>Task finished</source>
<target state="translated">Görev tamamlandı</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="4665451070906079743" datatype="html" xml:space="preserve">
<source>Favorited</source>
<target state="translated">Favorilendi</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">65</context>
</context-group>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html" xml:space="preserve">
<source>Name</source>
<target state="translated">İsim</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="ea2b65121b93921fe54692025da9b9e3ce779ad5" datatype="html" xml:space="preserve">
<source>Task settings - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></source>
<target state="translated">Görev ayarları - <x id="INTERPOLATION" equiv-text="{{task.title}}"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="11a0771f88158a540a54e0e4ec5d25733d65fc0e" datatype="html" xml:space="preserve">
<source>Favorite</source>
<target state="translated">Favori</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
</trans-unit>
<trans-unit id="1f2809e6a99d511fdb6eaf041d785fe54d0680cc" datatype="html" xml:space="preserve">
<source>File card size</source>
<target state="translated">Dosya kardı boyutu</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="c35ef0f03a863d33b04aae6807f140397a50f491" datatype="html" xml:space="preserve">
<source>Generate RSS URL</source>
<target state="translated">RSS URLsi oluştur</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">273</context>
</context-group>
</trans-unit>
<trans-unit id="1a9b415816364f554ee411020e65219092655271" datatype="html" xml:space="preserve">
<source>Title filter</source>
<target state="translated">Başlık filtresi</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
</trans-unit>
<trans-unit id="b78a98bc54259a29cf6250dbaeab5fe11fae91cf" datatype="html" xml:space="preserve">
<source>Favorited</source>
<target state="translated">Favorilendi</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
</trans-unit>
<trans-unit id="1091872159779006651" datatype="html" xml:space="preserve">
<source>You must input a time!</source>
<target state="translated">Zaman girmelisiniz!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component.ts</context>
<context context-type="linenumber">70</context>
</context-group>
</trans-unit>
<trans-unit id="35cf4cdcedc8ef3f94b6100e0d86836e31dbb908" datatype="html" xml:space="preserve">
<source>Force autoplay</source>
<target state="translated">Otomatik oynatmayı zorla</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">218</context>
</context-group>
</trans-unit>
<trans-unit id="37469c9f3e31d95087fa22b6c9c3bc64adf1692d" datatype="html" xml:space="preserve">
<source>Enable RSS Feed</source>
<target state="translated">RSS Akışını Etkinleştir</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">271</context>
</context-group>
</trans-unit>
<trans-unit id="a4ed8eba1e057e67d5c2d87b52230f182b3dae4e" datatype="html" xml:space="preserve">
<source>Restart required.</source>
<target state="translated">Yeniden başlatma gerekli.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">446</context>
</context-group>
</trans-unit>
<trans-unit id="2481374649045841364" datatype="html" xml:space="preserve">
<source>Would you like to delete <x id="category name" equiv-text="category['name']"/>?</source>
<target state="translated"><x id="category name" equiv-text="category['name']"/> kategorisini silmek istiyor musunuz?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="6080b77234e92ad41bb52653b239c4c4f851317d" datatype="html" xml:space="preserve">
<source>Error</source>
<target state="translated">Hata</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.html</context>
<context context-type="linenumber">39</context>
</context-group>
</trans-unit>
<trans-unit id="8456659390937171831" datatype="html" xml:space="preserve">
<source>Show error</source>
<target state="translated">Hatayı göster</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="1236604279860679031" datatype="html" xml:space="preserve">
<source>Restart</source>
<target state="translated">Yeniden Başlat</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="9042260521669277115" datatype="html" xml:space="preserve">
<source>Pause</source>
<target state="translated">Durdur</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="7182974689040833178" datatype="html" xml:space="preserve">
<source>Resume</source>
<target state="translated">Devam et</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">74</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">80</context>
</context-group>
</trans-unit>
<trans-unit id="019d4bd6a5690f0cfa0ecf346a4e6bf7f0d8debb" datatype="html" xml:space="preserve">
<source>Remove</source>
<target state="translated">Kaldır</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="6219551536751479443" datatype="html" xml:space="preserve">
<source>Finished downloading</source>
<target state="translated">İndirme tamamlandı</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="5947241266456580665" datatype="html" xml:space="preserve">
<source>Download failed</source>
<target state="translated">İndirme başarısız</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="8564202903947049539" datatype="html" xml:space="preserve">
<source>Play</source>
<target state="translated">Oynat</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="8643601595923420698" datatype="html" xml:space="preserve">
<source>Retry download</source>
<target state="translated">İndirmeyi yeniden dene</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="8571838164752006148" datatype="html" xml:space="preserve">
<source>View error</source>
<target state="translated">Hatayı görüntüle</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="5709555629190115111" datatype="html" xml:space="preserve">
<source>View task</source>
<target state="translated">Görevi görüntüle</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications-list/notifications-list.component.ts</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="5a105e7bd7e7db6ea211fe950fc9f317379acceb" datatype="html" xml:space="preserve">
<source>No notifications available</source>
<target state="translated">Bildirim yok</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.html</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="6876310993601590130" datatype="html" xml:space="preserve">
<source>Download completed</source>
<target state="translated">İndirme tamamlandı</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="1879058637439215882" datatype="html" xml:space="preserve">
<source>Download error</source>
<target state="translated">İndirme hatası</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="4578192247039196794" datatype="html" xml:space="preserve">
<source>Task</source>
<target state="translated">Görev</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="5000203534763292992" datatype="html" xml:space="preserve">
<source>Download restarted!</source>
<target state="translated">İndirme yeniden başlatıldı!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/notifications/notifications.component.ts</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="7911845622864460134" datatype="html" xml:space="preserve">
<source>Video only</source>
<target state="translated">Yalnızca video</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">55</context>
</context-group>
</trans-unit>
<trans-unit id="6437411876967154040" datatype="html" xml:space="preserve">
<source>Audio only</source>
<target state="translated">Yalnızca ses</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/recent-videos/recent-videos.component.ts</context>
<context context-type="linenumber">60</context>
</context-group>
</trans-unit>
<trans-unit id="6268070779441507380" datatype="html" xml:space="preserve">
<source>Download Date</source>
<target state="translated">İndirme Tarihi</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="3533826530554274875" datatype="html" xml:space="preserve">
<source>Upload Date</source>
<target state="translated">Yüklenme Tarihi</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="2492098975665776610" datatype="html" xml:space="preserve">
<source>File Size</source>
<target state="translated">Dosya Boyutu</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="7410432243549869948" datatype="html" xml:space="preserve">
<source>Duration</source>
<target state="translated">Uzunluk</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sort-property/sort-property.component.ts</context>
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
<trans-unit id="9aa1b4779a515170b297d2c0507e6ff9d2e3e0e0" datatype="html" xml:space="preserve">
<source>Blacklist deleted subscription files</source>
<target state="translated">Silinen abonelik dosyalarını kara listele</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
</trans-unit>
<trans-unit id="7b4585a9072f3c1292972c14a3d0e14978fbfc9c" datatype="html" xml:space="preserve">
<source>Delete old files:</source>
<target state="translated">Eski dosyaları sil:</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.html</context>
<context context-type="linenumber">66</context>
</context-group>
</trans-unit>
<trans-unit id="9aa62bf1a535a97a4d752bbc5cf1c31af0f0c1f7" datatype="html" xml:space="preserve">
<source>Supports regex</source>
<target state="translated">Regex destekler</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
</trans-unit>
<trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html" xml:space="preserve">
<source>User</source>
<target state="translated">Kullanıcı</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="c2faa86201eab08b5b39b5437f96ab9432e125e7" datatype="html" xml:space="preserve">
<source>Item limit</source>
<target state="translated">Öğe sınırı</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/generate-rss-url/generate-rss-url.component.html</context>
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit id="1698114086921246480" datatype="html" xml:space="preserve">
<source>Unsubscribe</source>
<target state="translated">Abonelikten çık</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="b6399391e706e2d7b7b7880eb5630e4e6f49728c" datatype="html" xml:space="preserve">
<source>Side</source>
<target state="translated">Yan</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">35,37</context>
</context-group>
</trans-unit>
<trans-unit id="e0a11fbea353b1ce1131161774e4a3e10bcb99b1" datatype="html" xml:space="preserve">
<source>Large</source>
<target state="translated">Büyük</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">44,46</context>
</context-group>
</trans-unit>
<trans-unit id="d57c023a4cf63b2f12c10328c15b636ff18929aa" datatype="html" xml:space="preserve">
<source>Best</source>
<target state="translated">En iyisi</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/main/main.component.html</context>
<context context-type="linenumber">24,25</context>
</context-group>
</trans-unit>
<trans-unit id="af30e51aa8b67e1133a341ec28359be05150e65c" datatype="html" xml:space="preserve">
<source>No description available.</source>
<target state="translated">Açıklama yok.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/player/player.component.html</context>
<context context-type="linenumber">25,27</context>
</context-group>
</trans-unit>
<trans-unit id="fd467148a18f0921c10d116d4e0174fe29452be4" datatype="html" xml:space="preserve">
<source>See documentation here.</source>
<target state="translated">Buradan dokümentasyonu okuyun.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">274</context>
</context-group>
</trans-unit>
<trans-unit id="8bcabdf6b16cad0313a86c7e940c5e3ad7f9f8ab" datatype="html" xml:space="preserve">
<source>Notifications</source>
<target state="translated">Bildirimler</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">343</context>
</context-group>
</trans-unit>
<trans-unit id="c40370dc182b5e4333828b70f7478bde58bb5cfe" datatype="html" xml:space="preserve">
<source>Enable notifications</source>
<target state="translated">Bildirimleri etkinleştir</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">349</context>
</context-group>
</trans-unit>
<trans-unit id="c5dc5fbcce45e9b1530e2a5c2baa8ebe722aef4c" datatype="html" xml:space="preserve">
<source>Download complete</source>
<target state="translated">İndirme tamamlandı</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">358</context>
</context-group>
</trans-unit>
<trans-unit id="3ffd9490f3a4c0b24021d25e1dc71fcfe5d39cd6" datatype="html" xml:space="preserve">
<source>Download error</source>
<target state="translated">İndirme hatası</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">359</context>
</context-group>
</trans-unit>
<trans-unit id="2361a4f76caaa4574803fbcdca8b0a47c91cc7ed" datatype="html" xml:space="preserve">
<source>Task finished</source>
<target state="translated">Görev tamamlandı</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">360</context>
</context-group>
</trans-unit>
<trans-unit id="9e766e11a9de375907aaf566897ecc6dac393ebc" datatype="html" xml:space="preserve">
<source>Webhook URL</source>
<target state="translated">Webhook URLsi</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">366</context>
</context-group>
</trans-unit>
<trans-unit id="3264d82792954815be755b3da01e2625458711dc" datatype="html" xml:space="preserve">
<source>Discord Webhook URL</source>
<target state="translated">Discord Webhook URLsi</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">373</context>
</context-group>
</trans-unit>
<trans-unit id="0cfc9cfe7cd8ea14bc053693b28872da739af02c" datatype="html" xml:space="preserve">
<source>See docs here.</source>
<target state="translated">Buradan dokümentasyonu okuyun.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">375</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">382</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">392</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">402</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">409</context>
</context-group>
</trans-unit>
<trans-unit id="7cedb649779673568447b994463b2882c4e0436a" datatype="html" xml:space="preserve">
<source>Slack Webhook URL</source>
<target state="translated">Slack Webhook URLsi</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">380</context>
</context-group>
</trans-unit>
<trans-unit id="eeb0ba2e4743901d8f5eebd9a3529aa1f236c608" datatype="html" xml:space="preserve">
<source>Create bot here.</source>
<target state="translated">Buradan bot oluşturun.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">419</context>
</context-group>
</trans-unit>
<trans-unit id="378c072ce05889c9771718d05106e7685fcd3507" datatype="html" xml:space="preserve">
<source>Medium</source>
<target state="translated">Orta</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">47,49</context>
</context-group>
</trans-unit>
<trans-unit id="cdf5297d8d080a78e8b10debc5c38b7845a3cbe7" datatype="html" xml:space="preserve">
<source>Do not ask for confirmation</source>
<target state="translated">Doğrulama isteme</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="9176960997786930103" datatype="html" xml:space="preserve">
<source>Error for: <x id="PH" equiv-text="task['title']"/></source>
<target state="translated">Şunun için hata: <x id="PH" equiv-text="task['title']"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/tasks/tasks.component.ts</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="3640026747176198246" datatype="html" xml:space="preserve">
<source>Watch content</source>
<target state="translated">İçerik izle</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/downloads/downloads.component.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit id="5ac5a0e5ffe8e5623b40696f4c2403c17349271f" datatype="html" xml:space="preserve">
<source>Sidepanel mode</source>
<target state="translated">Yan panel modu</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="6785427850041119037" datatype="html" xml:space="preserve">
<source>Delete category</source>
<target state="translated">Kategoriyi sil</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">158</context>
</context-group>
</trans-unit>
<trans-unit id="784837056777689544" datatype="html" xml:space="preserve">
<source>Would you like to unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/>?</source>
<target state="translated"><x id="subscription name" equiv-text="this.sub['name']"/> aboneliğinden çıkmak istiyor musunuz?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="33a7c6d5ff3515fa237f1fd4e43df8b65373954d" datatype="html" xml:space="preserve">
<source>Enable all notifications</source>
<target state="translated">Tüm bildirimleri etkinleştir</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">352</context>
</context-group>
</trans-unit>
<trans-unit id="c41475a25c9f9d9639db9efa56637603a77528b4" datatype="html" xml:space="preserve">
<source>Download archive</source>
<target state="translated">Arşivi indir</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/archive-viewer/archive-viewer.component.html</context>
<context context-type="linenumber">80</context>
</context-group>
</trans-unit>
<trans-unit id="61d6b5fa4311b1c617b66dad72496f9dd43b07b4" datatype="html" xml:space="preserve">
<source>Be careful enabling this with multi-user mode! User data may be exposed.</source>
<target state="translated">Çoklu kullanıcı moduyla beraber bunu etkinleştirirken dikkatli olun! Kullanıcı verileri açığa çıkabilir.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">272</context>
</context-group>
</trans-unit>
<trans-unit id="c65dd978b3c7566551c0ebefb234c2d41942b847" datatype="html" xml:space="preserve">
<source>Delete files older than</source>
<target state="translated">Şundan eski dosyaları sil</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">6</context>
</context-group>
</trans-unit>
<trans-unit id="56b1a3c93fb95fed1805005c561a5e431d57ffae" datatype="html" xml:space="preserve">
<source>Blacklist all files</source>
<target state="translated">Tüm dosyaları kara listele</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/task-settings/task-settings.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
</trans-unit>
<trans-unit id="1d4fa01d25990f60abf21c3a451fa8ba262b7912" datatype="html" xml:space="preserve">
<source>Unfavorite</source>
<target state="translated">Favoriden Çıkar</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/unified-file-card/unified-file-card.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="8336047719608684263" datatype="html" xml:space="preserve">
<source>Unsubscribe from <x id="subscription name" equiv-text="this.sub['name']"/></source>
<target state="translated"><x id="subscription name" equiv-text="this.sub['name']"/> aboneliğinden çık</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/subscription-info-dialog/subscription-info-dialog.component.ts</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="9a865c2922f5c01899d06c472dba2e5bd63bcff9" datatype="html" xml:space="preserve">
<source>Small</source>
<target state="translated">Küçük</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dialogs/user-profile-dialog/user-profile-dialog.component.html</context>
<context context-type="linenumber">50,52</context>
</context-group>
</trans-unit>
<trans-unit id="7332320960988475089" datatype="html" xml:space="preserve">
<source>Successfully deleted <x id="category name" equiv-text="category['name']"/>!</source>
<target state="translated"><x id="category name" equiv-text="category['name']"/> başarıyla silindi!</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.ts</context>
<context context-type="linenumber">168</context>
</context-group>
</trans-unit>
<trans-unit id="9c562d26e041390ecc3f49dabc51cc50ebba7469" datatype="html" xml:space="preserve">
<source>Allowed notification types</source>
<target state="translated">İzin verilen bildirim türleri</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
<context context-type="linenumber">356</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
/* You can add global styles to this file, and also import other style files */
@use '@angular/material' as mat;
@import '~material-icons/iconfont/material-icons.css';
@@ -13,6 +12,7 @@
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
@import '@angular/material/theming';
// Plus imports for other components in your app.
/*// Typography
@@ -24,22 +24,22 @@ $custom-typography: mat-typography-config(
@include angular-material-typography($custom-typography);
*/
// Default colors
$my-app-primary: mat.define-palette(mat.$light-blue-palette, 700, 100, 800); // mat-palette($mat-light-blue, 700, 100, 800);
$my-app-accent: mat.define-palette(mat.$blue-palette, 700, 100, 800);
$my-app-warn: mat.define-palette(mat.$red-palette, 700, 100, 800);
$my-app-primary: mat-palette($mat-light-blue, 700, 100, 800);
$my-app-accent: mat-palette($mat-blue, 700, 100, 800);
$my-app-warn: mat-palette($mat-red, 700, 100, 800);
$my-app-theme: mat.define-light-theme((color: (primary: $my-app-primary, accent: $my-app-accent, warn: $my-app-warn)));
@include mat.all-component-themes($my-app-theme);
$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);
@include angular-material-theme($my-app-theme);
// Dark theme
$dark-primary: mat.define-palette(mat.$light-blue-palette, 700, 100, 800);
$dark-accent: mat.define-palette(mat.$blue-palette);
$dark-warn: mat.define-palette(mat.$deep-orange-palette);
$dark-primary: mat-palette($mat-light-blue, 700, 100, 800);
$dark-accent: mat-palette($mat-blue);
$dark-warn: mat-palette($mat-deep-orange);
$dark-theme: mat.define-dark-theme((color: (primary: $dark-primary, accent: $dark-accent, warn: $dark-warn)));
$dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn);
.dark-theme {
@include mat.all-component-themes($dark-theme);
@include angular-material-theme($dark-theme);
}
.mat-mdc-outlined-button, .mat-mdc-raised-button, .mat-mdc-flat-button {
@@ -47,14 +47,14 @@ $dark-theme: mat.define-dark-theme((color: (primary: $dark-primary, accent: $d
}
// Light theme
$light-primary: mat.define-palette(mat.$grey-palette, 200, 500, 300);
$light-accent: mat.define-palette(mat.$brown-palette, 200);
$light-warn: mat.define-palette(mat.$deep-orange-palette, 200);
$light-primary: mat-palette($mat-grey, 200, 500, 300);
$light-accent: mat-palette($mat-brown, 200);
$light-warn: mat-palette($mat-deep-orange, 200);
$light-theme: mat.define-light-theme((color: (primary: $light-primary, accent: $light-accent, warn: $light-warn)));
$light-theme: mat-light-theme($light-primary, $light-accent, $light-warn);
.light-theme {
@include mat.all-component-themes($light-theme);
@include angular-material-theme($light-theme);
}
.no-outline {
@@ -65,7 +65,9 @@ $light-theme: mat.define-light-theme((color: (primary: $light-primary, accent: $
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat.core();
@include mat-core();
// @import '../node_modules/@angular/material/theming';
.centered {
margin: 0 auto;