mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-12 07:40:57 +03:00
Compare commits
7 Commits
rebuild-da
...
v4.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eadb37532 | ||
|
|
2c61260e0f | ||
|
|
ba0de7f95c | ||
|
|
3dfdbcb151 | ||
|
|
c207e56855 | ||
|
|
441131e930 | ||
|
|
7447ca038a |
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -80,7 +80,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||||
|
|||||||
@@ -1758,14 +1758,14 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
required:
|
required:
|
||||||
- task_key
|
- task_key
|
||||||
UpdateTaskScheduleRequest:
|
UpdateTaskScheduleRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
new_schedule:
|
new_schedule:
|
||||||
$ref: '#/components/schemas/Schedule'
|
$ref: '#/components/schemas/Schedule'
|
||||||
required:
|
required:
|
||||||
@@ -1775,7 +1775,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
new_data:
|
new_data:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -1785,7 +1785,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
task_key:
|
task_key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
new_options:
|
new_options:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -2726,7 +2726,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
key:
|
key:
|
||||||
type: string
|
$ref: '#/components/schemas/TaskType'
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
last_ran:
|
last_ran:
|
||||||
@@ -2745,6 +2745,17 @@ components:
|
|||||||
$ref: '#/components/schemas/Schedule'
|
$ref: '#/components/schemas/Schedule'
|
||||||
options:
|
options:
|
||||||
type: object
|
type: object
|
||||||
|
TaskType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- backup_local_db
|
||||||
|
- missing_files_check
|
||||||
|
- missing_db_records
|
||||||
|
- duplicate_files_check
|
||||||
|
- youtubedl_update_check
|
||||||
|
- delete_old_files
|
||||||
|
- import_legacy_archives
|
||||||
|
- rebuild_database
|
||||||
Schedule:
|
Schedule:
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
|
|||||||
@@ -535,6 +535,7 @@ async function loadConfig() {
|
|||||||
if (allowSubscriptions) {
|
if (allowSubscriptions) {
|
||||||
// set downloading to false
|
// set downloading to false
|
||||||
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
let subscriptions = await subscriptions_api.getAllSubscriptions();
|
||||||
|
subscriptions.forEach(async sub => subscriptions_api.writeSubscriptionMetadata(sub));
|
||||||
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
|
subscriptions_api.updateSubscriptionPropertyMultiple(subscriptions, {downloading: false});
|
||||||
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
// runs initially, then runs every ${subscriptionCheckInterval} seconds
|
||||||
const watchSubscriptionsInterval = function() {
|
const watchSubscriptionsInterval = function() {
|
||||||
@@ -1928,9 +1929,38 @@ app.post('/api/clearAllLogs', optionalJwt, async function(req, res) {
|
|||||||
|
|
||||||
// user authentication
|
// user authentication
|
||||||
|
|
||||||
app.post('/api/auth/register'
|
app.post('/api/auth/register', optionalJwt, async (req, res) => {
|
||||||
, optionalJwt
|
const userid = req.body.userid;
|
||||||
, auth_api.registerUser);
|
const username = req.body.username;
|
||||||
|
const plaintextPassword = req.body.password;
|
||||||
|
|
||||||
|
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
||||||
|
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
||||||
|
res.sendStatus(409);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plaintextPassword === "") {
|
||||||
|
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
||||||
|
res.sendStatus(409);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userid || !username) {
|
||||||
|
logger.error(`Registration failed for user ${userid}. Username or userid is invalid.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_user = await auth_api.registerUser(userid, username, plaintextPassword);
|
||||||
|
|
||||||
|
if (!new_user) {
|
||||||
|
res.sendStatus(409);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
user: new_user
|
||||||
|
});
|
||||||
|
});
|
||||||
app.post('/api/auth/login'
|
app.post('/api/auth/login'
|
||||||
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
, auth_api.passport.authenticate(['local', 'ldapauth'], {})
|
||||||
, auth_api.generateJWT
|
, auth_api.generateJWT
|
||||||
@@ -1982,18 +2012,7 @@ app.post('/api/updateUser', optionalJwt, async (req, res) => {
|
|||||||
app.post('/api/deleteUser', optionalJwt, async (req, res) => {
|
app.post('/api/deleteUser', optionalJwt, async (req, res) => {
|
||||||
let uid = req.body.uid;
|
let uid = req.body.uid;
|
||||||
try {
|
try {
|
||||||
let success = false;
|
const success = await auth_api.deleteUser(uid);
|
||||||
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
|
||||||
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
|
||||||
const user_db_obj = await db_api.getRecord('users', {uid: uid});
|
|
||||||
if (user_db_obj) {
|
|
||||||
// user exists, let's delete
|
|
||||||
await fs.remove(user_folder);
|
|
||||||
await db_api.removeRecord('users', {uid: uid});
|
|
||||||
success = true;
|
|
||||||
} else {
|
|
||||||
logger.error(`Could not find user with uid ${uid}`);
|
|
||||||
}
|
|
||||||
res.send({success: success});
|
res.send({success: success});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
const config_api = require('../config');
|
const config_api = require('../config');
|
||||||
const consts = require('../consts');
|
const CONSTS = require('../consts');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
const db_api = require('../db');
|
const db_api = require('../db');
|
||||||
|
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { uuid } = require('uuidv4');
|
const { uuid } = require('uuidv4');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
var LocalStrategy = require('passport-local').Strategy;
|
var LocalStrategy = require('passport-local').Strategy;
|
||||||
var LdapStrategy = require('passport-ldapauth');
|
var LdapStrategy = require('passport-ldapauth');
|
||||||
@@ -16,7 +18,7 @@ var JwtStrategy = require('passport-jwt').Strategy,
|
|||||||
let SERVER_SECRET = null;
|
let SERVER_SECRET = null;
|
||||||
let JWT_EXPIRATION = null;
|
let JWT_EXPIRATION = null;
|
||||||
let opts = null;
|
let opts = null;
|
||||||
let saltRounds = null;
|
let saltRounds = 10;
|
||||||
|
|
||||||
exports.initialize = function () {
|
exports.initialize = function () {
|
||||||
/*************************
|
/*************************
|
||||||
@@ -31,8 +33,6 @@ exports.initialize = function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saltRounds = 10;
|
|
||||||
|
|
||||||
// Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813
|
// Sometimes this value is not properly typed: https://github.com/Tzahi12345/YoutubeDL-Material/issues/813
|
||||||
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
JWT_EXPIRATION = config_api.getConfigItem('ytdl_jwt_expiration');
|
||||||
if (!(+JWT_EXPIRATION)) {
|
if (!(+JWT_EXPIRATION)) {
|
||||||
@@ -68,7 +68,7 @@ exports.initialize = function () {
|
|||||||
const setupRoles = async () => {
|
const setupRoles = async () => {
|
||||||
const required_roles = {
|
const required_roles = {
|
||||||
admin: {
|
admin: {
|
||||||
permissions: consts.AVAILABLE_PERMISSIONS
|
permissions: CONSTS.AVAILABLE_PERMISSIONS
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
permissions: [
|
permissions: [
|
||||||
@@ -106,55 +106,41 @@ exports.passport.deserializeUser(function(user, done) {
|
|||||||
/***************************************
|
/***************************************
|
||||||
* Register user with hashed password
|
* Register user with hashed password
|
||||||
**************************************/
|
**************************************/
|
||||||
exports.registerUser = async function(req, res) {
|
|
||||||
var userid = req.body.userid;
|
|
||||||
var username = req.body.username;
|
|
||||||
var plaintextPassword = req.body.password;
|
|
||||||
|
|
||||||
if (userid !== 'admin' && !config_api.getConfigItem('ytdl_allow_registration') && !req.isAuthenticated() && (!req.user || !exports.userHasPermission(req.user.uid, 'settings'))) {
|
exports.registerUser = async (userid, username, plaintextPassword) => {
|
||||||
res.sendStatus(409);
|
const hash = await bcrypt.hash(plaintextPassword, saltRounds);
|
||||||
logger.error(`Registration failed for user ${userid}. Registration is disabled.`);
|
const new_user = generateUserObject(userid, username, hash);
|
||||||
return;
|
// check if user exists
|
||||||
|
if (await db_api.getRecord('users', {uid: userid})) {
|
||||||
|
// user id is taken!
|
||||||
|
logger.error('Registration failed: UID is already taken!');
|
||||||
|
return null;
|
||||||
|
} else if (await db_api.getRecord('users', {name: username})) {
|
||||||
|
// user name is taken!
|
||||||
|
logger.error('Registration failed: User name is already taken!');
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
// add to db
|
||||||
|
await db_api.insertRecordIntoTable('users', new_user);
|
||||||
|
logger.verbose(`New user created: ${new_user.name}`);
|
||||||
|
return new_user;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (plaintextPassword === "") {
|
exports.deleteUser = async (uid) => {
|
||||||
res.sendStatus(400);
|
let success = false;
|
||||||
logger.error(`Registration failed for user ${userid}. A password must be provided.`);
|
let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
return;
|
const user_folder = path.join(__dirname, usersFileFolder, uid);
|
||||||
|
const user_db_obj = await db_api.getRecord('users', {uid: uid});
|
||||||
|
if (user_db_obj) {
|
||||||
|
// user exists, let's delete
|
||||||
|
await fs.remove(user_folder);
|
||||||
|
await db_api.removeRecord('users', {uid: uid});
|
||||||
|
success = true;
|
||||||
|
} else {
|
||||||
|
logger.error(`Could not find user with uid ${uid}`);
|
||||||
}
|
}
|
||||||
|
return success;
|
||||||
bcrypt.hash(plaintextPassword, saltRounds)
|
|
||||||
.then(async function(hash) {
|
|
||||||
let new_user = generateUserObject(userid, username, hash);
|
|
||||||
// check if user exists
|
|
||||||
if (await db_api.getRecord('users', {uid: userid})) {
|
|
||||||
// user id is taken!
|
|
||||||
logger.error('Registration failed: UID is already taken!');
|
|
||||||
res.status(409).send('UID is already taken!');
|
|
||||||
} else if (await db_api.getRecord('users', {name: username})) {
|
|
||||||
// user name is taken!
|
|
||||||
logger.error('Registration failed: User name is already taken!');
|
|
||||||
res.status(409).send('User name is already taken!');
|
|
||||||
} else {
|
|
||||||
// add to db
|
|
||||||
await db_api.insertRecordIntoTable('users', new_user);
|
|
||||||
logger.verbose(`New user created: ${new_user.name}`);
|
|
||||||
res.send({
|
|
||||||
user: new_user
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function(result) {
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
logger.error(err);
|
|
||||||
if( err.code == 'ER_DUP_ENTRY' ) {
|
|
||||||
res.status(409).send('UserId already taken');
|
|
||||||
} else {
|
|
||||||
res.sendStatus(409);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************
|
/***************************************
|
||||||
@@ -235,7 +221,7 @@ exports.returnAuthResponse = async function(req, res) {
|
|||||||
user: req.user,
|
user: req.user,
|
||||||
token: req.token,
|
token: req.token,
|
||||||
permissions: await exports.userPermissions(req.user.uid),
|
permissions: await exports.userPermissions(req.user.uid),
|
||||||
available_permissions: consts['AVAILABLE_PERMISSIONS']
|
available_permissions: CONSTS.AVAILABLE_PERMISSIONS
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +305,7 @@ exports.getUserVideos = async function(user_uid, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
|
exports.getUserVideo = async function(user_uid, file_uid, requireSharing = false) {
|
||||||
let file = await db_api.getRecord('files', {file_uid: file_uid});
|
let file = await db_api.getRecord('files', {uid: file_uid});
|
||||||
|
|
||||||
// prevent unauthorized users from accessing the file info
|
// prevent unauthorized users from accessing the file info
|
||||||
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
if (file && !file['sharingEnabled'] && requireSharing) file = null;
|
||||||
@@ -406,8 +392,8 @@ exports.userPermissions = async function(user_uid) {
|
|||||||
const role_obj = await db_api.getRecord('roles', {key: role});
|
const role_obj = await db_api.getRecord('roles', {key: role});
|
||||||
const role_permissions = role_obj['permissions'];
|
const role_permissions = role_obj['permissions'];
|
||||||
|
|
||||||
for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) {
|
for (let i = 0; i < CONSTS.AVAILABLE_PERMISSIONS.length; i++) {
|
||||||
let permission = consts['AVAILABLE_PERMISSIONS'][i];
|
let permission = CONSTS.AVAILABLE_PERMISSIONS[i];
|
||||||
|
|
||||||
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
const user_has_explicit_permission = user_obj['permissions'].includes(permission);
|
||||||
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
const permission_in_overrides = user_obj['permission_overrides'].includes(permission);
|
||||||
|
|||||||
@@ -347,6 +347,8 @@ const YTDL_ARGS_WITH_VALUES = [
|
|||||||
'--convert-subs'
|
'--convert-subs'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
exports.SUBSCRIPTION_BACKUP_PATH = 'subscription_backup.json'
|
||||||
|
|
||||||
// we're using a Set here for performance
|
// we're using a Set here for performance
|
||||||
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
exports.YTDL_ARGS_WITH_VALUES = new Set(YTDL_ARGS_WITH_VALUES);
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
CMD="npm start && pm2 start"
|
|
||||||
|
|
||||||
# if the first arg starts with "-" pass it to program
|
|
||||||
if [ "${1#-}" != "$1" ]; then
|
|
||||||
set -- "$CMD" "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# chown current working directory to current user
|
# chown current working directory to current user
|
||||||
if [ "$*" = "$CMD" ] && [ "$(id -u)" = "0" ]; then
|
echo "[entrypoint] setup permission, this may take a while"
|
||||||
find . \! -user "$UID" -exec chown "$UID:$GID" -R '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
find . \! -user "$UID" -exec chown "$UID:$GID" '{}' + || echo "WARNING! Could not change directory ownership. If you manage permissions externally this is fine, otherwise you may experience issues when downloading or deleting videos."
|
||||||
exec gosu "$UID:$GID" "$0" "$@"
|
exec gosu "$UID:$GID" "$@"
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$@"
|
|
||||||
|
|||||||
@@ -6,20 +6,21 @@ const config_api = require('./config');
|
|||||||
const archive_api = require('./archive');
|
const archive_api = require('./archive');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const CONSTS = require('./consts');
|
||||||
|
|
||||||
const debugMode = process.env.YTDL_MODE === 'debug';
|
const debugMode = process.env.YTDL_MODE === 'debug';
|
||||||
|
|
||||||
const db_api = require('./db');
|
const db_api = require('./db');
|
||||||
const downloader_api = require('./downloader');
|
const downloader_api = require('./downloader');
|
||||||
|
|
||||||
async function subscribe(sub, user_uid = null) {
|
exports.subscribe = async (sub, user_uid = null, skip_get_info = false) => {
|
||||||
const result_obj = {
|
const result_obj = {
|
||||||
success: false,
|
success: false,
|
||||||
error: ''
|
error: ''
|
||||||
};
|
};
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
// sub should just have url and name. here we will get isPlaylist and path
|
// sub should just have url and name. here we will get isPlaylist and path
|
||||||
sub.isPlaylist = sub.url.includes('playlist');
|
sub.isPlaylist = sub.isPlaylist || sub.url.includes('playlist');
|
||||||
sub.videos = [];
|
sub.videos = [];
|
||||||
|
|
||||||
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
|
let url_exists = !!(await db_api.getRecord('subscriptions', {url: sub.url, user_uid: user_uid}));
|
||||||
@@ -34,10 +35,11 @@ async function subscribe(sub, user_uid = null) {
|
|||||||
sub['user_uid'] = user_uid ? user_uid : undefined;
|
sub['user_uid'] = user_uid ? user_uid : undefined;
|
||||||
await db_api.insertRecordIntoTable('subscriptions', sub);
|
await db_api.insertRecordIntoTable('subscriptions', sub);
|
||||||
|
|
||||||
let success = await getSubscriptionInfo(sub);
|
let success = skip_get_info ? true : await getSubscriptionInfo(sub);
|
||||||
|
exports.writeSubscriptionMetadata(sub);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
getVideosForSub(sub, user_uid);
|
if (!sub.paused) exports.getVideosForSub(sub, user_uid);
|
||||||
} else {
|
} else {
|
||||||
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
logger.error('Subscribe: Failed to get subscription info. Subscribe failed.')
|
||||||
}
|
}
|
||||||
@@ -109,7 +111,7 @@ async function getSubscriptionInfo(sub) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribe(sub, deleteMode, user_uid = null) {
|
exports.unsubscribe = async (sub, deleteMode, user_uid = null) => {
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
if (user_uid)
|
if (user_uid)
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
@@ -148,7 +150,7 @@ async function unsubscribe(sub, deleteMode, user_uid = null) {
|
|||||||
await db_api.removeAllRecords('archives', {sub_id: sub.id});
|
await db_api.removeAllRecords('archives', {sub_id: sub.id});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null, user_uid = null) {
|
exports.deleteSubscriptionFile = async (sub, file, deleteForever, file_uid = null, user_uid = null) => {
|
||||||
if (typeof sub === 'string') {
|
if (typeof sub === 'string') {
|
||||||
// TODO: fix bad workaround where sub is a sub_id
|
// TODO: fix bad workaround where sub is a sub_id
|
||||||
sub = await db_api.getRecord('subscriptions', {sub_id: sub});
|
sub = await db_api.getRecord('subscriptions', {sub_id: sub});
|
||||||
@@ -216,8 +218,8 @@ async function deleteSubscriptionFile(sub, file, deleteForever, file_uid = null,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideosForSub(sub, user_uid = null) {
|
exports.getVideosForSub = async (sub, user_uid = null) => {
|
||||||
const latest_sub_obj = await getSubscription(sub.id);
|
const latest_sub_obj = await exports.getSubscription(sub.id);
|
||||||
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
if (!latest_sub_obj || latest_sub_obj['downloading']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -305,7 +307,7 @@ async function handleOutputJSON(output, sub, user_uid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
const files_to_download = await getFilesToDownload(sub, output_jsons);
|
||||||
const base_download_options = generateOptionsForSubscriptionDownload(sub, user_uid);
|
const base_download_options = exports.generateOptionsForSubscriptionDownload(sub, user_uid);
|
||||||
|
|
||||||
for (let j = 0; j < files_to_download.length; j++) {
|
for (let j = 0; j < files_to_download.length; j++) {
|
||||||
const file_to_download = files_to_download[j];
|
const file_to_download = files_to_download[j];
|
||||||
@@ -316,7 +318,7 @@ async function handleOutputJSON(output, sub, user_uid) {
|
|||||||
return files_to_download;
|
return files_to_download;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateOptionsForSubscriptionDownload(sub, user_uid) {
|
exports.generateOptionsForSubscriptionDownload = (sub, user_uid) => {
|
||||||
let basePath = null;
|
let basePath = null;
|
||||||
if (user_uid)
|
if (user_uid)
|
||||||
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
basePath = path.join(config_api.getConfigItem('ytdl_users_base_path'), user_uid, 'subscriptions');
|
||||||
@@ -371,10 +373,13 @@ async function generateArgsForSubscription(sub, user_uid, redownload = false, de
|
|||||||
|
|
||||||
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
// skip videos that are in the archive. otherwise sub download can be permanently slow (vs. just the first time)
|
||||||
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
const archive_text = await archive_api.generateArchive(sub.type, sub.user_uid, sub.id);
|
||||||
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_text.split('\n').length - 1} entries.`)
|
const archive_count = archive_text.split('\n').length - 1;
|
||||||
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
if (archive_count > 0) {
|
||||||
await fs.writeFile(archive_path, archive_text);
|
logger.verbose(`Generating temporary archive file for subscription ${sub.name} with ${archive_count} entries.`)
|
||||||
downloadConfig.push('--download-archive', archive_path);
|
const archive_path = path.join(appendedBasePath, 'archive.txt');
|
||||||
|
await fs.writeFile(archive_path, archive_text);
|
||||||
|
downloadConfig.push('--download-archive', archive_path);
|
||||||
|
}
|
||||||
|
|
||||||
if (sub.custom_args) {
|
if (sub.custom_args) {
|
||||||
const customArgsArray = sub.custom_args.split(',,');
|
const customArgsArray = sub.custom_args.split(',,');
|
||||||
@@ -440,35 +445,36 @@ async function getFilesToDownload(sub, output_jsons) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getSubscriptions(user_uid = null) {
|
exports.getSubscriptions = async (user_uid = null) => {
|
||||||
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
return await db_api.getRecords('subscriptions', {user_uid: user_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllSubscriptions() {
|
exports.getAllSubscriptions = async () => {
|
||||||
const all_subs = await db_api.getRecords('subscriptions');
|
const all_subs = await db_api.getRecords('subscriptions');
|
||||||
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
const multiUserMode = config_api.getConfigItem('ytdl_multi_user_mode');
|
||||||
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
|
return all_subs.filter(sub => !!(sub.user_uid) === !!multiUserMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSubscription(subID) {
|
exports.getSubscription = async (subID) => {
|
||||||
// stringify and parse because we may override the 'downloading' property
|
// stringify and parse because we may override the 'downloading' property
|
||||||
const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID})));
|
const sub = JSON.parse(JSON.stringify(await db_api.getRecord('subscriptions', {id: subID})));
|
||||||
// now with the download_queue, we may need to override 'downloading'
|
// now with the download_queue, we may need to override 'downloading'
|
||||||
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: sub.id}, true);
|
const current_downloads = await db_api.getRecords('download_queue', {running: true, sub_id: subID}, true);
|
||||||
if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
|
if (!sub['downloading']) sub['downloading'] = current_downloads > 0;
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSubscriptionByName(subName, user_uid = null) {
|
exports.getSubscriptionByName = async (subName, user_uid = null) => {
|
||||||
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
|
return await db_api.getRecord('subscriptions', {name: subName, user_uid: user_uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSubscription(sub) {
|
exports.updateSubscription = async (sub) => {
|
||||||
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
|
await db_api.updateRecord('subscriptions', {id: sub.id}, sub);
|
||||||
|
exports.writeSubscriptionMetadata(sub);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSubscriptionPropertyMultiple(subs, assignment_obj) {
|
exports.updateSubscriptionPropertyMultiple = async (subs, assignment_obj) => {
|
||||||
subs.forEach(async sub => {
|
subs.forEach(async sub => {
|
||||||
await updateSubscriptionProperty(sub, assignment_obj);
|
await updateSubscriptionProperty(sub, assignment_obj);
|
||||||
});
|
});
|
||||||
@@ -480,6 +486,14 @@ async function updateSubscriptionProperty(sub, assignment_obj) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.writeSubscriptionMetadata = (sub) => {
|
||||||
|
let basePath = sub.user_uid ? path.join(config_api.getConfigItem('ytdl_users_base_path'), sub.user_uid, 'subscriptions')
|
||||||
|
: config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
const appendedBasePath = getAppendedBasePath(sub, basePath);
|
||||||
|
const metadata_path = path.join(appendedBasePath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
|
||||||
|
fs.writeJSONSync(metadata_path, sub);
|
||||||
|
}
|
||||||
|
|
||||||
async function setFreshUploads(sub) {
|
async function setFreshUploads(sub) {
|
||||||
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
const sub_files = await db_api.getRecords('files', {sub_id: sub.id});
|
||||||
if (!sub_files) return;
|
if (!sub_files) return;
|
||||||
@@ -534,17 +548,3 @@ async function checkVideoIfBetterExists(file_obj, sub, user_uid) {
|
|||||||
function getAppendedBasePath(sub, base_path) {
|
function getAppendedBasePath(sub, base_path) {
|
||||||
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
return path.join(base_path, (sub.isPlaylist ? 'playlists/' : 'channels/'), sub.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getSubscription : getSubscription,
|
|
||||||
getSubscriptionByName : getSubscriptionByName,
|
|
||||||
getSubscriptions : getSubscriptions,
|
|
||||||
getAllSubscriptions : getAllSubscriptions,
|
|
||||||
updateSubscription : updateSubscription,
|
|
||||||
subscribe : subscribe,
|
|
||||||
unsubscribe : unsubscribe,
|
|
||||||
deleteSubscriptionFile : deleteSubscriptionFile,
|
|
||||||
getVideosForSub : getVideosForSub,
|
|
||||||
updateSubscriptionPropertyMultiple : updateSubscriptionPropertyMultiple,
|
|
||||||
generateOptionsForSubscriptionDownload: generateOptionsForSubscriptionDownload
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,10 +3,17 @@ const notifications_api = require('./notifications');
|
|||||||
const youtubedl_api = require('./youtube-dl');
|
const youtubedl_api = require('./youtube-dl');
|
||||||
const archive_api = require('./archive');
|
const archive_api = require('./archive');
|
||||||
const files_api = require('./files');
|
const files_api = require('./files');
|
||||||
|
const subscriptions_api = require('./subscriptions');
|
||||||
|
const config_api = require('./config');
|
||||||
|
const auth_api = require('./authentication/auth');
|
||||||
|
const utils = require('./utils');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const CONSTS = require('./consts');
|
||||||
|
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const logger = require('./logger');
|
const path = require('path');
|
||||||
const scheduler = require('node-schedule');
|
const scheduler = require('node-schedule');
|
||||||
|
const { uuid } = require('uuidv4');
|
||||||
|
|
||||||
const TASKS = {
|
const TASKS = {
|
||||||
backup_local_db: {
|
backup_local_db: {
|
||||||
@@ -47,6 +54,11 @@ const TASKS = {
|
|||||||
run: archive_api.importArchives,
|
run: archive_api.importArchives,
|
||||||
title: 'Import legacy archives',
|
title: 'Import legacy archives',
|
||||||
job: null
|
job: null
|
||||||
|
},
|
||||||
|
rebuild_database: {
|
||||||
|
run: rebuildDB,
|
||||||
|
title: 'Rebuild database',
|
||||||
|
job: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,4 +277,68 @@ async function autoDeleteFiles(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function rebuildDB() {
|
||||||
|
await db_api.backupDB();
|
||||||
|
let subs_to_add = await guessSubscriptions(false);
|
||||||
|
subs_to_add = subs_to_add.concat(await guessSubscriptions(true));
|
||||||
|
const users_to_add = await guessUsers();
|
||||||
|
for (const user_to_add of users_to_add) {
|
||||||
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
|
||||||
|
const user_exists = await db_api.getRecord('users', {uid: user_to_add});
|
||||||
|
if (!user_exists) {
|
||||||
|
await auth_api.registerUser(user_to_add, user_to_add, 'password');
|
||||||
|
logger.info(`Regenerated user ${user_to_add}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user_channel_subs = await guessSubscriptions(false, path.join(usersFileFolder, user_to_add), user_to_add);
|
||||||
|
const user_playlist_subs = await guessSubscriptions(true, path.join(usersFileFolder, user_to_add), user_to_add);
|
||||||
|
subs_to_add = subs_to_add.concat(user_channel_subs, user_playlist_subs);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sub_to_add of subs_to_add) {
|
||||||
|
const sub_exists = !!(await subscriptions_api.getSubscriptionByName(sub_to_add['name'], sub_to_add['user_uid']));
|
||||||
|
// TODO: we shouldn't be creating this here
|
||||||
|
const new_sub = Object.assign({}, sub_to_add, {paused: true});
|
||||||
|
if (!sub_exists) {
|
||||||
|
await subscriptions_api.subscribe(new_sub, sub_to_add['user_uid'], true);
|
||||||
|
logger.info(`Regenerated subscription ${sub_to_add['name']}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Importing unregistered files`);
|
||||||
|
await files_api.importUnregisteredFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
const guessUsers = async () => {
|
||||||
|
const usersFileFolder = config_api.getConfigItem('ytdl_users_base_path');
|
||||||
|
const userPaths = await utils.getDirectoriesInDirectory(usersFileFolder);
|
||||||
|
return userPaths.map(userPath => path.basename(userPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guessSubscriptions = async (isPlaylist, basePath = null) => {
|
||||||
|
const guessed_subs = [];
|
||||||
|
const subscriptionsFileFolder = config_api.getConfigItem('ytdl_subscriptions_base_path');
|
||||||
|
|
||||||
|
const subsSubPath = basePath ? path.join(basePath, 'subscriptions') : subscriptionsFileFolder;
|
||||||
|
const subsPath = path.join(subsSubPath, isPlaylist ? 'playlists' : 'channels');
|
||||||
|
|
||||||
|
const subs = await utils.getDirectoriesInDirectory(subsPath);
|
||||||
|
for (const subPath of subs) {
|
||||||
|
const sub_backup_path = path.join(subPath, CONSTS.SUBSCRIPTION_BACKUP_PATH);
|
||||||
|
if (!fs.existsSync(sub_backup_path)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sub_backup = fs.readJSONSync(sub_backup_path)
|
||||||
|
delete sub_backup['_id'];
|
||||||
|
guessed_subs.push(sub_backup);
|
||||||
|
} catch(err) {
|
||||||
|
logger.warn(`Failed to reimport subscription in path ${subPath}`)
|
||||||
|
logger.warn(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return guessed_subs;
|
||||||
|
}
|
||||||
|
|
||||||
exports.TASKS = TASKS;
|
exports.TASKS = TASKS;
|
||||||
@@ -337,16 +337,22 @@ describe('Database', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Multi User', async function() {
|
describe('Multi User', async function() {
|
||||||
let user = null;
|
const user_to_test = 'test_user';
|
||||||
const user_to_test = 'admin';
|
const user_password = 'test_pass';
|
||||||
const sub_to_test = 'dc834388-3454-41bf-a618-e11cb8c7de1c';
|
const sub_to_test = '';
|
||||||
const playlist_to_test = 'ysabVZz4x';
|
const playlist_to_test = '';
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
await db_api.connectToDB();
|
await db_api.connectToDB();
|
||||||
user = await auth_api.login('admin', 'pass');
|
await auth_api.deleteUser(user_to_test);
|
||||||
});
|
});
|
||||||
describe('Authentication', function() {
|
describe('Basic', function() {
|
||||||
it('login', async function() {
|
it('Register', async function() {
|
||||||
|
const user = await auth_api.registerUser(user_to_test, user_to_test, user_password);
|
||||||
|
assert(user);
|
||||||
|
});
|
||||||
|
it('Login', async function() {
|
||||||
|
await auth_api.registerUser(user_to_test, user_to_test, user_password);
|
||||||
|
const user = await auth_api.login(user_to_test, user_password);
|
||||||
assert(user);
|
assert(user);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -362,14 +368,14 @@ describe('Multi User', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Video access - disallowed', async function() {
|
it('Video access - disallowed', async function() {
|
||||||
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false}, user_to_test);
|
await db_api.setVideoProperty(video_to_test, {sharingEnabled: false});
|
||||||
const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
const video_obj = auth_api.getUserVideo(user_to_test, video_to_test, true);
|
||||||
assert(!video_obj);
|
assert(!video_obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Video access - allowed', async function() {
|
it('Video access - allowed', async function() {
|
||||||
await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test);
|
await db_api.setVideoProperty(video_to_test, {sharingEnabled: true}, user_to_test);
|
||||||
const video_obj = auth_api.getUserVideo('admin', video_to_test, true);
|
const video_obj = auth_api.getUserVideo(user_to_test, video_to_test, true);
|
||||||
assert(video_obj);
|
assert(video_obj);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -519,6 +519,17 @@ exports.convertFlatObjectToNestedObject = (obj) => {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getDirectoriesInDirectory = async (basePath) => {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(basePath, { withFileTypes: true });
|
||||||
|
return files
|
||||||
|
.filter((file) => file.isDirectory())
|
||||||
|
.map((file) => path.join(basePath, file.name));
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// objects
|
// objects
|
||||||
|
|
||||||
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date, description, view_count, height, abr) {
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ version: 0.1.0
|
|||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "4.3.1"
|
appVersion: "4.3.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-dl-material",
|
"name": "youtube-dl-material",
|
||||||
"version": "4.3.1",
|
"version": "4.3.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export type { SubscriptionRequestData } from './models/SubscriptionRequestData';
|
|||||||
export type { SuccessObject } from './models/SuccessObject';
|
export type { SuccessObject } from './models/SuccessObject';
|
||||||
export type { TableInfo } from './models/TableInfo';
|
export type { TableInfo } from './models/TableInfo';
|
||||||
export type { Task } from './models/Task';
|
export type { Task } from './models/Task';
|
||||||
|
export { TaskType } from './models/TaskType';
|
||||||
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
export type { TestConnectionStringRequest } from './models/TestConnectionStringRequest';
|
||||||
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
export type { TestConnectionStringResponse } from './models/TestConnectionStringResponse';
|
||||||
export type { TransferDBRequest } from './models/TransferDBRequest';
|
export type { TransferDBRequest } from './models/TransferDBRequest';
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
YoutubeDLMaterial: Record<string, any>;
|
YoutubeDLMaterial: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ export type Download = {
|
|||||||
user_uid?: string;
|
user_uid?: string;
|
||||||
sub_id?: string;
|
sub_id?: string;
|
||||||
sub_name?: string;
|
sub_name?: string;
|
||||||
prefetched_info?: Record<string, any>;
|
prefetched_info?: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
export type GetFileFormatsResponse = {
|
export type GetFileFormatsResponse = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
result: {
|
result: {
|
||||||
formats?: Array<Record<string, any>>;
|
formats?: Array<any>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ import type { Subscription } from './Subscription';
|
|||||||
|
|
||||||
export type GetSubscriptionResponse = {
|
export type GetSubscriptionResponse = {
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
files: Array<Record<string, any>>;
|
files: Array<any>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import type { TaskType } from './TaskType';
|
||||||
|
|
||||||
export type GetTaskRequest = {
|
export type GetTaskRequest = {
|
||||||
task_key: string;
|
task_key: TaskType;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export type Notification = {
|
|||||||
user_uid?: string;
|
user_uid?: string;
|
||||||
action?: Array<NotificationAction>;
|
action?: Array<NotificationAction>;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
data?: Record<string, any>;
|
data?: any;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ export type Subscription = {
|
|||||||
timerange?: string;
|
timerange?: string;
|
||||||
custom_args?: string;
|
custom_args?: string;
|
||||||
custom_output?: string;
|
custom_output?: string;
|
||||||
videos: Array<Record<string, any>>;
|
videos: Array<any>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import type { Schedule } from './Schedule';
|
import type { Schedule } from './Schedule';
|
||||||
|
import type { TaskType } from './TaskType';
|
||||||
|
|
||||||
export type Task = {
|
export type Task = {
|
||||||
key: string;
|
key: TaskType;
|
||||||
title?: string;
|
title?: string;
|
||||||
last_ran: number;
|
last_ran: number;
|
||||||
last_confirmed: number;
|
last_confirmed: number;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
confirming: boolean;
|
confirming: boolean;
|
||||||
data: Record<string, any>;
|
data: any;
|
||||||
error: string;
|
error: string;
|
||||||
schedule: Schedule;
|
schedule: Schedule;
|
||||||
options?: Record<string, any>;
|
options?: any;
|
||||||
};
|
};
|
||||||
|
|||||||
14
src/api-types/models/TaskType.ts
Normal file
14
src/api-types/models/TaskType.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export enum TaskType {
|
||||||
|
BACKUP_LOCAL_DB = 'backup_local_db',
|
||||||
|
MISSING_FILES_CHECK = 'missing_files_check',
|
||||||
|
MISSING_DB_RECORDS = 'missing_db_records',
|
||||||
|
DUPLICATE_FILES_CHECK = 'duplicate_files_check',
|
||||||
|
YOUTUBEDL_UPDATE_CHECK = 'youtubedl_update_check',
|
||||||
|
DELETE_OLD_FILES = 'delete_old_files',
|
||||||
|
IMPORT_LEGACY_ARCHIVES = 'import_legacy_archives',
|
||||||
|
REBUILD_DATABASE = 'rebuild_database',
|
||||||
|
}
|
||||||
@@ -10,5 +10,5 @@ export type UpdateFileRequest = {
|
|||||||
/**
|
/**
|
||||||
* Object with fields to update as keys and their new values
|
* Object with fields to update as keys and their new values
|
||||||
*/
|
*/
|
||||||
change_obj: Record<string, any>;
|
change_obj: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import type { TaskType } from './TaskType';
|
||||||
|
|
||||||
export type UpdateTaskDataRequest = {
|
export type UpdateTaskDataRequest = {
|
||||||
task_key: string;
|
task_key: TaskType;
|
||||||
new_data: Record<string, any>;
|
new_data: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import type { TaskType } from './TaskType';
|
||||||
|
|
||||||
export type UpdateTaskOptionsRequest = {
|
export type UpdateTaskOptionsRequest = {
|
||||||
task_key: string;
|
task_key: TaskType;
|
||||||
new_options: Record<string, any>;
|
new_options: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import type { Schedule } from './Schedule';
|
import type { Schedule } from './Schedule';
|
||||||
|
import type { TaskType } from './TaskType';
|
||||||
|
|
||||||
export type UpdateTaskScheduleRequest = {
|
export type UpdateTaskScheduleRequest = {
|
||||||
task_key: string;
|
task_key: TaskType;
|
||||||
new_schedule: Schedule;
|
new_schedule: Schedule;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
</mat-menu>
|
</mat-menu>
|
||||||
<button [matMenuTriggerFor]="menuSettings" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
<button [matMenuTriggerFor]="menuSettings" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||||
<mat-menu #menuSettings="matMenu">
|
<mat-menu #menuSettings="matMenu">
|
||||||
<button class="top-menu-button" (click)="openProfileDialog()" *ngIf="postsService.isLoggedIn" mat-menu-item>
|
<button class="top-menu-button" (click)="openProfileDialog()" mat-menu-item>
|
||||||
<mat-icon>person</mat-icon>
|
<mat-icon>person</mat-icon>
|
||||||
<span i18n="Profile menu label">Profile</span>
|
<span i18n="Profile menu label">Profile</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
|
<button *ngIf="!postsService.config?.Advanced.multi_user_mode || postsService.isLoggedIn" class="top-menu-button" (click)="openArchivesDialog()" mat-menu-item>
|
||||||
<mat-icon>topic</mat-icon>
|
<mat-icon>topic</mat-icon>
|
||||||
<span i18n="Archives menu label">Archives</span>
|
<span i18n="Archives menu label">Archives</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { MatBadgeModule } from '@angular/material/badge';
|
|||||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||||
|
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
@@ -189,6 +190,7 @@ registerLocaleData(es, 'es');
|
|||||||
DragDropModule,
|
DragDropModule,
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
TextFieldModule,
|
TextFieldModule,
|
||||||
|
ScrollingModule,
|
||||||
NgxFileDropModule,
|
NgxFileDropModule,
|
||||||
AvatarModule,
|
AvatarModule,
|
||||||
ContentLoaderModule,
|
ContentLoaderModule,
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
<!-- Title Column -->
|
<!-- Title Column -->
|
||||||
<ng-container matColumnDef="title">
|
<ng-container matColumnDef="title">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Title">Title</ng-container> </mat-header-cell>
|
<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">
|
<mat-cell *matCellDef="let element" style="flex: 2">
|
||||||
<span class="one-line" [matTooltip]="element.title ? element.title : null">
|
<span class="one-line" [matTooltip]="element.title ? element.title : null">
|
||||||
{{element.title}}
|
{{element.title}}
|
||||||
</span>
|
</span>
|
||||||
@@ -31,41 +31,47 @@
|
|||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Stage Column -->
|
|
||||||
<ng-container matColumnDef="step_index">
|
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Stage">Stage</ng-container> </mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let element"> {{STEP_INDEX_TO_LABEL[element.step_index]}} </mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Progress Column -->
|
<!-- Progress Column -->
|
||||||
<ng-container matColumnDef="percent_complete">
|
<ng-container matColumnDef="percent_complete">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Progress">Progress</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header> <ng-container i18n="Progress">Progress</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<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">
|
<ng-container *ngIf="element.percent_complete">
|
||||||
{{+(element.percent_complete) > 100 ? '100' : element.percent_complete}}%
|
{{+(element.percent_complete) > 100 ? '100' : element.percent_complete}}%
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!element.percent_complete">
|
<ng-container *ngIf="!element.percent_complete">
|
||||||
N/A
|
N/A
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="element.error" i18n="Error">Error</ng-container>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Actions Column -->
|
<!-- Actions Column -->
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef [ngStyle]="{flex: actionsFlex}"> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element" [ngStyle]="{flex: actionsFlex}">
|
||||||
<div>
|
<div *ngIf="!minimizeButtons">
|
||||||
<ng-container *ngIf="!element.finished">
|
<ng-container *ngFor="let downloadAction of downloadActions">
|
||||||
<button (click)="pauseDownload(element.uid)" *ngIf="!element.paused || !element.finished_step" [disabled]="element.paused && !element.finished_step" mat-icon-button matTooltip="Pause" i18n-matTooltip="Pause"><mat-spinner [diameter]="28" *ngIf="element.paused && !element.finished_step" class="icon-button-spinner"></mat-spinner><mat-icon>pause</mat-icon></button>
|
<span class="button-span">
|
||||||
<button (click)="resumeDownload(element.uid)" *ngIf="element.paused && element.finished_step" mat-icon-button matTooltip="Resume" i18n-matTooltip="Resume"><mat-icon>play_arrow</mat-icon></button>
|
<mat-spinner [diameter]="28" *ngIf="downloadAction.loading && downloadAction.loading(element)" class="icon-button-spinner"></mat-spinner>
|
||||||
<button *ngIf="false && !element.paused" (click)="cancelDownload(element.uid)" mat-icon-button matTooltip="Cancel" i18n-matTooltip="Cancel"><mat-icon>cancel</mat-icon></button>
|
<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>
|
</ng-container>
|
||||||
<ng-container *ngIf="element.finished">
|
</div>
|
||||||
<button *ngIf="!element.error" (click)="watchContent(element)" mat-icon-button matTooltip="Watch content" i18n-matTooltip="Watch content"><mat-icon>smart_display</mat-icon></button>
|
<div *ngIf="minimizeButtons">
|
||||||
<button *ngIf="element.error" (click)="showError(element)" mat-icon-button matTooltip="Show error" i18n-matTooltip="Show error"><mat-icon>warning</mat-icon></button>
|
<button [matMenuTriggerFor]="download_actions" mat-icon-button><mat-icon>more_vert</mat-icon></button>
|
||||||
<button (click)="restartDownload(element.uid)" mat-icon-button matTooltip="Restart" i18n-matTooltip="Restart"><mat-icon>restart_alt</mat-icon></button>
|
<mat-menu #download_actions="matMenu">
|
||||||
</ng-container>
|
<ng-container *ngFor="let downloadAction of downloadActions">
|
||||||
<button *ngIf="element.finished || element.paused" (click)="clearDownload(element.uid)" mat-icon-button matTooltip="Clear" i18n-matTooltip="Clear"><mat-icon>delete</mat-icon></button>
|
<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>
|
</div>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -80,9 +86,9 @@
|
|||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!uids" class="downloads-action-button-div">
|
<div *ngIf="!uids" class="downloads-action-button-div">
|
||||||
<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]="!running_download_exists" mat-stroked-button (click)="pauseAllDownloads()"><ng-container i18n="Pause all downloads">Pause all downloads</ng-container></button>
|
||||||
<button style="margin-left: 10px;" [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" [disabled]="!paused_download_exists" mat-stroked-button (click)="resumeAllDownloads()"><ng-container i18n="Resume all downloads">Resume all downloads</ng-container></button>
|
||||||
<button color="warn" style="margin-left: 10px;" mat-stroked-button (click)="clearDownloadsByType()"><ng-container i18n="Clear downloads">Clear 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,23 @@ mat-header-cell, mat-cell {
|
|||||||
|
|
||||||
.icon-button-spinner {
|
.icon-button-spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 7px;
|
top: -13px;
|
||||||
left: 6px;
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-span {
|
||||||
|
position: relative;;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloads-action-button-div {
|
.downloads-action-button-div {
|
||||||
margin-top: 10px;
|
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloads-action-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded-top {
|
.rounded-top {
|
||||||
border-radius: 16px 16px 0px 0px !important;
|
border-radius: 16px 16px 0px 0px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, OnDestroy, ViewChild, Input, EventEmitter } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChild, Input, EventEmitter, HostListener } from '@angular/core';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { trigger, transition, animateChild, stagger, query, style, animate } from '@angular/animations';
|
import { trigger, transition, animateChild, stagger, query, style, animate } from '@angular/animations';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -13,31 +13,7 @@ import { Download } from 'api-types';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-downloads',
|
selector: 'app-downloads',
|
||||||
templateUrl: './downloads.component.html',
|
templateUrl: './downloads.component.html',
|
||||||
styleUrls: ['./downloads.component.scss'],
|
styleUrls: ['./downloads.component.scss']
|
||||||
animations: [
|
|
||||||
// nice stagger effect when showing existing elements
|
|
||||||
trigger('list', [
|
|
||||||
transition(':enter', [
|
|
||||||
// child animation selector + stagger
|
|
||||||
query('@items',
|
|
||||||
stagger(100, animateChild()), { optional: true }
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
trigger('items', [
|
|
||||||
// cubic-bezier for a tiny bouncing feel
|
|
||||||
transition(':enter', [
|
|
||||||
style({ transform: 'scale(0.5)', opacity: 0 }),
|
|
||||||
animate('500ms cubic-bezier(.8,-0.6,0.2,1.5)',
|
|
||||||
style({ transform: 'scale(1)', opacity: 1 }))
|
|
||||||
]),
|
|
||||||
transition(':leave', [
|
|
||||||
style({ transform: 'scale(1)', opacity: 1, height: '*' }),
|
|
||||||
animate('1s cubic-bezier(.8,-0.6,0.2,1.5)',
|
|
||||||
style({ transform: 'scale(0.5)', opacity: 0, height: '0px', margin: '0px' }))
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class DownloadsComponent implements OnInit, OnDestroy {
|
export class DownloadsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@@ -62,13 +38,79 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
3: $localize`Complete`
|
3: $localize`Complete`
|
||||||
}
|
}
|
||||||
|
|
||||||
displayedColumns: string[] = ['timestamp_start', 'title', 'step_index', 'sub_name', 'percent_complete', 'actions'];
|
actionsFlex = 2;
|
||||||
|
minimizeButtons = false;
|
||||||
|
displayedColumnsBig: string[] = ['timestamp_start', 'title', 'sub_name', 'percent_complete', 'actions'];
|
||||||
|
displayedColumnsSmall: string[] = ['title', 'percent_complete', 'actions'];
|
||||||
|
displayedColumns: string[] = this.displayedColumnsBig;
|
||||||
dataSource = null; // new MatTableDataSource<Download>();
|
dataSource = null; // new MatTableDataSource<Download>();
|
||||||
|
|
||||||
|
// The purpose of this is to reduce code reuse for displaying these actions as icons or in a menu
|
||||||
|
downloadActions: DownloadAction[] = [
|
||||||
|
{
|
||||||
|
tooltip: $localize`Watch content`,
|
||||||
|
action: (download: Download) => this.watchContent(download),
|
||||||
|
show: (download: Download) => download.finished && !download.error,
|
||||||
|
icon: 'smart_display'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Show error`,
|
||||||
|
action: (download: Download) => this.showError(download),
|
||||||
|
show: (download: Download) => download.finished && !!download.error,
|
||||||
|
icon: 'warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Restart`,
|
||||||
|
action: (download: Download) => this.restartDownload(download),
|
||||||
|
show: (download: Download) => download.finished,
|
||||||
|
icon: 'restart_alt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Pause`,
|
||||||
|
action: (download: Download) => this.pauseDownload(download),
|
||||||
|
show: (download: Download) => !download.finished && (!download.paused || !download.finished_step),
|
||||||
|
icon: 'pause',
|
||||||
|
loading: (download: Download) => download.paused && !download.finished_step
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Resume`,
|
||||||
|
action: (download: Download) => this.resumeDownload(download),
|
||||||
|
show: (download: Download) => !download.finished && download.paused && download.finished_step,
|
||||||
|
icon: 'play_arrow'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Resume`,
|
||||||
|
action: (download: Download) => this.resumeDownload(download),
|
||||||
|
show: (download: Download) => !download.finished && download.paused && download.finished_step,
|
||||||
|
icon: 'play_arrow'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Cancel`,
|
||||||
|
action: (download: Download) => this.cancelDownload(download),
|
||||||
|
show: (download: Download) => false && !download.finished && !download.paused, // TODO: add possibility to cancel download
|
||||||
|
icon: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: $localize`Clear`,
|
||||||
|
action: (download: Download) => this.clearDownload(download),
|
||||||
|
show: (download: Download) => download.finished || download.paused,
|
||||||
|
icon: 'delete'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
downloads_retrieved = false;
|
downloads_retrieved = false;
|
||||||
|
|
||||||
|
innerWidth: number;
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
onResize(): void {
|
||||||
|
this.innerWidth = window.innerWidth;
|
||||||
|
this.recalculateColumns();
|
||||||
|
}
|
||||||
|
|
||||||
sort_downloads = (a: Download, b: Download): number => {
|
sort_downloads = (a: Download, b: Download): number => {
|
||||||
const result = b.timestamp_start - a.timestamp_start;
|
const result = b.timestamp_start - a.timestamp_start;
|
||||||
return result;
|
return result;
|
||||||
@@ -77,6 +119,10 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog, private clipboard: Clipboard) { }
|
constructor(public postsService: PostsService, private router: Router, private dialog: MatDialog, private clipboard: Clipboard) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Remove sub name as it's not necessary for one-off downloads
|
||||||
|
if (this.uids) this.displayedColumnsBig = this.displayedColumnsBig.filter(col => col !== 'sub_name');
|
||||||
|
this.innerWidth = window.innerWidth;
|
||||||
|
this.recalculateColumns();
|
||||||
if (this.postsService.initialized) {
|
if (this.postsService.initialized) {
|
||||||
this.getCurrentDownloadsRecurring();
|
this.getCurrentDownloadsRecurring();
|
||||||
} else {
|
} else {
|
||||||
@@ -164,8 +210,8 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pauseDownload(download_uid: string): void {
|
pauseDownload(download: Download): void {
|
||||||
this.postsService.pauseDownload(download_uid).subscribe(res => {
|
this.postsService.pauseDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
||||||
}
|
}
|
||||||
@@ -180,8 +226,8 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeDownload(download_uid: string): void {
|
resumeDownload(download: Download): void {
|
||||||
this.postsService.resumeDownload(download_uid).subscribe(res => {
|
this.postsService.resumeDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to resume download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to resume download! See server logs for more info.`);
|
||||||
}
|
}
|
||||||
@@ -196,8 +242,8 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
restartDownload(download_uid: string): void {
|
restartDownload(download: Download): void {
|
||||||
this.postsService.restartDownload(download_uid).subscribe(res => {
|
this.postsService.restartDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to restart download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to restart download! See server logs for more info.`);
|
||||||
} else {
|
} else {
|
||||||
@@ -208,16 +254,16 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelDownload(download_uid: string): void {
|
cancelDownload(download: Download): void {
|
||||||
this.postsService.cancelDownload(download_uid).subscribe(res => {
|
this.postsService.cancelDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to cancel download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to cancel download! See server logs for more info.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clearDownload(download_uid: string): void {
|
clearDownload(download: Download): void {
|
||||||
this.postsService.clearDownload(download_uid).subscribe(res => {
|
this.postsService.clearDownload(download['uid']).subscribe(res => {
|
||||||
if (!res['success']) {
|
if (!res['success']) {
|
||||||
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
this.postsService.openSnackBar($localize`Failed to pause download! See server logs for more info.`);
|
||||||
}
|
}
|
||||||
@@ -257,6 +303,7 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showError(download: Download): void {
|
showError(download: Download): void {
|
||||||
|
console.log(download)
|
||||||
const copyToClipboardEmitter = new EventEmitter<boolean>();
|
const copyToClipboardEmitter = new EventEmitter<boolean>();
|
||||||
this.dialog.open(ConfirmDialogComponent, {
|
this.dialog.open(ConfirmDialogComponent, {
|
||||||
data: {
|
data: {
|
||||||
@@ -276,4 +323,22 @@ export class DownloadsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recalculateColumns() {
|
||||||
|
if (this.innerWidth < 650) this.displayedColumns = this.displayedColumnsSmall;
|
||||||
|
else this.displayedColumns = this.displayedColumnsBig;
|
||||||
|
|
||||||
|
this.actionsFlex = this.uids || this.innerWidth < 800 ? 1 : 2;
|
||||||
|
|
||||||
|
if (this.innerWidth < 800 && !this.uids || this.innerWidth < 1100 && this.uids) this.minimizeButtons = true;
|
||||||
|
else this.minimizeButtons = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DownloadAction {
|
||||||
|
tooltip: string,
|
||||||
|
action: (download: Download) => void,
|
||||||
|
show: (download: Download) => boolean,
|
||||||
|
icon: string,
|
||||||
|
loading?: (download: Download) => boolean
|
||||||
|
}
|
||||||
@@ -1,30 +1,32 @@
|
|||||||
<div class="card-radius mat-elevation-z2" *ngFor="let notification of notifications; let i = index;">
|
<cdk-virtual-scroll-viewport itemSize="50" class="viewport" minBufferPx="1200" maxBufferPx="1200">
|
||||||
<mat-card class="notification-card card-radius">
|
<div #notification_parent class="notification-card-parent card-radius mat-elevation-z2" *cdkVirtualFor="let notification of notifications; let i = index;">
|
||||||
<mat-card-header>
|
<mat-card class="notification-card card-radius">
|
||||||
<mat-card-subtitle>
|
<mat-card-header>
|
||||||
<div>
|
<mat-card-subtitle>
|
||||||
<span class="notification-timestamp">{{notification.timestamp * 1000 | date:'short'}}</span>
|
<div>
|
||||||
</div>
|
<span class="notification-timestamp">{{notification.timestamp * 1000 | date:'short'}}</span>
|
||||||
</mat-card-subtitle>
|
</div>
|
||||||
<mat-card-title>
|
</mat-card-subtitle>
|
||||||
<ng-container *ngIf="NOTIFICATION_PREFIX[notification.type]">
|
<mat-card-title>
|
||||||
{{NOTIFICATION_PREFIX[notification.type]}}
|
<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>
|
</ng-container>
|
||||||
</mat-card-title>
|
</mat-card-content>
|
||||||
</mat-card-header>
|
<mat-card-actions class="notification-actions" *ngIf="notification.actions?.length > 0">
|
||||||
<mat-card-content>
|
<button matTooltip="Remove" i18n-matTooltip="Remove" (click)="emitDeleteNotification(notification.uid)" mat-icon-button><mat-icon>close</mat-icon></button>
|
||||||
<ng-container *ngIf="NOTIFICATION_SUFFIX_KEY[notification.type]">
|
<span *ngFor="let action of notification.actions">
|
||||||
<div style="word-break: break-word">
|
<button [matTooltip]="NOTIFICATION_ACTION_TO_STRING[action]" (click)="emitNotificationAction(notification, action)" mat-icon-button><mat-icon>{{NOTIFICATION_ICON[action]}}</mat-icon></button>
|
||||||
{{notification['data'][NOTIFICATION_SUFFIX_KEY[notification.type]]}}
|
</span>
|
||||||
</div>
|
</mat-card-actions>
|
||||||
</ng-container>
|
<span *ngIf="!notification.read" class="dot"></span>
|
||||||
</mat-card-content>
|
</mat-card>
|
||||||
<mat-card-actions *ngIf="notification.actions?.length > 0">
|
</div>
|
||||||
<button matTooltip="Remove" i18n-matTooltip="Remove" (click)="emitDeleteNotification(notification.uid)" mat-icon-button><mat-icon>close</mat-icon></button>
|
</cdk-virtual-scroll-viewport>
|
||||||
<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>
|
|
||||||
@@ -13,12 +13,21 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-card-parent {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-card {
|
.notification-card {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-actions {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.card-radius {
|
.card-radius {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
height: 166px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
@@ -30,4 +39,8 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.viewport {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -4,7 +4,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notifications-list-parent {
|
.notifications-list-parent {
|
||||||
max-height: 70vh;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0px 10px 10px 10px;
|
padding: 0px 10px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-list {
|
||||||
|
display: block
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<mat-chip-listbox [value]="selectedFilters" [multiple]="true" (change)="selectedFiltersChanged($event)">
|
<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-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>
|
</mat-chip-listbox>
|
||||||
<app-notifications-list (notificationAction)="notificationAction($event)" (deleteNotification)="deleteNotification($event)" [notifications]="filtered_notifications"></app-notifications-list>
|
<app-notifications-list class="notifications-list" [style.height]="list_height" (notificationAction)="notificationAction($event)" (deleteNotification)="deleteNotification($event)" [notifications]="filtered_notifications"></app-notifications-list>
|
||||||
</div>
|
</div>
|
||||||
<button style="margin: 10px 0px 2px 10px;" *ngIf="notifications?.length > 0" color="warn" (click)="deleteAllNotifications()" mat-stroked-button>Remove all</button>
|
<button style="margin: 10px 0px 2px 10px;" *ngIf="notifications?.length > 0" color="warn" (click)="deleteAllNotifications()" mat-stroked-button>Remove all</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export class NotificationsComponent implements OnInit {
|
|||||||
|
|
||||||
notifications: Notification[] = null;
|
notifications: Notification[] = null;
|
||||||
filtered_notifications: Notification[] = null;
|
filtered_notifications: Notification[] = null;
|
||||||
|
list_height = '65vh';
|
||||||
|
|
||||||
@Output() notificationCount = new EventEmitter<number>();
|
@Output() notificationCount = new EventEmitter<number>();
|
||||||
|
|
||||||
@@ -110,6 +111,8 @@ export class NotificationsComponent implements OnInit {
|
|||||||
|
|
||||||
filterNotifications(): void {
|
filterNotifications(): void {
|
||||||
this.filtered_notifications = this.notifications.filter(notification => this.selectedFilters.length === 0 || this.selectedFilters.includes(notification.type));
|
this.filtered_notifications = this.notifications.filter(notification => this.selectedFilters.length === 0 || this.selectedFilters.includes(notification.type));
|
||||||
|
// We need to do this to get the virtual scroll component to have an appropriate height
|
||||||
|
this.calculateListHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedFiltersChanged(event: MatChipListboxChange): void {
|
selectedFiltersChanged(event: MatChipListboxChange): void {
|
||||||
@@ -117,6 +120,12 @@ export class NotificationsComponent implements OnInit {
|
|||||||
this.filterNotifications();
|
this.filterNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calculateListHeight() {
|
||||||
|
const avgHeight = 166;
|
||||||
|
const calcHeight = this.filtered_notifications.length * avgHeight;
|
||||||
|
this.list_height = calcHeight > window.innerHeight*0.65 ? '65vh' : `${calcHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
originalOrder = (): number => {
|
originalOrder = (): number => {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
import { Task } from 'api-types';
|
import { Task, TaskType } from 'api-types';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -9,7 +9,7 @@ import { PostsService } from 'app/posts.services';
|
|||||||
styleUrls: ['./task-settings.component.scss']
|
styleUrls: ['./task-settings.component.scss']
|
||||||
})
|
})
|
||||||
export class TaskSettingsComponent {
|
export class TaskSettingsComponent {
|
||||||
task_key: string;
|
task_key: TaskType;
|
||||||
new_options = {};
|
new_options = {};
|
||||||
task: Task = null;
|
task: Task = null;
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
|
<mat-header-cell *matHeaderCellDef> <ng-container i18n="Actions">Actions</ng-container> </mat-header-cell>
|
||||||
<mat-cell *matCellDef="let element">
|
<mat-cell *matCellDef="let element">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<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;">
|
<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>
|
<ng-container>
|
||||||
<button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button>
|
<button (click)="confirmTask(element.key)" [disabled]="element.running || element.confirming" mat-stroked-button>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ConfirmDialogComponent } from 'app/dialogs/confirm-dialog/confirm-dialo
|
|||||||
import { RestoreDbDialogComponent } from 'app/dialogs/restore-db-dialog/restore-db-dialog.component';
|
import { RestoreDbDialogComponent } from 'app/dialogs/restore-db-dialog/restore-db-dialog.component';
|
||||||
import { UpdateTaskScheduleDialogComponent } from 'app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component';
|
import { UpdateTaskScheduleDialogComponent } from 'app/dialogs/update-task-schedule-dialog/update-task-schedule-dialog.component';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { Task } from 'api-types';
|
import { Task, TaskType } from 'api-types';
|
||||||
import { TaskSettingsComponent } from '../task-settings/task-settings.component';
|
import { TaskSettingsComponent } from '../task-settings/task-settings.component';
|
||||||
import { Clipboard } from '@angular/cdk/clipboard';
|
import { Clipboard } from '@angular/cdk/clipboard';
|
||||||
|
|
||||||
@@ -28,6 +28,15 @@ export class TasksComponent implements OnInit {
|
|||||||
|
|
||||||
db_backups = [];
|
db_backups = [];
|
||||||
|
|
||||||
|
TASKS_TO_REQUIRE_DIALOG: { [key in TaskType]? : {dialogTitle: string, dialogText: string, submitText: string, warnSubmitColor: boolean}} = {
|
||||||
|
[TaskType.REBUILD_DATABASE]: {
|
||||||
|
dialogTitle: $localize`Rebuild database`,
|
||||||
|
dialogText: $localize`Are you sure you want to rebuild the database? All missing users, subscriptions, and files will be reimported. Note that if missing users are detected, they will be created with the password: 'password'. A backup of your current database will be created.`,
|
||||||
|
submitText: $localize`Rebuild database`,
|
||||||
|
warnSubmitColor: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
@@ -79,7 +88,29 @@ export class TasksComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
runTask(task_key: string): void {
|
runTask(task_key: TaskType): void {
|
||||||
|
const taskToRequireDialog = this.TASKS_TO_REQUIRE_DIALOG[task_key];
|
||||||
|
if (taskToRequireDialog) {
|
||||||
|
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
||||||
|
data: {
|
||||||
|
dialogTitle: taskToRequireDialog['dialogTitle'],
|
||||||
|
dialogText: taskToRequireDialog['dialogText'],
|
||||||
|
submitText: taskToRequireDialog['submitText'],
|
||||||
|
warnSubmitColor: taskToRequireDialog['warnSubmitColor']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialogRef.afterClosed().subscribe(confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
this._runTask(task_key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._runTask(task_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
_runTask(task_key: TaskType): void {
|
||||||
this.postsService.runTask(task_key).subscribe(res => {
|
this.postsService.runTask(task_key).subscribe(res => {
|
||||||
this.getTasks();
|
this.getTasks();
|
||||||
this.getDBBackups();
|
this.getDBBackups();
|
||||||
@@ -91,7 +122,7 @@ export class TasksComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmTask(task_key: string): void {
|
confirmTask(task_key: TaskType): void {
|
||||||
this.postsService.confirmTask(task_key).subscribe(res => {
|
this.postsService.confirmTask(task_key).subscribe(res => {
|
||||||
this.getTasks();
|
this.getTasks();
|
||||||
if (res['success']) this.postsService.openSnackBar($localize`Successfully confirmed task!`);
|
if (res['success']) this.postsService.openSnackBar($localize`Successfully confirmed task!`);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const CURRENT_VERSION = 'v4.3.1';
|
export const CURRENT_VERSION = 'v4.3.2';
|
||||||
|
|||||||
@@ -35,36 +35,6 @@
|
|||||||
<p>
|
<p>
|
||||||
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container> <a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a> <ng-container i18n="About bug suffix">to create an issue!</ng-container>
|
<ng-container i18n="About bug prefix">Found a bug or have a suggestion?</ng-container> <a [href]="issuesLink" target="_blank"><ng-container i18n="About bug click here">Click here</ng-container></a> <ng-container i18n="About bug suffix">to create an issue!</ng-container>
|
||||||
</p>
|
</p>
|
||||||
<mat-divider></mat-divider>
|
|
||||||
<div style="margin-top: 10px;">
|
|
||||||
<h5>Personal settings:</h5>
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label i18n="Sidepanel mode">Sidepanel mode</mat-label>
|
|
||||||
<mat-select [(ngModel)]="sidepanel_mode" (selectionChange)="sidePanelModeChanged($event.value)">
|
|
||||||
<mat-option value="over">
|
|
||||||
Over
|
|
||||||
</mat-option>
|
|
||||||
<mat-option 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 value="large">
|
|
||||||
Large
|
|
||||||
</mat-option>
|
|
||||||
<mat-option value="medium">
|
|
||||||
Medium
|
|
||||||
</mat-option>
|
|
||||||
<mat-option value="small">
|
|
||||||
Small
|
|
||||||
</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ export class AboutDialogComponent implements OnInit {
|
|||||||
checking_for_updates = true;
|
checking_for_updates = true;
|
||||||
|
|
||||||
current_version_tag = CURRENT_VERSION;
|
current_version_tag = CURRENT_VERSION;
|
||||||
sidepanel_mode = this.postsService.sidepanel_mode;
|
|
||||||
card_size = this.postsService.card_size;
|
|
||||||
|
|
||||||
constructor(public postsService: PostsService) { }
|
constructor(public postsService: PostsService) { }
|
||||||
|
|
||||||
@@ -31,15 +29,4 @@ export class AboutDialogComponent implements OnInit {
|
|||||||
this.latestGithubRelease = res;
|
this.latestGithubRelease = res;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sidePanelModeChanged(new_mode) {
|
|
||||||
localStorage.setItem('sidepanel_mode', new_mode);
|
|
||||||
this.postsService.sidepanel_mode = new_mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
cardSizeOptionChanged(new_size) {
|
|
||||||
localStorage.setItem('card_size', new_size);
|
|
||||||
this.postsService.card_size = new_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,19 +13,52 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
</div>
|
</div>
|
||||||
|
<mat-divider style="margin-bottom: 20px"></mat-divider>
|
||||||
</div>
|
</div>
|
||||||
|
<mat-form-field color="accent">
|
||||||
<div *ngIf="!postsService.isLoggedIn || !postsService.user">
|
<mat-label><ng-container i18n="Language select label">Language</ng-container></mat-label>
|
||||||
<h5><mat-icon>warn</mat-icon><ng-container i18n="Not logged in notification">You are not logged in.</ng-container></h5>
|
<mat-select (selectionChange)="localeSelectChanged($event.value)" [(value)]="initialLocale">
|
||||||
<button (click)="loginClicked()" mat-raised-button color="primary"><ng-container i18n="Login">Login</ng-container></button>
|
<mat-option *ngFor="let locale of supported_locales" [value]="locale">
|
||||||
</div>
|
<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-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<div style="width: 100%">
|
<div style="width: 100%">
|
||||||
<div style="position: relative">
|
<div style="position: relative">
|
||||||
<button mat-stroked-button mat-dialog-close color="primary"><ng-container i18n="Close">Close</ng-container></button>
|
<button mat-stroked-button mat-dialog-close color="primary"><ng-container i18n="Close">Close</ng-container></button>
|
||||||
<button style="position: absolute; right: 0px;" (click)="logoutClicked()" mat-stroked-button color="warn"><ng-container i18n="Logout">Logout</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>
|
</div>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MatDialogRef } from '@angular/material/dialog';
|
import { MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { isoLangs } from './locales_list';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-profile-dialog',
|
selector: 'app-user-profile-dialog',
|
||||||
@@ -10,9 +11,24 @@ import { MatDialogRef } from '@angular/material/dialog';
|
|||||||
})
|
})
|
||||||
export class UserProfileDialogComponent implements OnInit {
|
export class UserProfileDialogComponent implements OnInit {
|
||||||
|
|
||||||
|
all_locales = isoLangs;
|
||||||
|
supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'pt', 'it', 'ca', 'cs', 'nb', 'ru', 'zh', 'ko', 'id', 'en-GB'];
|
||||||
|
initialLocale = localStorage.getItem('locale');
|
||||||
|
sidepanel_mode = this.postsService.sidepanel_mode;
|
||||||
|
card_size = this.postsService.card_size;
|
||||||
|
|
||||||
constructor(public postsService: PostsService, private router: Router, public dialogRef: MatDialogRef<UserProfileDialogComponent>) { }
|
constructor(public postsService: PostsService, private router: Router, public dialogRef: MatDialogRef<UserProfileDialogComponent>) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.postsService.getSupportedLocales().subscribe(res => {
|
||||||
|
if (res && res['supported_locales']) {
|
||||||
|
this.supported_locales = ['en', 'en-GB']; // required
|
||||||
|
this.supported_locales = this.supported_locales.concat(res['supported_locales']);
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
console.error(`Failed to retrieve list of supported languages! You may need to run: 'node src/postbuild.mjs'. Error below:`);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loginClicked() {
|
loginClicked() {
|
||||||
@@ -25,4 +41,19 @@ export class UserProfileDialogComponent implements OnInit {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localeSelectChanged(new_val: string): void {
|
||||||
|
localStorage.setItem('locale', new_val);
|
||||||
|
this.postsService.openSnackBar($localize`Language successfully changed! Reload to update the page.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
sidePanelModeChanged(new_mode) {
|
||||||
|
localStorage.setItem('sidepanel_mode', new_mode);
|
||||||
|
this.postsService.sidepanel_mode = new_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
cardSizeOptionChanged(new_size) {
|
||||||
|
localStorage.setItem('card_size', new_size);
|
||||||
|
this.postsService.card_size = new_size;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: center;" *ngIf="downloads && downloads.length > 0 && !autoplay">
|
<div style="display: flex; justify-content: center;" *ngIf="downloads && downloads.length > 0 && !autoplay">
|
||||||
<app-downloads style="width: 80%; margin-bottom: 10px" [uids]="download_uids"></app-downloads>
|
<app-downloads style="width: 80%; min-width: 350px; margin-bottom: 10px" [uids]="download_uids"></app-downloads>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
||||||
|
|||||||
@@ -37,11 +37,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
bottom: 1px;
|
bottom: -2px;
|
||||||
left: 2px;
|
left: 6px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.save-button {
|
.save-button {
|
||||||
right: 25px;
|
right: 25px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -85,13 +89,6 @@
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-div {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 12px;
|
|
||||||
top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skip-ad-button {
|
.skip-ad-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
|
|||||||
@@ -22,20 +22,22 @@
|
|||||||
</p>
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!db_file || !db_file['description']">
|
<ng-container *ngIf="!db_file || !db_file['description']">
|
||||||
<p style="text-align: center;">
|
<p i18n="No description" style="text-align: center;">
|
||||||
No description available.
|
No description available.
|
||||||
</p>
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<ng-container *ngIf="db_playlist">
|
<span class="buttons" *ngIf="db_playlist">
|
||||||
<button (click)="downloadContent()" [disabled]="downloading" mat-icon-button><mat-icon>save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
|
<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>
|
<button *ngIf="(!postsService.isLoggedIn || postsService.permissions.includes('sharing')) && !auto" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
||||||
</ng-container>
|
</span>
|
||||||
<ng-container *ngIf="db_file">
|
<span class="buttons" *ngIf="db_file">
|
||||||
<button (click)="downloadFile()" [disabled]="downloading" mat-icon-button><mat-icon>cloud_download</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="35"></mat-spinner></button>
|
<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>
|
<button *ngIf="!postsService.isLoggedIn || postsService.permissions.includes('sharing')" (click)="openShareDialog()" mat-icon-button><mat-icon>share</mat-icon></button>
|
||||||
</ng-container>
|
</span>
|
||||||
<ng-container *ngIf="db_file || playlist[currentIndex]"></ng-container>
|
<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 (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>
|
<button *ngIf="db_file && db_file.url.includes('twitch.tv')" (click)="drawer.toggle()" mat-icon-button><mat-icon>chat</mat-icon></button>
|
||||||
@@ -56,14 +58,6 @@
|
|||||||
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
|
<app-twitch-chat #twitchchat [db_file]="db_file" [current_timestamp]="api.currentTime" [sub]="subscription"></app-twitch-chat>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-drawer>
|
</mat-drawer>
|
||||||
|
|
||||||
<!-- <div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
|
|
||||||
<div class="spinner-div">
|
|
||||||
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
|
|
||||||
</div>
|
|
||||||
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button><ng-container i18n="Playlist save changes button">Save changes</ng-container> <mat-icon>update</mat-icon></button>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
</mat-drawer-container>
|
</mat-drawer-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
url = null;
|
url = null;
|
||||||
name = null;
|
name = null;
|
||||||
|
|
||||||
innerWidth: number;
|
|
||||||
|
|
||||||
downloading = false;
|
downloading = false;
|
||||||
|
|
||||||
save_volume_timer = null;
|
save_volume_timer = null;
|
||||||
@@ -70,14 +68,7 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
@ViewChild('twitchchat') twitchChat: TwitchChatComponent;
|
@ViewChild('twitchchat') twitchChat: TwitchChatComponent;
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
|
||||||
onResize(): void {
|
|
||||||
this.innerWidth = window.innerWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.innerWidth = window.innerWidth;
|
|
||||||
|
|
||||||
this.playlist_id = this.route.snapshot.paramMap.get('playlist_id');
|
this.playlist_id = this.route.snapshot.paramMap.get('playlist_id');
|
||||||
this.uid = this.route.snapshot.paramMap.get('uid');
|
this.uid = this.route.snapshot.paramMap.get('uid');
|
||||||
this.sub_id = this.route.snapshot.paramMap.get('sub_id');
|
this.sub_id = this.route.snapshot.paramMap.get('sub_id');
|
||||||
|
|||||||
@@ -112,9 +112,10 @@ import {
|
|||||||
ImportArchiveRequest,
|
ImportArchiveRequest,
|
||||||
Archive,
|
Archive,
|
||||||
Subscription,
|
Subscription,
|
||||||
RestartDownloadResponse
|
RestartDownloadResponse,
|
||||||
|
TaskType
|
||||||
} from '../api-types';
|
} from '../api-types';
|
||||||
import { isoLangs } from './settings/locales_list';
|
import { isoLangs } from './dialogs/user-profile-dialog/locales_list';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { MatDrawerMode } from '@angular/material/sidenav';
|
import { MatDrawerMode } from '@angular/material/sidenav';
|
||||||
|
|
||||||
@@ -640,32 +641,32 @@ export class PostsService implements CanActivate {
|
|||||||
return this.http.post<SuccessObject>(this.path + 'resetTasks', {}, this.httpOptions);
|
return this.http.post<SuccessObject>(this.path + 'resetTasks', {}, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTask(task_key: string) {
|
getTask(task_key: TaskType) {
|
||||||
const body: GetTaskRequest = {task_key: task_key};
|
const body: GetTaskRequest = {task_key: task_key};
|
||||||
return this.http.post<GetTaskResponse>(this.path + 'getTask', body, this.httpOptions);
|
return this.http.post<GetTaskResponse>(this.path + 'getTask', body, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
runTask(task_key: string) {
|
runTask(task_key: TaskType) {
|
||||||
const body: GetTaskRequest = {task_key: task_key};
|
const body: GetTaskRequest = {task_key: task_key};
|
||||||
return this.http.post<SuccessObject>(this.path + 'runTask', body, this.httpOptions);
|
return this.http.post<SuccessObject>(this.path + 'runTask', body, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmTask(task_key: string) {
|
confirmTask(task_key: TaskType) {
|
||||||
const body: GetTaskRequest = {task_key: task_key};
|
const body: GetTaskRequest = {task_key: task_key};
|
||||||
return this.http.post<SuccessObject>(this.path + 'confirmTask', body, this.httpOptions);
|
return this.http.post<SuccessObject>(this.path + 'confirmTask', body, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTaskSchedule(task_key: string, schedule: Schedule) {
|
updateTaskSchedule(task_key: TaskType, schedule: Schedule) {
|
||||||
const body: UpdateTaskScheduleRequest = {task_key: task_key, new_schedule: schedule};
|
const body: UpdateTaskScheduleRequest = {task_key: task_key, new_schedule: schedule};
|
||||||
return this.http.post<SuccessObject>(this.path + 'updateTaskSchedule', body, this.httpOptions);
|
return this.http.post<SuccessObject>(this.path + 'updateTaskSchedule', body, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTaskData(task_key: string, data: any) {
|
updateTaskData(task_key: TaskType, data: any) {
|
||||||
const body: UpdateTaskDataRequest = {task_key: task_key, new_data: data};
|
const body: UpdateTaskDataRequest = {task_key: task_key, new_data: data};
|
||||||
return this.http.post<SuccessObject>(this.path + 'updateTaskData', body, this.httpOptions);
|
return this.http.post<SuccessObject>(this.path + 'updateTaskData', body, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTaskOptions(task_key: string, options: any) {
|
updateTaskOptions(task_key: TaskType, options: any) {
|
||||||
const body: UpdateTaskOptionsRequest = {task_key: task_key, new_options: options};
|
const body: UpdateTaskOptionsRequest = {task_key: task_key, new_options: options};
|
||||||
return this.http.post<SuccessObject>(this.path + 'updateTaskOptions', body, this.httpOptions);
|
return this.http.post<SuccessObject>(this.path + 'updateTaskOptions', body, this.httpOptions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,23 +78,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
|
||||||
<div *ngIf="new_config" class="container-fluid">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 mt-3">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
<!-- Downloader -->
|
<!-- Downloader -->
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Component, OnInit, EventEmitter } from '@angular/core';
|
import { Component, OnInit, EventEmitter } from '@angular/core';
|
||||||
import { PostsService } from 'app/posts.services';
|
import { PostsService } from 'app/posts.services';
|
||||||
import { isoLangs } from './locales_list';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import {DomSanitizer} from '@angular/platform-browser';
|
import {DomSanitizer} from '@angular/platform-browser';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
@@ -22,10 +21,6 @@ import { GenerateRssUrlComponent } from 'app/dialogs/generate-rss-url/generate-r
|
|||||||
styleUrls: ['./settings.component.scss']
|
styleUrls: ['./settings.component.scss']
|
||||||
})
|
})
|
||||||
export class SettingsComponent implements OnInit {
|
export class SettingsComponent implements OnInit {
|
||||||
all_locales = isoLangs;
|
|
||||||
supported_locales = ['en', 'es', 'de', 'fr', 'nl', 'pt', 'it', 'ca', 'cs', 'nb', 'ru', 'zh', 'ko', 'id', 'en-GB'];
|
|
||||||
initialLocale = localStorage.getItem('locale');
|
|
||||||
|
|
||||||
initial_config = null;
|
initial_config = null;
|
||||||
new_config = null
|
new_config = null
|
||||||
loading_config = false;
|
loading_config = false;
|
||||||
@@ -83,16 +78,6 @@ export class SettingsComponent implements OnInit {
|
|||||||
|
|
||||||
const tab = this.route.snapshot.paramMap.get('tab');
|
const tab = this.route.snapshot.paramMap.get('tab');
|
||||||
this.tabIndex = tab && this.TAB_TO_INDEX[tab] ? this.TAB_TO_INDEX[tab] : 0;
|
this.tabIndex = tab && this.TAB_TO_INDEX[tab] ? this.TAB_TO_INDEX[tab] : 0;
|
||||||
|
|
||||||
this.postsService.getSupportedLocales().subscribe(res => {
|
|
||||||
if (res && res['supported_locales']) {
|
|
||||||
this.supported_locales = ['en', 'en-GB']; // required
|
|
||||||
this.supported_locales = this.supported_locales.concat(res['supported_locales']);
|
|
||||||
}
|
|
||||||
}, err => {
|
|
||||||
console.error(`Failed to retrieve list of supported languages! You may need to run: 'node src/postbuild.mjs'. Error below:`);
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig(): void {
|
getConfig(): void {
|
||||||
@@ -207,11 +192,6 @@ export class SettingsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
localeSelectChanged(new_val: string): void {
|
|
||||||
localStorage.setItem('locale', new_val);
|
|
||||||
this.postsService.openSnackBar($localize`Language successfully changed! Reload to update the page.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
generateBookmarklet(): void {
|
generateBookmarklet(): void {
|
||||||
this.bookmarksite('YTDL-Material', this.generated_bookmarklet_code);
|
this.bookmarksite('YTDL-Material', this.generated_bookmarklet_code);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1432,7 +1432,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
|
<trans-unit id="994363f08f9fbfa3b3994ff7b35c6904fdff18d8" datatype="html">
|
||||||
<source>Profile</source>
|
<source>Profile</source>
|
||||||
<target state="translated">资料</target>
|
<target state="translated">个人资料</target>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">app/app.component.html</context>
|
<context context-type="sourcefile">app/app.component.html</context>
|
||||||
<context context-type="linenumber">19</context>
|
<context context-type="linenumber">19</context>
|
||||||
@@ -5015,6 +5015,24 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Use gotify API setting</note>
|
<note priority="1" from="description">Use gotify API setting</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="7cedb649779673568447b994463b2882c4e0436a" datatype="html">
|
||||||
|
<source>Slack Webhook URL</source>
|
||||||
|
<target state="translated">Slack Webhook 网址</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">397</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Slack Webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3264d82792954815be755b3da01e2625458711dc" datatype="html">
|
||||||
|
<source>Discord Webhook URL</source>
|
||||||
|
<target state="translated">Discord Webhook 网址</target>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">390</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">Discord Webhook URL</note>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user