mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
#64 new: process page
This commit is contained in:
@@ -360,7 +360,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 357;
|
CURRENT_PROJECT_VERSION = 361;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -368,7 +368,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.357;
|
MARKETING_VERSION = 1.0.361;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -491,7 +491,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 357;
|
CURRENT_PROJECT_VERSION = 361;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -499,7 +499,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.357;
|
MARKETING_VERSION = 1.0.361;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
@@ -516,7 +516,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 357;
|
CURRENT_PROJECT_VERSION = 361;
|
||||||
DEVELOPMENT_TEAM = BA88US33G6;
|
DEVELOPMENT_TEAM = BA88US33G6;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.357;
|
MARKETING_VERSION = 1.0.361;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
|||||||
123
lib/data/model/server/proc.dart
Normal file
123
lib/data/model/server/proc.dart
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// Models for `ps -aux`
|
||||||
|
|
||||||
|
// Each line
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class Proc {
|
||||||
|
final String user;
|
||||||
|
final int pid;
|
||||||
|
final double cpu;
|
||||||
|
final double mem;
|
||||||
|
final String vsz;
|
||||||
|
final String rss;
|
||||||
|
final String tty;
|
||||||
|
final String stat;
|
||||||
|
final String start;
|
||||||
|
final String time;
|
||||||
|
final String command;
|
||||||
|
|
||||||
|
Proc({
|
||||||
|
required this.user,
|
||||||
|
required this.pid,
|
||||||
|
required this.cpu,
|
||||||
|
required this.mem,
|
||||||
|
required this.vsz,
|
||||||
|
required this.rss,
|
||||||
|
required this.tty,
|
||||||
|
required this.stat,
|
||||||
|
required this.start,
|
||||||
|
required this.time,
|
||||||
|
required this.command,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Proc.parse(String raw) {
|
||||||
|
final parts = raw.split(RegExp(r'\s+'));
|
||||||
|
return Proc(
|
||||||
|
user: parts[0],
|
||||||
|
pid: int.parse(parts[1]),
|
||||||
|
cpu: double.parse(parts[2]),
|
||||||
|
mem: double.parse(parts[3]),
|
||||||
|
vsz: parts[4],
|
||||||
|
rss: parts[5],
|
||||||
|
tty: parts[6],
|
||||||
|
stat: parts[7],
|
||||||
|
start: parts[8],
|
||||||
|
time: parts[9],
|
||||||
|
command: parts.sublist(10).join(' '),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map toJson() {
|
||||||
|
return {
|
||||||
|
'user': user,
|
||||||
|
'pid': pid,
|
||||||
|
'cpu': cpu,
|
||||||
|
'mem': mem,
|
||||||
|
'vsz': vsz,
|
||||||
|
'rss': rss,
|
||||||
|
'tty': tty,
|
||||||
|
'stat': stat,
|
||||||
|
'start': start,
|
||||||
|
'time': time,
|
||||||
|
'command': command,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return const JsonEncoder.withIndent(' ').convert(toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
String get binary {
|
||||||
|
final parts = command.split(' ');
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `ps -aux` result
|
||||||
|
class PsResult {
|
||||||
|
final List<Proc> procs;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
PsResult({
|
||||||
|
required this.procs,
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
|
||||||
|
final lines = raw.split('\n');
|
||||||
|
final procs = <Proc>[];
|
||||||
|
var err = '';
|
||||||
|
for (var i = 1; i < lines.length; i++) {
|
||||||
|
final line = lines[i];
|
||||||
|
if (line.isEmpty) continue;
|
||||||
|
try {
|
||||||
|
procs.add(Proc.parse(line));
|
||||||
|
} catch (e) {
|
||||||
|
err += '$line: $e\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (sort) {
|
||||||
|
case ProcSortMode.cpu:
|
||||||
|
procs.sort((a, b) => b.cpu.compareTo(a.cpu));
|
||||||
|
break;
|
||||||
|
case ProcSortMode.mem:
|
||||||
|
procs.sort((a, b) => b.mem.compareTo(a.mem));
|
||||||
|
break;
|
||||||
|
case ProcSortMode.pid:
|
||||||
|
procs.sort((a, b) => a.pid.compareTo(b.pid));
|
||||||
|
break;
|
||||||
|
case ProcSortMode.user:
|
||||||
|
procs.sort((a, b) => a.user.compareTo(b.user));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return PsResult(procs: procs, error: err.isEmpty ? null : err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProcSortMode {
|
||||||
|
cpu,
|
||||||
|
mem,
|
||||||
|
pid,
|
||||||
|
user;
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
class BuildData {
|
class BuildData {
|
||||||
static const String name = "ServerBox";
|
static const String name = "ServerBox";
|
||||||
static const int build = 357;
|
static const int build = 361;
|
||||||
static const String engine = "3.10.0";
|
static const String engine = "3.10.0";
|
||||||
static const String buildAt = "2023-06-08 22:45:50.770980";
|
static const String buildAt = "2023-06-21 18:31:01.595350";
|
||||||
static const int modifications = 3;
|
static const int modifications = 2;
|
||||||
}
|
}
|
||||||
|
|||||||
139
lib/view/page/process.dart
Normal file
139
lib/view/page/process.dart
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:toolbox/core/extension/stringx.dart';
|
||||||
|
import 'package:toolbox/core/extension/uint8list.dart';
|
||||||
|
import 'package:toolbox/core/utils/ui.dart';
|
||||||
|
import 'package:toolbox/data/model/server/proc.dart';
|
||||||
|
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||||
|
import 'package:toolbox/data/res/ui.dart';
|
||||||
|
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||||
|
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||||
|
|
||||||
|
import '../../data/provider/server.dart';
|
||||||
|
import '../../locator.dart';
|
||||||
|
|
||||||
|
class ProcessPage extends StatefulWidget {
|
||||||
|
final ServerPrivateInfo spi;
|
||||||
|
const ProcessPage({super.key, required this.spi});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ProcessPageState createState() => _ProcessPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProcessPageState extends State<ProcessPage> {
|
||||||
|
late S _s;
|
||||||
|
late Timer _timer;
|
||||||
|
|
||||||
|
PsResult _result = PsResult(procs: []);
|
||||||
|
int? _lastFocusId;
|
||||||
|
ProcSortMode _procSortMode = ProcSortMode.cpu;
|
||||||
|
|
||||||
|
final _serverProvider = locator<ServerProvider>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final client = _serverProvider.servers[widget.spi.id]?.client;
|
||||||
|
if (client == null) {
|
||||||
|
showSnackBar(context, Text(_s.noClient));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 3), (_) async {
|
||||||
|
if (mounted) {
|
||||||
|
final result = await client.run('ps -aux'.withLangExport).string;
|
||||||
|
if (result.isEmpty) {
|
||||||
|
showSnackBar(context, Text(_s.noResult));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_result = PsResult.parse(result, sort: _procSortMode);
|
||||||
|
setState(() {});
|
||||||
|
} else {
|
||||||
|
_timer.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_timer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_s = S.of(context)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final actions = <Widget>[
|
||||||
|
PopupMenuButton<ProcSortMode>(
|
||||||
|
onSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
_procSortMode = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.sort),
|
||||||
|
initialValue: _procSortMode,
|
||||||
|
itemBuilder: (_) => ProcSortMode.values
|
||||||
|
.map(
|
||||||
|
(e) => PopupMenuItem(value: e, child: Text(e.name)),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if (_result.error != null) {
|
||||||
|
actions.add(IconButton(
|
||||||
|
icon: const Icon(Icons.error),
|
||||||
|
onPressed: () => showRoundDialog(
|
||||||
|
context: context,
|
||||||
|
child: Text(_result.error!),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Widget child;
|
||||||
|
if (_result.procs.isEmpty) {
|
||||||
|
child = centerLoading;
|
||||||
|
} else {
|
||||||
|
child = ListView.builder(
|
||||||
|
itemCount: _result.procs.length,
|
||||||
|
itemBuilder: (ctx, idx) {
|
||||||
|
final proc = _result.procs[idx];
|
||||||
|
return _buildListItem(proc);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
centerTitle: true,
|
||||||
|
title: TwoLineText(up: widget.spi.name, down: _s.process),
|
||||||
|
actions: actions,
|
||||||
|
),
|
||||||
|
body: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListItem(Proc proc) {
|
||||||
|
return RoundRectCard(ListTile(
|
||||||
|
leading: SizedBox(
|
||||||
|
width: 57,
|
||||||
|
child: TwoLineText(up: proc.pid.toString(), down: 'pid'),
|
||||||
|
),
|
||||||
|
title: Text(proc.binary),
|
||||||
|
subtitle: Text(proc.command, style: grey),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TwoLineText(up: proc.cpu.toStringAsFixed(1), down: 'cpu'),
|
||||||
|
width13,
|
||||||
|
TwoLineText(up: proc.mem.toStringAsFixed(1), down: 'mem'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => _lastFocusId = proc.pid,
|
||||||
|
autofocus: _lastFocusId == proc.pid,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:toolbox/core/extension/navigator.dart';
|
import 'package:toolbox/core/extension/navigator.dart';
|
||||||
import 'package:toolbox/core/extension/order.dart';
|
import 'package:toolbox/core/extension/order.dart';
|
||||||
import 'package:toolbox/core/utils/misc.dart';
|
import 'package:toolbox/core/utils/misc.dart';
|
||||||
import 'package:toolbox/data/res/server_cmd.dart';
|
import 'package:toolbox/view/page/process.dart';
|
||||||
|
|
||||||
import '../../../core/route.dart';
|
import '../../../core/route.dart';
|
||||||
import '../../../core/utils/ui.dart';
|
import '../../../core/utils/ui.dart';
|
||||||
@@ -338,13 +338,14 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
AppRoute(DockerManagePage(spi), 'Docker manage').go(context);
|
AppRoute(DockerManagePage(spi), 'Docker manage').go(context);
|
||||||
break;
|
break;
|
||||||
case ServerTabMenuType.process:
|
case ServerTabMenuType.process:
|
||||||
AppRoute(
|
// AppRoute(
|
||||||
SSHPage(
|
// SSHPage(
|
||||||
spi: spi,
|
// spi: spi,
|
||||||
initCmd: 'sh $shellPath -${shellFuncProcess.flag}',
|
// initCmd: 'sh $shellPath -${shellFuncProcess.flag}',
|
||||||
),
|
// ),
|
||||||
'ssh page (process)',
|
// 'ssh page (process)',
|
||||||
).go(context);
|
// ).go(context);
|
||||||
|
AppRoute(ProcessPage(spi: spi), 'process page').go(context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,14 +10,18 @@ class TwoLineText extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
up,
|
up,
|
||||||
style: textSize15,
|
style: textSize15,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
down,
|
down,
|
||||||
style: textSize11,
|
style: textSize11,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -475,9 +475,9 @@
|
|||||||
baseConfigurationReference = C1C758C41C4E208965A68933 /* Pods-RunnerTests.debug.xcconfig */;
|
baseConfigurationReference = C1C758C41C4E208965A68933 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 357;
|
CURRENT_PROJECT_VERSION = 361;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0.357;
|
MARKETING_VERSION = 1.0.361;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -490,9 +490,9 @@
|
|||||||
baseConfigurationReference = 15AF97DF993E8968098D6EBE /* Pods-RunnerTests.release.xcconfig */;
|
baseConfigurationReference = 15AF97DF993E8968098D6EBE /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 357;
|
CURRENT_PROJECT_VERSION = 361;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0.357;
|
MARKETING_VERSION = 1.0.361;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -505,9 +505,9 @@
|
|||||||
baseConfigurationReference = 7CFA7DE7FABA75685DFB6948 /* Pods-RunnerTests.profile.xcconfig */;
|
baseConfigurationReference = 7CFA7DE7FABA75685DFB6948 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 357;
|
CURRENT_PROJECT_VERSION = 361;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0.357;
|
MARKETING_VERSION = 1.0.361;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|||||||
Reference in New Issue
Block a user