mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
implemented custom logger
[android] fixed solid color screen until the player on the navbar is stopped
This commit is contained in:
parent
8dea8dc051
commit
0cf5bfea50
@ -4,6 +4,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class ArtistAlbumView extends ConsumerStatefulWidget {
|
||||
@ -23,6 +24,8 @@ class _ArtistAlbumViewState extends ConsumerState<ArtistAlbumView> {
|
||||
final PagingController<int, Album> _pagingController =
|
||||
PagingController<int, Album>(firstPageKey: 0);
|
||||
|
||||
final logger = createLogger(ArtistAlbumView);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -51,8 +54,7 @@ class _ArtistAlbumViewState extends ConsumerState<ArtistAlbumView> {
|
||||
_pagingController.appendPage(items, albums.nextOffset);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[ArtistAlbumView._fetchPage] $e");
|
||||
print(stack);
|
||||
logger.e(e, null, stack);
|
||||
_pagingController.error = e;
|
||||
}
|
||||
}
|
||||
|
@ -5,17 +5,20 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/hooks/usePagingController.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class CategoryCard extends HookWidget {
|
||||
final Category category;
|
||||
final Iterable<PlaylistSimple>? playlists;
|
||||
const CategoryCard(
|
||||
CategoryCard(
|
||||
this.category, {
|
||||
Key? key,
|
||||
this.playlists,
|
||||
}) : super(key: key);
|
||||
|
||||
final logger = createLogger(CategoryCard);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@ -68,9 +71,7 @@ class CategoryCard extends HookWidget {
|
||||
if (!_error.value) _error.value = true;
|
||||
pagingController.error = e;
|
||||
}
|
||||
print(
|
||||
"[CategoryCard.pagingController.addPageRequestListener] $e");
|
||||
print(stack);
|
||||
logger.e("pagingController.addPageRequestListener", e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import 'package:spotube/hooks/useHotKeys.dart';
|
||||
import 'package:spotube/hooks/usePagingController.dart';
|
||||
import 'package:spotube/hooks/useSharedPreferences.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/models/generated_secrets.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
@ -38,7 +39,8 @@ List<String> spotifyScopes = [
|
||||
];
|
||||
|
||||
class Home extends HookConsumerWidget {
|
||||
const Home({Key? key}) : super(key: key);
|
||||
Home({Key? key}) : super(key: key);
|
||||
final logger = createLogger(Home);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -82,8 +84,7 @@ class Home extends HookConsumerWidget {
|
||||
}
|
||||
} catch (e, stack) {
|
||||
pagingController.error = e;
|
||||
print("[Home.pagingController.addPageRequestListener] $e");
|
||||
print(stack);
|
||||
logger.e("pagingController.addPageRequestListener", e, stack);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -146,16 +147,14 @@ class Home extends HookConsumerWidget {
|
||||
clientSecret: clientSecret,
|
||||
);
|
||||
}
|
||||
print("[Home.useEffect.spotify.getCredentials]: $e");
|
||||
print(stack);
|
||||
logger.e("useEffect.spotify.getCredentials", e, stack);
|
||||
});
|
||||
} else {
|
||||
pagingController.addPageRequestListener(listener);
|
||||
pagingController.notifyPageRequestListeners(0);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[Home.initState]: $e");
|
||||
print(stack);
|
||||
logger.e("initState", e, stack);
|
||||
}
|
||||
return () {
|
||||
pagingController.removePageRequestListener(listener);
|
||||
@ -218,7 +217,7 @@ class Home extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
// player itself
|
||||
const Player(),
|
||||
Player(),
|
||||
SpotubeNavigationBar(
|
||||
selectedIndex: _selectedIndex.value,
|
||||
onSelectedIndexChanged: _onSelectedIndexChanged,
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Artist/ArtistCard.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class UserArtists extends ConsumerStatefulWidget {
|
||||
@ -15,6 +16,7 @@ class UserArtists extends ConsumerStatefulWidget {
|
||||
class _UserArtistsState extends ConsumerState<UserArtists> {
|
||||
final PagingController<String, Artist> _pagingController =
|
||||
PagingController(firstPageKey: "");
|
||||
final logger = createLogger(UserArtists);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -36,8 +38,7 @@ class _UserArtistsState extends ConsumerState<UserArtists> {
|
||||
}
|
||||
} catch (e, stack) {
|
||||
_pagingController.error = e;
|
||||
print("[UserArtists.pagingController]: $e");
|
||||
print(stack);
|
||||
logger.e("pagingController", e, stack);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -8,11 +8,13 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/helpers/oauth-login.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
|
||||
class Login extends HookConsumerWidget {
|
||||
const Login({Key? key}) : super(key: key);
|
||||
Login({Key? key}) : super(key: key);
|
||||
final log = createLogger(Login);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -37,7 +39,7 @@ class Login extends HookConsumerWidget {
|
||||
(value) => GoRouter.of(context).pop(),
|
||||
);
|
||||
} catch (e) {
|
||||
print("[Login.handleLogin] $e");
|
||||
log.e("[Login.handleLogin] $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,11 +11,14 @@ import 'package:spotube/components/Player/PlayerControls.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Player extends HookConsumerWidget {
|
||||
const Player({Key? key}) : super(key: key);
|
||||
Player({Key? key}) : super(key: key);
|
||||
|
||||
final logger = createLogger(Player);
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
@ -37,7 +40,9 @@ class Player extends HookConsumerWidget {
|
||||
/// [disposeAllPlayers] method which is throwing
|
||||
/// [UnimplementedException] in the [PlatformInterface]
|
||||
/// implementation
|
||||
player.setAsset("assets/warmer.mp3");
|
||||
playback.audioSession
|
||||
?.setActive(true)
|
||||
.then((_) => player.setAsset("assets/warmer.mp3"));
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
@ -65,8 +70,7 @@ class Player extends HookConsumerWidget {
|
||||
entryRef.value = null;
|
||||
} catch (e, stack) {
|
||||
if (e is! AssertionError) {
|
||||
print("[Player.useEffect.cleanup] $e");
|
||||
print(stack);
|
||||
logger.e("useEffect.cleanup", e, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -107,7 +111,7 @@ class Player extends HookConsumerWidget {
|
||||
children: [
|
||||
Expanded(child: PlayerTrackDetails(albumArt: albumArt)),
|
||||
// controls
|
||||
const Expanded(
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: PlayerControls(),
|
||||
),
|
||||
@ -133,8 +137,7 @@ class Player extends HookConsumerWidget {
|
||||
);
|
||||
});
|
||||
} catch (e, stack) {
|
||||
print("[VolumeSlider.onChange()] $e");
|
||||
print(stack);
|
||||
logger.e("onChange", e, stack);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -4,15 +4,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||
import 'package:spotube/hooks/playback.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
|
||||
class PlayerControls extends HookConsumerWidget {
|
||||
final Color? iconColor;
|
||||
const PlayerControls({
|
||||
PlayerControls({
|
||||
this.iconColor,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final logger = createLogger(PlayerControls);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final Playback playback = ref.watch(playbackProvider);
|
||||
@ -115,8 +118,7 @@ class PlayerControls extends HookConsumerWidget {
|
||||
_shuffled.value = false;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onShuffle()] $e");
|
||||
print(stack);
|
||||
logger.e("onShuffle", e, stack);
|
||||
}
|
||||
}),
|
||||
IconButton(
|
||||
@ -150,8 +152,7 @@ class PlayerControls extends HookConsumerWidget {
|
||||
_shuffled.value = false;
|
||||
playback.reset();
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onStop()] $e");
|
||||
print(stack);
|
||||
logger.e("onStop", e, stack);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
|
@ -3,8 +3,11 @@ import 'package:html/parser.dart' as parser;
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:spotube/helpers/get-random-element.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/models/generated_secrets.dart';
|
||||
|
||||
final logger = createLogger("GetLyrics");
|
||||
|
||||
String getTitle(String title, String artist) {
|
||||
return "$title $artist"
|
||||
.toLowerCase()
|
||||
@ -40,8 +43,7 @@ Future<String?> extractLyrics(Uri url) async {
|
||||
|
||||
return lyrics;
|
||||
} catch (e, stack) {
|
||||
print("[extractLyrics] $e");
|
||||
print(stack);
|
||||
logger.e("extractLyrics", e, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@ -78,8 +80,7 @@ Future<List?> searchSong(
|
||||
}).toList();
|
||||
return results;
|
||||
} catch (e, stack) {
|
||||
print("[searchSong] $e");
|
||||
print(stack);
|
||||
logger.e("searchSong", e, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@ -103,8 +104,7 @@ Future<String?> getLyrics(
|
||||
String? lyrics = await extractLyrics(Uri.parse(results.first["url"]));
|
||||
return lyrics;
|
||||
} catch (e, stack) {
|
||||
print("[getLyrics] $e");
|
||||
print(stack);
|
||||
logger.e("getLyrics", e, stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,11 @@ import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Home/Home.dart';
|
||||
import 'package:spotube/helpers/server_ipc.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
|
||||
const redirectUri = "http://localhost:4304/auth/spotify/callback";
|
||||
final logger = createLogger("OAuthLogin");
|
||||
|
||||
Future<void> oauthLogin(Auth auth,
|
||||
{required String clientId, required String clientSecret}) async {
|
||||
@ -57,8 +59,7 @@ Future<void> oauthLogin(Auth auth,
|
||||
isLoggedIn: true,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
print("[oauthLogin()] $e");
|
||||
print(stack);
|
||||
logger.e("oauthLogin", e, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
final logger = createLogger("ServerIPC");
|
||||
|
||||
Future<String?> connectIpc(String authUri, String redirectUri) async {
|
||||
try {
|
||||
print("[Launching]: $authUri");
|
||||
logger.i("[Launching]: $authUri");
|
||||
await launch(authUri);
|
||||
|
||||
HttpServer server =
|
||||
await HttpServer.bind(InternetAddress.loopbackIPv4, 4304);
|
||||
print("Server started");
|
||||
logger.i("Server started");
|
||||
|
||||
await for (HttpRequest request in server) {
|
||||
if (request.uri.path == "/auth/spotify/callback" &&
|
||||
@ -30,9 +33,8 @@ Future<String?> connectIpc(String authUri, String redirectUri) async {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error, stack) {
|
||||
print("[connectIpc]: $error");
|
||||
print(stack);
|
||||
} catch (e, stack) {
|
||||
logger.e("connectIpc", e, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
|
||||
final logger = createLogger("PlaybackHook");
|
||||
|
||||
Future<void> Function() useNextTrack(Playback playback) {
|
||||
return () async {
|
||||
try {
|
||||
@ -7,8 +10,7 @@ Future<void> Function() useNextTrack(Playback playback) {
|
||||
await playback.player.seek(Duration.zero);
|
||||
playback.movePlaylistPositionBy(1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onNext()] $e");
|
||||
print(stack);
|
||||
logger.e("useNextTrack", e, stack);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -20,8 +22,7 @@ Future<void> Function() usePreviousTrack(Playback playback) {
|
||||
await playback.player.seek(Duration.zero);
|
||||
playback.movePlaylistPositionBy(-1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onPrevious()] $e");
|
||||
print(stack);
|
||||
logger.e("onPrevious", e, stack);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -34,8 +35,7 @@ Future<void> Function([dynamic]) useTogglePlayPause(Playback playback) {
|
||||
? await playback.player.pause()
|
||||
: await playback.player.play();
|
||||
} catch (e, stack) {
|
||||
print("[PlayPauseShortcut] $e");
|
||||
print(stack);
|
||||
logger.e("useTogglePlayPause", e, stack);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/models/GoRouteDeclarations.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/AudioPlayer.dart';
|
||||
import 'package:spotube/provider/ThemeProvider.dart';
|
||||
import 'package:spotube/provider/YouTube.dart';
|
||||
@ -38,6 +39,7 @@ void main() async {
|
||||
|
||||
class MyApp extends HookConsumerWidget {
|
||||
final GoRouter _router = createGoRouter();
|
||||
final logger = createLogger(MyApp);
|
||||
|
||||
MyApp({Key? key}) : super(key: key);
|
||||
@override
|
||||
|
@ -15,12 +15,12 @@ GoRouter createGoRouter() => GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
builder: (context, state) => const Home(),
|
||||
builder: (context, state) => Home(),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/login",
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: const Login(),
|
||||
child: Login(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
|
25
lib/models/Logger.dart
Normal file
25
lib/models/Logger.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
_SpotubeLogger createLogger<T>(T owner) =>
|
||||
_SpotubeLogger(owner is String ? owner : owner.toString());
|
||||
|
||||
class _SpotubeLogger extends Logger {
|
||||
String owner;
|
||||
_SpotubeLogger(this.owner);
|
||||
|
||||
@override
|
||||
void log(Level level, message, [error, StackTrace? stackTrace]) {
|
||||
getApplicationDocumentsDirectory().then((dir) async {
|
||||
final file = File(path.join(dir.path, ".spotube_logs"));
|
||||
if (level == Level.error) {
|
||||
await file.writeAsString("[${DateTime.now()}]\n$message\n$stackTrace",
|
||||
mode: FileMode.writeOnlyAppend);
|
||||
}
|
||||
});
|
||||
super.log(level, "[$owner] $message", error, stackTrace);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/helpers/artist-to-string.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/helpers/search-youtube.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/AudioPlayer.dart';
|
||||
import 'package:spotube/provider/YouTube.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
@ -47,6 +48,7 @@ class CurrentPlaylist {
|
||||
}
|
||||
|
||||
class Playback extends ChangeNotifier {
|
||||
final _logger = createLogger(Playback);
|
||||
CurrentPlaylist? _currentPlaylist;
|
||||
Track? _currentTrack;
|
||||
|
||||
@ -109,8 +111,7 @@ class Playback extends ChangeNotifier {
|
||||
movePlaylistPositionBy(1);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[PrecessingStateStreamListener] $e");
|
||||
print(stack);
|
||||
_logger.e("PrecessingStateStreamListener", e, stack);
|
||||
}
|
||||
});
|
||||
|
||||
@ -126,6 +127,7 @@ class Playback extends ChangeNotifier {
|
||||
CurrentPlaylist? get currentPlaylist => _currentPlaylist;
|
||||
Track? get currentTrack => _currentTrack;
|
||||
bool get isPlaying => _isPlaying;
|
||||
AudioSession? get audioSession => _audioSession;
|
||||
|
||||
/// this duration field is almost static & changes occasionally
|
||||
///
|
||||
@ -258,8 +260,7 @@ class Playback extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[Playback.startPlaying] $e");
|
||||
print(stack);
|
||||
_logger.e("startPlaying", e, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
|
||||
class UserPreferences extends ChangeNotifier {
|
||||
String geniusAccessToken;
|
||||
@ -20,6 +21,8 @@ class UserPreferences extends ChangeNotifier {
|
||||
onInit();
|
||||
}
|
||||
|
||||
final logger = createLogger(UserPreferences);
|
||||
|
||||
Future<HotKey?> _getHotKeyFromLocalStorage(
|
||||
SharedPreferences preferences, String key) async {
|
||||
String? str = preferences.getString(key);
|
||||
@ -68,8 +71,7 @@ class UserPreferences extends ChangeNotifier {
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e, stack) {
|
||||
print("[UserPreferences.onInit]: $e");
|
||||
print(stack);
|
||||
logger.e("onInit", e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -373,6 +373,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -55,6 +55,7 @@ dependencies:
|
||||
palette_generator: ^0.3.3
|
||||
audio_session: ^0.1.6+1
|
||||
just_audio_background: ^0.0.1-beta.5
|
||||
logger: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
Reference in New Issue
Block a user