mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: implement custom path support for youtube engines
This commit is contained in:
parent
f8663875e3
commit
fc9121b488
9
Makefile
9
Makefile
@ -45,4 +45,11 @@ gensums:
|
||||
sh -c scripts/gensums.sh
|
||||
|
||||
migrate:
|
||||
dart run drift_dev make-migrations
|
||||
dart run drift_dev make-migrations
|
||||
|
||||
dmg:
|
||||
flutter build macos &&\
|
||||
if [ -f dist/Spotube-macos-universal.dmg ];\
|
||||
then rm dist/Spotube-macos-universal.dmg;\
|
||||
fi &&\
|
||||
appdmg appdmg.json dist/Spotube-macos-universal.dmg
|
@ -1,9 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
|
||||
|
||||
void useCheckYtDlpInstalled(WidgetRef ref) {
|
||||
@ -17,8 +20,12 @@ void useCheckYtDlpInstalled(WidgetRef ref) {
|
||||
),
|
||||
);
|
||||
|
||||
final customPath =
|
||||
KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp);
|
||||
|
||||
if (youtubeEngine == YoutubeClientEngine.ytDlp &&
|
||||
!await YtDlpEngine.isInstalled() &&
|
||||
(customPath == null || !await File(customPath).exists()) &&
|
||||
context.mounted) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
|
@ -418,6 +418,9 @@
|
||||
"no_logs_found": "No logs found",
|
||||
"youtube_engine": "YouTube Engine",
|
||||
"youtube_engine_not_installed_title": "{engine} is not installed",
|
||||
"youtube_engine_not_installed_message": "{engine} is not installed in your system.\nPlease install it and make sure it's available in the PATH variable\n\nAfter installing, restart the app",
|
||||
"download": "Download"
|
||||
"youtube_engine_not_installed_message": "{engine} is not installed in your system.",
|
||||
"youtube_engine_set_path": "Make sure it's available in the PATH variable or\nset the absolute path to the {engine} executable below",
|
||||
"youtube_engine_unix_issue_message": "In macOS/Linux/unix like OS's, setting path on .zshrc/.bashrc/.bash_profile etc. won't work.\nYou need to set the path in the shell configuration file",
|
||||
"download": "Download",
|
||||
"file_not_found": "File not found"
|
||||
}
|
@ -84,10 +84,15 @@ Future<void> main(List<String> rawArgs) async {
|
||||
MetadataGod.initialize();
|
||||
}
|
||||
|
||||
await KVStoreService.initialize();
|
||||
|
||||
if (kIsDesktop) {
|
||||
await windowManager.setPreventClose(true);
|
||||
await YtDlp.instance
|
||||
.setBinaryLocation("yt-dlp${kIsWindows ? '.exe' : ''}")
|
||||
.setBinaryLocation(
|
||||
KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ??
|
||||
"yt-dlp${kIsWindows ? '.exe' : ''}",
|
||||
)
|
||||
.catchError((e, stack) => null);
|
||||
await FlutterDiscordRPC.initialize(Env.discordAppId);
|
||||
}
|
||||
@ -96,7 +101,6 @@ Future<void> main(List<String> rawArgs) async {
|
||||
await SMTCWindows.initialize();
|
||||
}
|
||||
|
||||
await KVStoreService.initialize();
|
||||
await EncryptedKvStoreService.initialize();
|
||||
|
||||
final database = AppDatabase();
|
||||
|
@ -1,9 +1,18 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/form/text_form_field.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:yt_dlp_dart/yt_dlp_dart.dart';
|
||||
|
||||
const engineDownloadUrls = {
|
||||
YoutubeClientEngine.ytDlp:
|
||||
@ -19,6 +28,9 @@ class YouTubeEngineNotInstalledDialog extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final controller = useTextEditingController();
|
||||
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
spacing: 8,
|
||||
@ -31,33 +43,77 @@ class YouTubeEngineNotInstalledDialog extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.youtube_engine_not_installed_message(engine.label),
|
||||
),
|
||||
if (engineDownloadUrls[engine] != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("${context.l10n.download}:"),
|
||||
Button.link(
|
||||
child: Text(engineDownloadUrls[engine]!.split("?").first),
|
||||
onPressed: () async {
|
||||
launchUrl(Uri.parse(engineDownloadUrls[engine]!));
|
||||
},
|
||||
),
|
||||
],
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.youtube_engine_not_installed_message(engine.label),
|
||||
),
|
||||
],
|
||||
if (engineDownloadUrls[engine] != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("${context.l10n.download}:"),
|
||||
Button.link(
|
||||
child: Text(engineDownloadUrls[engine]!.split("?").first),
|
||||
onPressed: () async {
|
||||
launchUrl(Uri.parse(engineDownloadUrls[engine]!));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(context.l10n.youtube_engine_set_path(engine.label)),
|
||||
const Gap(8),
|
||||
FormBuilder(
|
||||
key: formKey,
|
||||
child: TextFormBuilderField(
|
||||
name: "path",
|
||||
controller: controller,
|
||||
placeholder: Text(switch (context.theme.platform) {
|
||||
TargetPlatform.macOS => "e.g. /opt/homebrew/bin/yt-dlp",
|
||||
TargetPlatform.windows =>
|
||||
r"e.g. C:\Program Files\yt-dlp\yt-dlp.exe",
|
||||
_ => "e.g. /home/user/.local/bin/yt-dlp",
|
||||
}),
|
||||
),
|
||||
),
|
||||
if (kIsMacOS || kIsLinux)
|
||||
Text(context.l10n.youtube_engine_unix_issue_message),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Button.text(
|
||||
onPressed: () {
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
Button.secondary(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(context.l10n.ok),
|
||||
onPressed: () async {
|
||||
if (controller.text.isNotEmpty) {
|
||||
if (!await File(controller.text).exists() && context.mounted) {
|
||||
formKey.currentState?.fields["path"]
|
||||
?.invalidate(context.l10n.file_not_found);
|
||||
return;
|
||||
}
|
||||
await KVStoreService.setYoutubeEnginePath(
|
||||
engine,
|
||||
controller.text,
|
||||
);
|
||||
if (engine == YoutubeClientEngine.ytDlp) {
|
||||
await YtDlp.instance.setBinaryLocation(controller.text);
|
||||
}
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(context.l10n.save),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -214,12 +214,12 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
if (value == YoutubeClientEngine.ytDlp &&
|
||||
!await YtDlpEngine.isInstalled() &&
|
||||
context.mounted) {
|
||||
await showDialog(
|
||||
final hasInstalled = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
YouTubeEngineNotInstalledDialog(engine: value),
|
||||
);
|
||||
return;
|
||||
if (hasInstalled != true) return;
|
||||
}
|
||||
preferencesNotifier.setYoutubeClientEngine(value);
|
||||
},
|
||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@ -87,4 +88,31 @@ abstract class KVStoreService {
|
||||
sharedPreferences.getBool('hasMigratedToDrift') ?? false;
|
||||
static Future<void> setHasMigratedToDrift(bool value) async =>
|
||||
await sharedPreferences.setBool('hasMigratedToDrift', value);
|
||||
|
||||
static Map<String, dynamic>? get _youtubeEnginePaths {
|
||||
final jsonRaw = sharedPreferences.getString('ytDlpPath');
|
||||
|
||||
if (jsonRaw == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return jsonDecode(jsonRaw);
|
||||
}
|
||||
|
||||
static String? getYoutubeEnginePath(YoutubeClientEngine engine) {
|
||||
return _youtubeEnginePaths?[engine.name];
|
||||
}
|
||||
|
||||
static Future<void> setYoutubeEnginePath(
|
||||
YoutubeClientEngine engine,
|
||||
String path,
|
||||
) async {
|
||||
await sharedPreferences.setString(
|
||||
'ytDlpPath',
|
||||
jsonEncode({
|
||||
...?_youtubeEnginePaths,
|
||||
engine.name: path,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2737,11 +2737,9 @@ packages:
|
||||
yt_dlp_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "4199bb019542bae361fbb38b3448b3583fbca022"
|
||||
resolved-ref: "4199bb019542bae361fbb38b3448b3583fbca022"
|
||||
url: "https://github.com/KRTirtho/yt_dlp_dart.git"
|
||||
source: git
|
||||
path: "../yt_dlp_dart"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0"
|
||||
sdks:
|
||||
dart: ">=3.6.1 <4.0.0"
|
||||
|
@ -141,9 +141,7 @@ dependencies:
|
||||
url: https://github.com/Hexer10/youtube_explode_dart.git
|
||||
ref: e519db65ad0b0a40b12f69285932f9db509da3cf
|
||||
yt_dlp_dart:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/yt_dlp_dart.git
|
||||
ref: 4199bb019542bae361fbb38b3448b3583fbca022
|
||||
path: ../yt_dlp_dart
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.13
|
||||
|
@ -19,7 +19,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
@ -42,7 +45,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
@ -65,7 +71,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
@ -88,7 +97,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"de": [
|
||||
@ -111,7 +123,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"es": [
|
||||
@ -134,7 +149,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
@ -157,7 +175,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
@ -180,7 +201,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
@ -203,7 +227,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
@ -226,7 +253,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
@ -249,7 +279,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"id": [
|
||||
@ -272,7 +305,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"it": [
|
||||
@ -295,7 +331,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
@ -318,7 +357,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ka": [
|
||||
@ -341,7 +383,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
@ -364,7 +409,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ne": [
|
||||
@ -387,7 +435,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
@ -410,7 +461,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
@ -433,7 +487,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
@ -456,7 +513,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
@ -479,7 +539,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"th": [
|
||||
@ -502,7 +565,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
@ -525,7 +591,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
@ -548,7 +617,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
@ -571,7 +643,10 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
@ -594,6 +669,9 @@
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
]
|
||||
}
|
||||
|
@ -22,8 +22,8 @@ SetupIconFile={{SETUP_ICON_FILE}}
|
||||
WizardStyle=modern
|
||||
WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp"
|
||||
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
|
||||
[Languages]
|
||||
{% for locale in LOCALES %}
|
||||
|
Loading…
Reference in New Issue
Block a user