Compare commits
1 Commits
v0.8.92-pr
...
v0.8.92-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdfb688e62 |
7
.run/main.dart.run.xml
Normal file
7
.run/main.dart.run.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
|
||||
<option name="additionalArgs" value="--dart-define-from-file env.json" />
|
||||
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
10
Makefile
10
Makefile
@@ -1,10 +0,0 @@
|
||||
android_arm64:
|
||||
dart ./setup.dart android --arch arm64
|
||||
macos_arm64:
|
||||
dart ./setup.dart macos --arch arm64
|
||||
android_app:
|
||||
dart ./setup.dart android
|
||||
android_arm64_core:
|
||||
dart ./setup.dart android --arch arm64 --out core
|
||||
macos_arm64_core:
|
||||
dart ./setup.dart macos --arch arm64 --out core
|
||||
@@ -181,20 +181,18 @@ object State {
|
||||
}
|
||||
}
|
||||
|
||||
fun handleStopService() {
|
||||
GlobalState.launch {
|
||||
runLock.withLock {
|
||||
if (runStateFlow.value != RunState.START) {
|
||||
return@launch
|
||||
}
|
||||
try {
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
runTime = Service.stopService()
|
||||
runStateFlow.tryEmit(RunState.STOP)
|
||||
} finally {
|
||||
if (runStateFlow.value == RunState.PENDING) {
|
||||
runStateFlow.tryEmit(RunState.START)
|
||||
}
|
||||
suspend fun handleStopService() {
|
||||
runLock.withLock {
|
||||
if (runStateFlow.value != RunState.START) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
runStateFlow.tryEmit(RunState.PENDING)
|
||||
runTime = Service.stopService()
|
||||
runStateFlow.tryEmit(RunState.STOP)
|
||||
} finally {
|
||||
if (runStateFlow.value == RunState.PENDING) {
|
||||
runStateFlow.tryEmit(RunState.START)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +86,10 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
}
|
||||
|
||||
private fun handleStop(result: MethodChannel.Result) {
|
||||
State.handleStopService()
|
||||
result.success(true)
|
||||
launch {
|
||||
State.handleStopService()
|
||||
result.success(true)
|
||||
}
|
||||
}
|
||||
|
||||
val semaphore = Semaphore(10)
|
||||
|
||||
Submodule core/Clash.Meta updated: 4d4492c38c...c27f82fbdf
40
core/hub.go
40
core/hub.go
@@ -99,27 +99,47 @@ func handleValidateConfig(path string) string {
|
||||
func handleGetProxies() ProxiesData {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
var all = []string{"GLOBAL"}
|
||||
all = append(all, config.ProxyList...)
|
||||
|
||||
nameList := config.GetProxyNameList()
|
||||
|
||||
proxies := make(map[string]constant.Proxy)
|
||||
|
||||
for name, proxy := range tunnel.Proxies() {
|
||||
proxies[name] = proxy
|
||||
}
|
||||
for _, p := range tunnel.Providers() {
|
||||
for _, proxy := range p.Proxies() {
|
||||
name := proxy.Name()
|
||||
proxies[name] = proxy
|
||||
proxies[proxy.Name()] = proxy
|
||||
}
|
||||
}
|
||||
|
||||
types := []constant.AdapterType{
|
||||
constant.Selector, constant.URLTest, constant.Fallback, constant.Relay, constant.LoadBalance,
|
||||
hasGlobal := false
|
||||
allNames := make([]string, 0, len(nameList)+1)
|
||||
|
||||
for _, name := range nameList {
|
||||
if name == "GLOBAL" {
|
||||
hasGlobal = true
|
||||
}
|
||||
|
||||
p, ok := proxies[name]
|
||||
if !ok || p == nil {
|
||||
continue
|
||||
}
|
||||
switch p.Type() {
|
||||
case constant.Selector, constant.URLTest, constant.Fallback, constant.Relay, constant.LoadBalance:
|
||||
allNames = append(allNames, name)
|
||||
default:
|
||||
}
|
||||
}
|
||||
nextAll := slices.DeleteFunc(all, func(name string) bool {
|
||||
return !slices.Contains(types, proxies[name].Type())
|
||||
})
|
||||
|
||||
if !hasGlobal {
|
||||
if p, ok := proxies["GLOBAL"]; ok && p != nil {
|
||||
allNames = append([]string{"GLOBAL"}, allNames...)
|
||||
}
|
||||
}
|
||||
|
||||
return ProxiesData{
|
||||
All: nextAll,
|
||||
All: allNames,
|
||||
Proxies: proxies,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,16 @@ class Migration {
|
||||
}) async {
|
||||
_oldVersion = await preferences.getVersion();
|
||||
if (_oldVersion == currentVersion) {
|
||||
return Config.realFromJson(configMap);
|
||||
try {
|
||||
return Config.realFromJson(configMap);
|
||||
} catch (_) {
|
||||
final isV0 = configMap?['proxiesStyle'] != null;
|
||||
if (isV0) {
|
||||
_oldVersion = 0;
|
||||
} else {
|
||||
throw 'Local data is damaged. A reset is required to fix this issue.';
|
||||
}
|
||||
}
|
||||
}
|
||||
MigrationData data = MigrationData(configMap: configMap);
|
||||
if (_oldVersion == 0 && configMap != null) {
|
||||
|
||||
@@ -576,14 +576,14 @@ Future<MigrationData> _restoreTask(RootIsolateToken token) async {
|
||||
final scripts = results[1].cast<Script>();
|
||||
final profilesMigration = profiles.map(
|
||||
(item) => VM2(
|
||||
_getProfilePath(restoreDirPath, item.fileName),
|
||||
_getProfilePath(homeDirPath, item.fileName),
|
||||
_getProfilePath(restoreDirPath, item.id.toString()),
|
||||
_getProfilePath(homeDirPath, item.id.toString()),
|
||||
),
|
||||
);
|
||||
final scriptsMigration = scripts.map(
|
||||
(item) => VM2(
|
||||
_getScriptPath(restoreDirPath, item.fileName),
|
||||
_getScriptPath(homeDirPath, item.fileName),
|
||||
_getScriptPath(restoreDirPath, item.id.toString()),
|
||||
_getScriptPath(homeDirPath, item.id.toString()),
|
||||
),
|
||||
);
|
||||
await _copyWithMapList([...profilesMigration, ...scriptsMigration]);
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -551,18 +550,20 @@ extension SetupControllerExt on AppController {
|
||||
|
||||
Future<void> updateStatus(bool isStart, {bool isInit = false}) async {
|
||||
if (isStart) {
|
||||
final res = await tryStartCore(true);
|
||||
if (res) {
|
||||
return;
|
||||
}
|
||||
_ref.read(runTimeProvider.notifier).update((state) {
|
||||
return state ?? 0;
|
||||
});
|
||||
if (!isInit) {
|
||||
final res = await tryStartCore(true);
|
||||
if (res) {
|
||||
return;
|
||||
}
|
||||
if (!_ref.read(initProvider)) {
|
||||
return;
|
||||
}
|
||||
await globalState.handleStart([updateRunTime, updateTraffic]);
|
||||
applyProfileDebounce(force: true, silence: true);
|
||||
} else {
|
||||
_ref.read(runTimeProvider.notifier).value = 0;
|
||||
await applyProfile(
|
||||
force: true,
|
||||
preloadInvoke: () async {
|
||||
@@ -610,6 +611,18 @@ extension SetupControllerExt on AppController {
|
||||
_ref.read(checkIpNumProvider.notifier).add();
|
||||
}
|
||||
|
||||
void tryCheckIp() {
|
||||
final isTimeout = _ref.read(
|
||||
networkDetectionProvider.select(
|
||||
(state) => state.ipInfo == null && state.isLoading == false,
|
||||
),
|
||||
);
|
||||
if (!isTimeout) {
|
||||
return;
|
||||
}
|
||||
_ref.read(checkIpNumProvider.notifier).add();
|
||||
}
|
||||
|
||||
void applyProfileDebounce({bool silence = false, bool force = false}) {
|
||||
debouncer.call(FunctionTag.applyProfile, (silence, force) {
|
||||
applyProfile(silence: silence, force: force);
|
||||
@@ -795,9 +808,6 @@ extension CoreControllerExt on AppController {
|
||||
}
|
||||
|
||||
Future<Result<bool>> _requestAdmin(bool enableTun) async {
|
||||
if (system.isWindows && kDebugMode) {
|
||||
return Result.success(false);
|
||||
}
|
||||
final realTunEnable = _ref.read(realTunEnableProvider);
|
||||
if (enableTun != realTunEnable && realTunEnable == false) {
|
||||
final code = await system.authorizeCore();
|
||||
@@ -817,8 +827,8 @@ extension CoreControllerExt on AppController {
|
||||
}
|
||||
|
||||
Future<void> restartCore([bool start = false]) async {
|
||||
globalState.isUserDisconnected = true;
|
||||
await coreController.shutdown();
|
||||
_ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
||||
await coreController.shutdown(true);
|
||||
await _connectCore();
|
||||
await _initCore();
|
||||
if (start || _ref.read(isStartProvider)) {
|
||||
@@ -1178,8 +1188,8 @@ extension CommonControllerExt on AppController {
|
||||
}
|
||||
final res = await futureFunction();
|
||||
return res;
|
||||
} catch (e) {
|
||||
commonPrint.log('$title===> $e', logLevel: LogLevel.warning);
|
||||
} catch (e, s) {
|
||||
commonPrint.log('$title ===> $e, $s', logLevel: LogLevel.warning);
|
||||
if (silence) {
|
||||
globalState.showNotifier(e.toString());
|
||||
} else {
|
||||
|
||||
@@ -65,8 +65,8 @@ class CoreController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> shutdown() async {
|
||||
await _interface.shutdown();
|
||||
Future<void> shutdown(bool isUser) async {
|
||||
await _interface.shutdown(isUser);
|
||||
}
|
||||
|
||||
FutureOr<bool> get isInit => _interface.isInit;
|
||||
@@ -201,9 +201,10 @@ class CoreController {
|
||||
final profilePath = await appPath.getProfilePath(id.toString());
|
||||
final res = await _interface.getConfig(profilePath);
|
||||
if (res.isSuccess) {
|
||||
res.data['rules'] = res.data['rule'];
|
||||
res.data.remove('rule');
|
||||
return res.data;
|
||||
final data = Map<String, dynamic>.from(res.data);
|
||||
data['rules'] = data['rule'];
|
||||
data.remove('rule');
|
||||
return data;
|
||||
} else {
|
||||
throw res.message;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ mixin CoreInterface {
|
||||
|
||||
Future<String> preload();
|
||||
|
||||
Future<bool> shutdown();
|
||||
Future<bool> shutdown(bool isUser);
|
||||
|
||||
Future<bool> get isInit;
|
||||
|
||||
@@ -86,7 +86,9 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
try {
|
||||
await completer.future.timeout(const Duration(seconds: 10));
|
||||
if (!completer.isCompleted) {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
commonPrint.log(
|
||||
'Invoke pre ${method.name} timeout $e',
|
||||
@@ -98,9 +100,9 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
commonPrint.log('Invoke ${method.name} ${DateTime.now()} $data');
|
||||
}
|
||||
|
||||
return utils.handleWatch(
|
||||
return await utils.handleWatch(
|
||||
function: () async {
|
||||
return await invoke(method: method, data: data, timeout: timeout);
|
||||
return await invoke<T>(method: method, data: data, timeout: timeout);
|
||||
},
|
||||
onWatch: (data, elapsedMilliseconds) {
|
||||
commonPrint.log('Invoke ${method.name} ${elapsedMilliseconds}ms');
|
||||
@@ -131,7 +133,7 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> shutdown();
|
||||
Future<bool> shutdown(bool isUser);
|
||||
|
||||
@override
|
||||
Future<bool> get isInit async {
|
||||
@@ -163,8 +165,8 @@ abstract class CoreHandlerInterface with CoreInterface {
|
||||
|
||||
@override
|
||||
Future<Result> getConfig(String path) async {
|
||||
return await _invoke<Result>(method: ActionMethod.getConfig, data: path) ??
|
||||
Result.success({});
|
||||
final res = await _invoke(method: ActionMethod.getConfig, data: path);
|
||||
return res ?? Result.success({});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -37,10 +37,12 @@ class CoreLib extends CoreHandlerInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> shutdown() async {
|
||||
await service?.shutdown();
|
||||
Future<bool> shutdown(_) async {
|
||||
if (!_connectedCompleter.isCompleted) {
|
||||
return false;
|
||||
}
|
||||
_connectedCompleter = Completer();
|
||||
return true;
|
||||
return service?.shutdown() ?? true;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -16,6 +16,8 @@ class CoreService extends CoreHandlerInterface {
|
||||
|
||||
Completer<Socket> _socketCompleter = Completer();
|
||||
|
||||
Completer<bool> _shutdownCompleter = Completer();
|
||||
|
||||
final Map<String, Completer> _callbackCompleterMap = {};
|
||||
|
||||
Process? _process;
|
||||
@@ -35,6 +37,9 @@ class CoreService extends CoreHandlerInterface {
|
||||
if (result.id?.isEmpty == true) {
|
||||
coreEventManager.sendEvent(CoreEvent.fromJson(result.data));
|
||||
}
|
||||
if (completer?.isCompleted == true) {
|
||||
return;
|
||||
}
|
||||
completer?.complete(data);
|
||||
}
|
||||
|
||||
@@ -76,6 +81,9 @@ class CoreService extends CoreHandlerInterface {
|
||||
})
|
||||
.onDone(() {
|
||||
_handleInvokeCrashEvent();
|
||||
if (!_shutdownCompleter.isCompleted) {
|
||||
_shutdownCompleter.complete(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,7 +95,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
|
||||
Future<void> start() async {
|
||||
if (_process != null) {
|
||||
await shutdown();
|
||||
await shutdown(false);
|
||||
}
|
||||
final serverSocket = await _serverCompleter.future;
|
||||
final arg = system.isWindows
|
||||
@@ -113,7 +121,7 @@ class CoreService extends CoreHandlerInterface {
|
||||
@override
|
||||
destroy() async {
|
||||
final server = await _serverCompleter.future;
|
||||
await shutdown();
|
||||
await shutdown(false);
|
||||
await server.close();
|
||||
await _deleteSocketFile();
|
||||
return true;
|
||||
@@ -135,12 +143,16 @@ class CoreService extends CoreHandlerInterface {
|
||||
if (_socketCompleter.isCompleted) {
|
||||
final socket = await _socketCompleter.future;
|
||||
_socketCompleter = Completer();
|
||||
socket.close();
|
||||
await socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
shutdown(bool isUser) async {
|
||||
if (!_socketCompleter.isCompleted && _process == null) {
|
||||
return false;
|
||||
}
|
||||
_shutdownCompleter = Completer();
|
||||
await _destroySocket();
|
||||
_clearCompleter();
|
||||
if (system.isWindows) {
|
||||
@@ -148,7 +160,11 @@ class CoreService extends CoreHandlerInterface {
|
||||
}
|
||||
_process?.kill();
|
||||
_process = null;
|
||||
return true;
|
||||
if (isUser) {
|
||||
return _shutdownCompleter.future;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void _clearCompleter() {
|
||||
|
||||
@@ -68,8 +68,8 @@ class _AppStateManagerState extends ConsumerState<AppStateManager>
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
render?.resume();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
appController.tryCheckIp();
|
||||
if (system.isAndroid) {
|
||||
appController.addCheckIp();
|
||||
appController.tryStartCore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -98,16 +98,14 @@ class _CoreContainerState extends ConsumerState<CoreManager>
|
||||
|
||||
@override
|
||||
Future<void> onCrash(String message) async {
|
||||
if (!globalState.isUserDisconnected &&
|
||||
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
|
||||
context.showNotifier(message);
|
||||
}
|
||||
globalState.isUserDisconnected = false;
|
||||
if (ref.read(coreStatusProvider) != CoreStatus.connected) {
|
||||
return;
|
||||
}
|
||||
ref.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
||||
await coreController.shutdown();
|
||||
if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
|
||||
context.showNotifier(message);
|
||||
}
|
||||
await coreController.shutdown(false);
|
||||
super.onCrash(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class InitErrorScreen extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('System Init Failed'),
|
||||
title: const Text('Init Failed'),
|
||||
backgroundColor: colorScheme.error,
|
||||
foregroundColor: colorScheme.onError,
|
||||
elevation: 0,
|
||||
@@ -24,7 +24,6 @@ class InitErrorScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Header Section
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
|
||||
@@ -1822,7 +1822,7 @@ final class NetworkDetectionProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$networkDetectionHash() => r'29770de6a7ea2ac04b6584145d5200a7d43e45b0';
|
||||
String _$networkDetectionHash() => r'501babec2bbf2a38e4fef96cf20c76e9352bc5ee';
|
||||
|
||||
abstract class _$NetworkDetection extends $Notifier<NetworkDetectionState> {
|
||||
NetworkDetectionState build();
|
||||
|
||||
@@ -41,7 +41,6 @@ class GlobalState {
|
||||
CorePalette? corePalette;
|
||||
DateTime? startTime;
|
||||
UpdateTasks tasks = [];
|
||||
bool isUserDisconnected = false;
|
||||
SetupState? lastSetupState;
|
||||
VpnState? lastVpnState;
|
||||
|
||||
@@ -88,9 +87,7 @@ class GlobalState {
|
||||
configMap,
|
||||
sync: (data) async {
|
||||
final newConfigMap = data.configMap;
|
||||
final config = newConfigMap != null
|
||||
? Config.fromJson(newConfigMap)
|
||||
: Config(themeProps: defaultThemeProps);
|
||||
final config = Config.realFromJson(newConfigMap);
|
||||
await Future.wait([
|
||||
database.restore(data.profiles, data.scripts, data.rules, data.links),
|
||||
preferences.saveConfig(config),
|
||||
|
||||
@@ -30,7 +30,7 @@ class _EditProfileViewState extends State<EditProfileView> {
|
||||
late final TextEditingController _labelController;
|
||||
late final TextEditingController _urlController;
|
||||
late final TextEditingController _autoUpdateDurationController;
|
||||
late final bool _autoUpdate;
|
||||
late bool _autoUpdate;
|
||||
String? _rawText;
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final _fileInfoNotifier = ValueNotifier<FileInfo?>(null);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: fl_clash
|
||||
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||
publish_to: 'none'
|
||||
version: 0.8.92+2026012301
|
||||
version: 0.8.92+2026012501
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
|
||||
25
setup.dart
25
setup.dart
@@ -369,6 +369,15 @@ class BuildCommand extends Command {
|
||||
.map((e) => e.arch!)
|
||||
.toList();
|
||||
|
||||
Future<void> _buildEnvFile(String env, {String? coreSha256}) async {
|
||||
final data = {
|
||||
'APP_ENV': env,
|
||||
if (coreSha256 != null) 'CORE_SHA256': coreSha256,
|
||||
};
|
||||
final envFile = File(join(current, 'env.json'))..create();
|
||||
await envFile.writeAsString(json.encode(data));
|
||||
}
|
||||
|
||||
Future<void> _getLinuxDependencies(Arch arch) async {
|
||||
await Build.exec(Build.getExecutable('sudo apt update -y'));
|
||||
await Build.exec(
|
||||
@@ -412,7 +421,7 @@ class BuildCommand extends Command {
|
||||
await Build.exec(
|
||||
name: name,
|
||||
Build.getExecutable(
|
||||
'flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose$args --build-dart-define=APP_ENV=$env',
|
||||
'flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose,dart-define-from-file=env.json$args',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -448,21 +457,23 @@ class BuildCommand extends Command {
|
||||
mode: mode,
|
||||
);
|
||||
|
||||
String? coreSha256;
|
||||
|
||||
if (Platform.isWindows) {
|
||||
coreSha256 = await Build.calcSha256(corePaths.first);
|
||||
await Build.buildHelper(target, coreSha256);
|
||||
}
|
||||
await _buildEnvFile(env, coreSha256: coreSha256);
|
||||
if (out != 'app') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
case Target.windows:
|
||||
final token = target != Target.android
|
||||
? await Build.calcSha256(corePaths.first)
|
||||
: null;
|
||||
Build.buildHelper(target, token!);
|
||||
_buildDistributor(
|
||||
target: target,
|
||||
targets: 'exe,zip',
|
||||
args:
|
||||
' --description $archName --build-dart-define=CORE_SHA256=$token',
|
||||
args: ' --description $archName',
|
||||
env: env,
|
||||
);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user