mirror of
https://github.com/foss42/apidash.git
synced 2025-06-30 12:57:27 +08:00
wip: environment pane
This commit is contained in:
@ -1,81 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'environment_model.dart';
|
||||
|
||||
@immutable
|
||||
class EnvironmentListModel {
|
||||
const EnvironmentListModel({
|
||||
this.actveEnvironment,
|
||||
this.globalEnvironment = const EnvironmentModel(id: "global"),
|
||||
this.environments = const [],
|
||||
});
|
||||
|
||||
final EnvironmentModel? actveEnvironment;
|
||||
final EnvironmentModel globalEnvironment;
|
||||
final List<EnvironmentModel> environments;
|
||||
|
||||
EnvironmentListModel copyWith({
|
||||
EnvironmentModel? actveEnvironment,
|
||||
EnvironmentModel? globalEnvironment,
|
||||
List<EnvironmentModel>? environments,
|
||||
}) {
|
||||
return EnvironmentListModel(
|
||||
actveEnvironment: actveEnvironment ?? this.actveEnvironment,
|
||||
globalEnvironment: globalEnvironment ?? this.globalEnvironment,
|
||||
environments: environments ?? this.environments,
|
||||
);
|
||||
}
|
||||
|
||||
factory EnvironmentListModel.fromJson(Map<dynamic, dynamic> data) {
|
||||
final actveEnvironment = data["actveEnvironment"] != null
|
||||
? EnvironmentModel.fromJson(
|
||||
data["actveEnvironment"] as Map<String, dynamic>)
|
||||
: null;
|
||||
final globalEnvironment = EnvironmentModel.fromJson(
|
||||
data["globalEnvironment"] as Map<String, dynamic>);
|
||||
final List<dynamic> environments = data["environments"] as List<dynamic>;
|
||||
|
||||
const em = EnvironmentListModel();
|
||||
|
||||
return em.copyWith(
|
||||
actveEnvironment: actveEnvironment,
|
||||
globalEnvironment: globalEnvironment,
|
||||
environments: environments
|
||||
.map((dynamic e) =>
|
||||
EnvironmentModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"actveEnvironment": actveEnvironment?.toJson(),
|
||||
"globalEnvironment": globalEnvironment.toJson(),
|
||||
"environments": environments.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
EnvironmentModel getEnvironment(String id) {
|
||||
if (id == "global") {
|
||||
return globalEnvironment;
|
||||
}
|
||||
return environments.firstWhere((e) => e.id == id);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is EnvironmentListModel &&
|
||||
other.actveEnvironment == actveEnvironment &&
|
||||
other.globalEnvironment == globalEnvironment &&
|
||||
listEquals(other.environments, environments);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
actveEnvironment,
|
||||
globalEnvironment,
|
||||
environments,
|
||||
);
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ part 'environment_model.g.dart';
|
||||
class EnvironmentModel with _$EnvironmentModel {
|
||||
const factory EnvironmentModel({
|
||||
required String id,
|
||||
@Default("") String name,
|
||||
@Default("New Environment") String name,
|
||||
@Default([]) List<EnvironmentVariableModel> values,
|
||||
}) = _EnvironmentModel;
|
||||
|
||||
|
@ -122,7 +122,7 @@ class __$$EnvironmentModelImplCopyWithImpl<$Res>
|
||||
class _$EnvironmentModelImpl implements _EnvironmentModel {
|
||||
const _$EnvironmentModelImpl(
|
||||
{required this.id,
|
||||
this.name = "",
|
||||
this.name = "New Environment",
|
||||
final List<EnvironmentVariableModel> values = const []})
|
||||
: _values = values;
|
||||
|
||||
|
@ -10,7 +10,7 @@ _$EnvironmentModelImpl _$$EnvironmentModelImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$EnvironmentModelImpl(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String? ?? "",
|
||||
name: json['name'] as String? ?? "New Environment",
|
||||
values: (json['values'] as List<dynamic>?)
|
||||
?.map((e) =>
|
||||
EnvironmentVariableModel.fromJson(e as Map<String, dynamic>))
|
||||
|
@ -8,7 +8,7 @@ part of 'http_response_model.dart';
|
||||
|
||||
_$HttpResponseModelImpl _$$HttpResponseModelImplFromJson(Map json) =>
|
||||
_$HttpResponseModelImpl(
|
||||
statusCode: json['statusCode'] as int?,
|
||||
statusCode: (json['statusCode'] as num?)?.toInt(),
|
||||
headers: (json['headers'] as Map?)?.map(
|
||||
(k, e) => MapEntry(k as String, e as String),
|
||||
),
|
||||
@ -19,7 +19,7 @@ _$HttpResponseModelImpl _$$HttpResponseModelImplFromJson(Map json) =>
|
||||
formattedBody: json['formattedBody'] as String?,
|
||||
bodyBytes:
|
||||
const Uint8ListConverter().fromJson(json['bodyBytes'] as List<int>?),
|
||||
time: const DurationConverter().fromJson(json['time'] as int?),
|
||||
time: const DurationConverter().fromJson((json['time'] as num?)?.toInt()),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$HttpResponseModelImplToJson(
|
||||
|
@ -15,7 +15,7 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl(
|
||||
? null
|
||||
: HttpRequestModel.fromJson(
|
||||
Map<String, Object?>.from(json['httpRequestModel'] as Map)),
|
||||
responseStatus: json['responseStatus'] as int?,
|
||||
responseStatus: (json['responseStatus'] as num?)?.toInt(),
|
||||
message: json['message'] as String?,
|
||||
httpResponseModel: json['httpResponseModel'] == null
|
||||
? null
|
||||
|
@ -5,7 +5,8 @@ import 'package:apidash/utils/file_utils.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../services/services.dart' show hiveHandler, HiveHandler;
|
||||
|
||||
final selectedEnvironmentIdProvider = StateProvider<String?>((ref) => null);
|
||||
final selectedEnvironmentIdStateProvider =
|
||||
StateProvider<String?>((ref) => null);
|
||||
|
||||
final environmentsStateNotifierProvider = StateNotifierProvider<
|
||||
EnvironmentsStateNotifier, Map<String, EnvironmentModel>?>((ref) {
|
||||
@ -27,8 +28,8 @@ class EnvironmentsStateNotifier
|
||||
state!.keys.first,
|
||||
];
|
||||
}
|
||||
ref.read(selectedEnvironmentIdProvider.notifier).state =
|
||||
ref.read(environmentSequenceProvider)[0];
|
||||
ref.read(selectedEnvironmentIdStateProvider.notifier).state =
|
||||
kGlobalEnvironmentId;
|
||||
});
|
||||
}
|
||||
|
||||
@ -80,7 +81,7 @@ class EnvironmentsStateNotifier
|
||||
ref
|
||||
.read(environmentSequenceProvider.notifier)
|
||||
.update((state) => [id, ...state]);
|
||||
ref.read(selectedEnvironmentIdProvider.notifier).state =
|
||||
ref.read(selectedEnvironmentIdStateProvider.notifier).state =
|
||||
newEnvironmentModel.id;
|
||||
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||
}
|
||||
@ -123,7 +124,7 @@ class EnvironmentsStateNotifier
|
||||
ref
|
||||
.read(environmentSequenceProvider.notifier)
|
||||
.update((state) => [...environmentIds]);
|
||||
ref.read(selectedEnvironmentIdProvider.notifier).state = newId;
|
||||
ref.read(selectedEnvironmentIdStateProvider.notifier).state = newId;
|
||||
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||
}
|
||||
|
||||
@ -142,7 +143,7 @@ class EnvironmentsStateNotifier
|
||||
newId = kGlobalEnvironmentId;
|
||||
}
|
||||
|
||||
ref.read(selectedEnvironmentIdProvider.notifier).state = newId;
|
||||
ref.read(selectedEnvironmentIdStateProvider.notifier).state = newId;
|
||||
|
||||
state = {
|
||||
...state!,
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:apidash/providers/providers.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'envvar/environment_page.dart';
|
||||
import 'home_page/home_page.dart';
|
||||
import 'intro_page.dart';
|
||||
import 'settings_page.dart';
|
||||
@ -43,8 +44,8 @@ class Dashboard extends ConsumerWidget {
|
||||
onPressed: () {
|
||||
ref.read(navRailIndexStateProvider.notifier).state = 1;
|
||||
},
|
||||
icon: const Icon(Icons.auto_awesome_mosaic_outlined),
|
||||
selectedIcon: const Icon(Icons.auto_awesome_mosaic),
|
||||
icon: const Icon(Icons.computer_outlined),
|
||||
selectedIcon: const Icon(Icons.computer_rounded),
|
||||
),
|
||||
Text(
|
||||
'Variables',
|
||||
@ -94,7 +95,7 @@ class Dashboard extends ConsumerWidget {
|
||||
index: railIdx,
|
||||
children: const [
|
||||
HomePage(),
|
||||
SizedBox(),
|
||||
EnvironmentPage(),
|
||||
IntroPage(),
|
||||
SettingsPage(),
|
||||
],
|
||||
|
@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:apidash/widgets/widgets.dart';
|
||||
import 'environments_pane.dart';
|
||||
|
||||
class EnvironmentPage extends StatelessWidget {
|
||||
const EnvironmentPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DashboardSplitView(
|
||||
sidebarWidget: EnvironmentsPane(),
|
||||
mainWidget: SizedBox(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import 'package:apidash/consts.dart';
|
||||
import 'package:apidash/extensions/extensions.dart';
|
||||
import 'package:apidash/models/environment_model.dart';
|
||||
import 'package:apidash/providers/providers.dart';
|
||||
import 'package:apidash/widgets/cards.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@ -13,7 +14,34 @@ class EnvironmentsPane extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return const SizedBox();
|
||||
return Padding(
|
||||
padding: kIsMacOS ? kP24CollectionPane : kP8CollectionPane,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: kPe8,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(environmentsStateNotifierProvider.notifier)
|
||||
.addEnvironment();
|
||||
},
|
||||
child: const Text(
|
||||
kLabelPlusNew,
|
||||
style: kTextStyleButton,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Expanded(child: EnvironmentsList()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,8 +150,35 @@ class EnvironmentItem extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedId = ref.watch(selectedEnvironmentIdProvider);
|
||||
final selectedId = ref.watch(selectedEnvironmentIdStateProvider);
|
||||
final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider);
|
||||
final editRequestId = ref.watch(selectedIdEditStateProvider);
|
||||
final mobileDrawerKey = ref.watch(mobileDrawerKeyProvider);
|
||||
|
||||
return Text(environmentModel.name);
|
||||
return SidebarEnvironmentCard(
|
||||
id: id,
|
||||
isActive: id == activeEnvironmentId,
|
||||
isGlobal: id == kGlobalEnvironmentId,
|
||||
name: environmentModel.name,
|
||||
selectedId: selectedId,
|
||||
editRequestId: editRequestId,
|
||||
setActive: (value) {
|
||||
ref.read(activeEnvironmentIdStateProvider.notifier).state = id;
|
||||
},
|
||||
onTap: () {
|
||||
mobileDrawerKey.currentState?.close();
|
||||
ref.read(selectedEnvironmentIdStateProvider.notifier).state = id;
|
||||
},
|
||||
focusNode: ref.watch(nameTextFieldFocusNodeProvider),
|
||||
onChangedNameEditor: (value) {
|
||||
value = value.trim();
|
||||
ref
|
||||
.read(environmentsStateNotifierProvider.notifier)
|
||||
.updateEnvironment(editRequestId!, name: value);
|
||||
},
|
||||
onTapOutsideNameEditor: () {
|
||||
ref.read(selectedEnvironmentIdStateProvider.notifier).state = null;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:apidash/screens/envvar/environments_pane.dart';
|
||||
import 'package:apidash/widgets/splitviews.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -81,7 +82,7 @@ class PageBranch extends StatelessWidget {
|
||||
offset: !context.isCompactWindow
|
||||
? const IDOffset.only(left: 0.1)
|
||||
: const IDOffset.only(left: 0.7),
|
||||
leftDrawerContent: const SizedBox(),
|
||||
leftDrawerContent: const EnvironmentsPane(),
|
||||
mainContent: const SizedBox(),
|
||||
);
|
||||
case 2:
|
||||
|
@ -83,10 +83,11 @@ class NavRail extends ConsumerWidget {
|
||||
final railIdx = ref.watch(navRailIndexStateProvider);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
child: Ink(
|
||||
width: 70,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
@ -124,6 +125,7 @@ class NavRail extends ConsumerWidget {
|
||||
Icons.help,
|
||||
Icons.help_outline,
|
||||
'About',
|
||||
showLabel: false,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
customNavigationDestination(
|
||||
@ -134,6 +136,7 @@ class NavRail extends ConsumerWidget {
|
||||
Icons.settings,
|
||||
Icons.settings_outlined,
|
||||
'Settings',
|
||||
showLabel: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'package:apidash/extensions/extensions.dart';
|
||||
import 'package:apidash/providers/providers.dart';
|
||||
import 'package:apidash/widgets/window_caption.dart';
|
||||
import '../navbar.dart';
|
||||
|
||||
class PageBase extends ConsumerWidget {
|
||||
final String title;
|
||||
@ -13,10 +15,10 @@ class PageBase extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isDarkMode =
|
||||
ref.watch(settingsProvider.select((value) => value.isDark));
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 70) +
|
||||
final scaffold = Container(
|
||||
padding: (context.isCompactWindow
|
||||
? const EdgeInsets.only(bottom: 70)
|
||||
: EdgeInsets.zero) +
|
||||
(kIsWindows || kIsMacOS ? kPt28 : EdgeInsets.zero),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Scaffold(
|
||||
@ -34,7 +36,19 @@ class PageBase extends ConsumerWidget {
|
||||
child: scaffoldBody,
|
||||
),
|
||||
),
|
||||
);
|
||||
return Stack(
|
||||
children: [
|
||||
!context.isCompactWindow
|
||||
? Row(
|
||||
children: [
|
||||
const NavRail(),
|
||||
Expanded(
|
||||
child: scaffold,
|
||||
),
|
||||
],
|
||||
)
|
||||
: scaffold,
|
||||
if (kIsWindows)
|
||||
SizedBox(
|
||||
height: 29,
|
||||
|
@ -38,7 +38,6 @@ class HiveHandler {
|
||||
late final Box dataBox;
|
||||
late final Box settingsBox;
|
||||
late final Box environmentBox;
|
||||
late final Box environmentIdsBox;
|
||||
|
||||
HiveHandler() {
|
||||
dataBox = Hive.box(kDataBox);
|
||||
@ -59,9 +58,9 @@ class HiveHandler {
|
||||
|
||||
void delete(String key) => dataBox.delete(key);
|
||||
|
||||
dynamic getEnvironmentIds() => environmentIdsBox.get(kKeyEnvironmentBoxIds);
|
||||
dynamic getEnvironmentIds() => environmentBox.get(kKeyEnvironmentBoxIds);
|
||||
Future<void> setEnvironmentIds(List<String>? ids) =>
|
||||
environmentIdsBox.put(kKeyEnvironmentBoxIds, ids);
|
||||
environmentBox.put(kKeyEnvironmentBoxIds, ids);
|
||||
|
||||
dynamic getEnvironment(String id) => environmentBox.get(id);
|
||||
Future<void> setEnvironment(
|
||||
|
@ -158,10 +158,11 @@ class SidebarEnvironmentCard extends StatelessWidget {
|
||||
super.key,
|
||||
required this.id,
|
||||
this.isGlobal = false,
|
||||
this.isSelected = false,
|
||||
this.isActive = false,
|
||||
this.name,
|
||||
this.selectedId,
|
||||
this.editRequestId,
|
||||
this.setActive,
|
||||
this.onTap,
|
||||
this.onDoubleTap,
|
||||
this.onSecondaryTap,
|
||||
@ -173,10 +174,11 @@ class SidebarEnvironmentCard extends StatelessWidget {
|
||||
|
||||
final String id;
|
||||
final bool isGlobal;
|
||||
final bool isSelected;
|
||||
final bool isActive;
|
||||
final String? name;
|
||||
final String? selectedId;
|
||||
final String? editRequestId;
|
||||
final void Function(bool?)? setActive;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onDoubleTap;
|
||||
final void Function()? onSecondaryTap;
|
||||
@ -187,6 +189,108 @@ class SidebarEnvironmentCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox();
|
||||
final Color color = Theme.of(context).colorScheme.surface;
|
||||
final Color colorVariant =
|
||||
Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5);
|
||||
final Color surfaceTint = Theme.of(context).colorScheme.primary;
|
||||
bool isSelected = selectedId == id;
|
||||
bool inEditMode = editRequestId == id;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Tooltip(
|
||||
message: name,
|
||||
triggerMode: TooltipTriggerMode.manual,
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: Card(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: kBorderRadius8,
|
||||
),
|
||||
elevation: isSelected ? 1 : 0,
|
||||
surfaceTintColor: isSelected ? surfaceTint : null,
|
||||
color: isSelected
|
||||
? colorScheme.brightness == Brightness.dark
|
||||
? colorVariant
|
||||
: color
|
||||
: color,
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: kBorderRadius8,
|
||||
hoverColor: colorVariant,
|
||||
focusColor: colorVariant.withOpacity(0.5),
|
||||
onTap: inEditMode ? null : onTap,
|
||||
// onDoubleTap: inEditMode ? null : onDoubleTap,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 6,
|
||||
right: isSelected ? 6 : 10,
|
||||
top: 5,
|
||||
bottom: 5,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
isGlobal
|
||||
? const SizedBox.shrink()
|
||||
: Checkbox(
|
||||
value: isActive,
|
||||
onChanged: isActive ? null : setActive,
|
||||
shape: const CircleBorder(),
|
||||
checkColor: colorScheme.onPrimary,
|
||||
fillColor: MaterialStateProperty.resolveWith<Color?>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return colorScheme.primary;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
kHSpacer4,
|
||||
Expanded(
|
||||
child: inEditMode
|
||||
? TextFormField(
|
||||
key: ValueKey("$id-name"),
|
||||
initialValue: name,
|
||||
// controller: controller,
|
||||
focusNode: focusNode,
|
||||
//autofocus: true,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
onTapOutside: (_) {
|
||||
onTapOutsideNameEditor?.call();
|
||||
//FocusScope.of(context).unfocus();
|
||||
},
|
||||
onFieldSubmitted: (value) {
|
||||
onTapOutsideNameEditor?.call();
|
||||
},
|
||||
onChanged: onChangedNameEditor,
|
||||
decoration: const InputDecoration(
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
name ?? "h",
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: isSelected && !inEditMode,
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
child: RequestCardMenu(
|
||||
onSelected: onMenuSelected,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user