Merge branch 'design'
This commit is contained in:
@ -1,35 +1,249 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import '../models/common/video_episode_type.dart';
|
||||
import 'widgets/badge.dart';
|
||||
import 'widgets/stat/danmu.dart';
|
||||
import 'widgets/stat/view.dart';
|
||||
|
||||
class EpisodeBottomSheet {
|
||||
final List<dynamic> episodes;
|
||||
final int currentCid;
|
||||
final dynamic dataType;
|
||||
final BuildContext context;
|
||||
final Function changeFucCall;
|
||||
final int? cid;
|
||||
final double? sheetHeight;
|
||||
bool isFullScreen = false;
|
||||
final UgcSeason? ugcSeason;
|
||||
|
||||
EpisodeBottomSheet({
|
||||
required this.episodes,
|
||||
required this.currentCid,
|
||||
required this.dataType,
|
||||
required this.context,
|
||||
required this.changeFucCall,
|
||||
this.cid,
|
||||
this.sheetHeight,
|
||||
this.isFullScreen = false,
|
||||
this.ugcSeason,
|
||||
});
|
||||
|
||||
Widget buildEpisodeListItem(
|
||||
dynamic episode,
|
||||
int index,
|
||||
bool isCurrentIndex,
|
||||
) {
|
||||
Widget buildShowContent() {
|
||||
return PagesBottomSheet(
|
||||
episodes: episodes,
|
||||
currentCid: currentCid,
|
||||
dataType: dataType,
|
||||
changeFucCall: changeFucCall,
|
||||
cid: cid,
|
||||
sheetHeight: sheetHeight,
|
||||
isFullScreen: isFullScreen,
|
||||
ugcSeason: ugcSeason,
|
||||
);
|
||||
}
|
||||
|
||||
PersistentBottomSheetController show(BuildContext context) {
|
||||
final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return buildShowContent();
|
||||
},
|
||||
);
|
||||
return btmSheetCtr;
|
||||
}
|
||||
}
|
||||
|
||||
class PagesBottomSheet extends StatefulWidget {
|
||||
const PagesBottomSheet({
|
||||
super.key,
|
||||
required this.episodes,
|
||||
required this.currentCid,
|
||||
required this.dataType,
|
||||
required this.changeFucCall,
|
||||
this.cid,
|
||||
this.sheetHeight,
|
||||
this.isFullScreen = false,
|
||||
this.ugcSeason,
|
||||
});
|
||||
|
||||
final List<dynamic> episodes;
|
||||
final int currentCid;
|
||||
final dynamic dataType;
|
||||
final Function changeFucCall;
|
||||
final int? cid;
|
||||
final double? sheetHeight;
|
||||
final bool isFullScreen;
|
||||
final UgcSeason? ugcSeason;
|
||||
|
||||
@override
|
||||
State<PagesBottomSheet> createState() => _PagesBottomSheetState();
|
||||
}
|
||||
|
||||
class _PagesBottomSheetState extends State<PagesBottomSheet> {
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
late int currentIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentIndex =
|
||||
widget.episodes.indexWhere((dynamic e) => e.cid == widget.currentCid);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (widget.dataType == VideoEpidoesType.videoEpisode) {
|
||||
_itemScrollController.jumpTo(index: currentIndex);
|
||||
} else {
|
||||
double itemHeight = (widget.isFullScreen
|
||||
? 400
|
||||
: Get.size.width - 3 * StyleString.safeSpace) /
|
||||
5.2;
|
||||
double offset = ((currentIndex - 1) / 2).ceil() * itemHeight;
|
||||
_scrollController.jumpTo(offset);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String prefix() {
|
||||
switch (widget.dataType) {
|
||||
case VideoEpidoesType.videoEpisode:
|
||||
return '选集';
|
||||
case VideoEpidoesType.videoPart:
|
||||
return '分集';
|
||||
case VideoEpidoesType.bangumiEpisode:
|
||||
return '选集';
|
||||
}
|
||||
return '选集';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return SizedBox(
|
||||
height: widget.sheetHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
TitleBar(
|
||||
title: '${prefix()}(${widget.episodes.length})',
|
||||
isFullScreen: widget.isFullScreen,
|
||||
),
|
||||
if (widget.ugcSeason != null) ...[
|
||||
UgcSeasonBuild(ugcSeason: widget.ugcSeason!),
|
||||
],
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: widget.dataType == VideoEpidoesType.videoEpisode
|
||||
? ScrollablePositionedList.builder(
|
||||
itemScrollController: _itemScrollController,
|
||||
itemCount: widget.episodes.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
bool isLastItem = index == widget.episodes.length;
|
||||
bool isCurrentIndex = currentIndex == index;
|
||||
return isLastItem
|
||||
? SizedBox(
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
20,
|
||||
)
|
||||
: EpisodeListItem(
|
||||
episode: widget.episodes[index],
|
||||
index: index,
|
||||
isCurrentIndex: isCurrentIndex,
|
||||
dataType: widget.dataType,
|
||||
changeFucCall: widget.changeFucCall,
|
||||
isFullScreen: widget.isFullScreen,
|
||||
);
|
||||
},
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0), // 设置左右间距为12
|
||||
child: GridView.count(
|
||||
controller: _scrollController,
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
childAspectRatio: 2.6,
|
||||
children: List.generate(
|
||||
widget.episodes.length,
|
||||
(index) {
|
||||
bool isCurrentIndex = currentIndex == index;
|
||||
return EpisodeGridItem(
|
||||
episode: widget.episodes[index],
|
||||
index: index,
|
||||
isCurrentIndex: isCurrentIndex,
|
||||
dataType: widget.dataType,
|
||||
changeFucCall: widget.changeFucCall,
|
||||
isFullScreen: widget.isFullScreen,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TitleBar extends StatelessWidget {
|
||||
final String title;
|
||||
final bool isFullScreen;
|
||||
|
||||
const TitleBar({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.isFullScreen,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
toolbarHeight: 45,
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: !isFullScreen
|
||||
? [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EpisodeListItem extends StatelessWidget {
|
||||
final dynamic episode;
|
||||
final int index;
|
||||
final bool isCurrentIndex;
|
||||
final dynamic dataType;
|
||||
final Function changeFucCall;
|
||||
final bool isFullScreen;
|
||||
|
||||
const EpisodeListItem({
|
||||
Key? key,
|
||||
required this.episode,
|
||||
required this.index,
|
||||
required this.isCurrentIndex,
|
||||
required this.dataType,
|
||||
required this.changeFucCall,
|
||||
required this.isFullScreen,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color primary = Theme.of(context).colorScheme.primary;
|
||||
Color onSurface = Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
@ -45,128 +259,308 @@ class EpisodeBottomSheet {
|
||||
title = '第${episode.title}话 ${episode.longTitle!}';
|
||||
break;
|
||||
}
|
||||
|
||||
return isFullScreen || episode?.cover == null || episode?.cover == ''
|
||||
? ListTile(
|
||||
onTap: () {
|
||||
SmartDialog.showToast('切换至「$title」');
|
||||
changeFucCall.call(episode, index);
|
||||
},
|
||||
dense: false,
|
||||
leading: isCurrentIndex
|
||||
? Image.asset(
|
||||
'assets/images/live.gif',
|
||||
color: primary,
|
||||
height: 12,
|
||||
? _buildListTile(context, title, primary, onSurface)
|
||||
: _buildInkWell(context, title, primary, onSurface);
|
||||
}
|
||||
|
||||
Widget _buildListTile(
|
||||
BuildContext context, String title, Color primary, Color onSurface) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
if (isCurrentIndex) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showToast('切换至「$title」');
|
||||
changeFucCall.call(episode, index);
|
||||
},
|
||||
dense: false,
|
||||
leading: isCurrentIndex
|
||||
? Image.asset(
|
||||
'assets/images/live.gif',
|
||||
color: primary,
|
||||
height: 12,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isCurrentIndex ? primary : onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInkWell(
|
||||
BuildContext context, String title, Color primary, Color onSurface) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (isCurrentIndex) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showToast('切换至「$title」');
|
||||
changeFucCall.call(episode, index);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace, 6, StyleString.safeSpace, 6),
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints boxConstraints) {
|
||||
const double width = 160;
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 88),
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context,
|
||||
BoxConstraints boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: episode?.cover ?? '',
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if (episode.duration != 0)
|
||||
PBadge(
|
||||
text: Utils.timeFormat(episode.duration!),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
episode.title as String,
|
||||
textAlign: TextAlign.start,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isCurrentIndex ? primary : onSurface,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (dataType != VideoEpidoesType.videoPart) ...[
|
||||
if (episode?.pubdate != null ||
|
||||
episode.pubTime != null)
|
||||
Text(
|
||||
Utils.dateFormat(
|
||||
episode?.pubdate ?? episode.pubTime),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color:
|
||||
Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
if (episode.stat != null)
|
||||
Row(
|
||||
children: [
|
||||
StatView(view: episode.stat.view),
|
||||
const SizedBox(width: 8),
|
||||
StatDanMu(danmu: episode.stat.danmaku),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
title: Text(title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isCurrentIndex ? primary : onSurface,
|
||||
)))
|
||||
: InkWell(
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EpisodeGridItem extends StatelessWidget {
|
||||
final dynamic episode;
|
||||
final int index;
|
||||
final bool isCurrentIndex;
|
||||
final dynamic dataType;
|
||||
final Function changeFucCall;
|
||||
final bool isFullScreen;
|
||||
|
||||
const EpisodeGridItem({
|
||||
Key? key,
|
||||
required this.episode,
|
||||
required this.index,
|
||||
required this.isCurrentIndex,
|
||||
required this.dataType,
|
||||
required this.changeFucCall,
|
||||
required this.isFullScreen,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
TextStyle textStyle = TextStyle(
|
||||
color: isCurrentIndex ? colorScheme.primary : colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
);
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: StyleString.safeSpace),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrentIndex
|
||||
? colorScheme.primaryContainer.withOpacity(0.6)
|
||||
: colorScheme.secondaryContainer.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isCurrentIndex
|
||||
? colorScheme.primary.withOpacity(0.8)
|
||||
: Colors.transparent,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
SmartDialog.showToast('切换至「$title」');
|
||||
if (isCurrentIndex) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showToast('切换至「${episode.title}」');
|
||||
changeFucCall.call(episode, index);
|
||||
},
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8),
|
||||
child: Row(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 130, height: 75, src: episode?.cover ?? ''),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isCurrentIndex ? primary : onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
dataType == VideoEpidoesType.bangumiEpisode
|
||||
? '第${index + 1}话'
|
||||
: '第${index + 1}p',
|
||||
style: textStyle),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
episode.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTitle() {
|
||||
return AppBar(
|
||||
toolbarHeight: 45,
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
'合集(${episodes.length})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: !isFullScreen
|
||||
? [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildShowContent(BuildContext context) {
|
||||
final ItemScrollController itemScrollController = ItemScrollController();
|
||||
int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid);
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
itemScrollController.jumpTo(index: currentIndex);
|
||||
});
|
||||
return Container(
|
||||
height: sheetHeight,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
buildTitle(),
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: PageStorage(
|
||||
bucket: PageStorageBucket(),
|
||||
child: ScrollablePositionedList.builder(
|
||||
itemScrollController: itemScrollController,
|
||||
itemCount: episodes.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
bool isLastItem = index == episodes.length;
|
||||
bool isCurrentIndex = currentIndex == index;
|
||||
return isLastItem
|
||||
? SizedBox(
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom + 20,
|
||||
)
|
||||
: buildEpisodeListItem(
|
||||
episodes[index],
|
||||
index,
|
||||
isCurrentIndex,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The [BuildContext] of the widget that calls the bottom sheet.
|
||||
PersistentBottomSheetController show(BuildContext context) {
|
||||
final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return buildShowContent(context);
|
||||
},
|
||||
if (dataType == VideoEpidoesType.bangumiEpisode &&
|
||||
episode.badge != '' &&
|
||||
episode.badge != null)
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 18,
|
||||
child: Text(
|
||||
episode.badge,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFFFF6699)),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UgcSeasonBuild extends StatelessWidget {
|
||||
final UgcSeason ugcSeason;
|
||||
|
||||
const UgcSeasonBuild({
|
||||
Key? key,
|
||||
required this.ugcSeason,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'合集:${ugcSeason.title}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (ugcSeason.intro != null && ugcSeason.intro != '') ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(ugcSeason.intro ?? '',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline)),
|
||||
),
|
||||
// SizedBox(
|
||||
// height: 32,
|
||||
// child: FilledButton.tonal(
|
||||
// onPressed: () {},
|
||||
// style: ButtonStyle(
|
||||
// padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
// ),
|
||||
// child: const Text('订阅'),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'),
|
||||
const TextSpan(text: ' · '),
|
||||
TextSpan(text: '${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return btmSheetCtr;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user