mirror of
https://github.com/nrop19/weiman_app.git
synced 2025-08-02 23:05:48 +08:00
Compare commits
No commits in common. "v1.1.2" and "master" have entirely different histories.
@ -1,4 +1,4 @@
|
|||||||
# 微漫 v1.1.2 [宣传页面](https://nrop19.github.io/weiman_app)
|
# 微漫 v1.1.4 [宣传页面](https://nrop19.github.io/weiman_app)
|
||||||
|
|
||||||
### 微漫脱敏后的开源代码
|
### 微漫脱敏后的开源代码
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
||||||
|
import 'package:weiman/activities/book/tapToSearch.dart';
|
||||||
import 'package:weiman/classes/chapter.dart';
|
import 'package:weiman/classes/chapter.dart';
|
||||||
import 'package:weiman/classes/networkImageSSL.dart';
|
import 'package:weiman/classes/networkImageSSL.dart';
|
||||||
import 'package:weiman/db/book.dart';
|
import 'package:weiman/db/book.dart';
|
||||||
@ -12,7 +13,6 @@ import 'package:weiman/utils.dart';
|
|||||||
import 'package:weiman/widgets/book.dart';
|
import 'package:weiman/widgets/book.dart';
|
||||||
import 'package:weiman/widgets/bookSettingDialog.dart';
|
import 'package:weiman/widgets/bookSettingDialog.dart';
|
||||||
import 'package:weiman/widgets/pullToRefreshHeader.dart';
|
import 'package:weiman/widgets/pullToRefreshHeader.dart';
|
||||||
import 'package:weiman/activities/book/tapToSearch.dart';
|
|
||||||
|
|
||||||
class ActivityBook extends StatefulWidget {
|
class ActivityBook extends StatefulWidget {
|
||||||
final Book book;
|
final Book book;
|
||||||
@ -134,8 +134,11 @@ class _ActivityBook extends State<ActivityBook> {
|
|||||||
final List<Chapter> chapters = _sort();
|
final List<Chapter> chapters = _sort();
|
||||||
final history = <Widget>[];
|
final history = <Widget>[];
|
||||||
if (widget.book.history != null && widget.book.chapters.length > 0) {
|
if (widget.book.history != null && widget.book.chapters.length > 0) {
|
||||||
final chapter = widget.book.chapters
|
final chapter = widget.book.chapters.firstWhere(
|
||||||
.firstWhere((chapter) => chapter.cid == widget.book.history.cid);
|
(chapter) => chapter.cid == widget.book.history.cid,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
if(chapter != null){
|
||||||
history.add(ListTile(title: Text('阅读历史')));
|
history.add(ListTile(title: Text('阅读历史')));
|
||||||
history.add(WidgetChapter(
|
history.add(WidgetChapter(
|
||||||
chapter: chapter,
|
chapter: chapter,
|
||||||
@ -153,6 +156,7 @@ class _ActivityBook extends State<ActivityBook> {
|
|||||||
} else {
|
} else {
|
||||||
history.add(ListTile(subtitle: Text('没有了')));
|
history.add(ListTile(subtitle: Text('没有了')));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
history.add(SizedBox(height: 20));
|
history.add(SizedBox(height: 20));
|
||||||
}
|
}
|
||||||
history.add(
|
history.add(
|
||||||
|
@ -104,7 +104,18 @@ class _State extends State<ChapterTab> {
|
|||||||
|
|
||||||
Widget imageBuilder(ctx, String image, int index) {
|
Widget imageBuilder(ctx, String image, int index) {
|
||||||
index += 1;
|
index += 1;
|
||||||
return ImageWidget(image: image, index: index, total: sourceList.length);
|
bool reDraw = false;
|
||||||
|
try {
|
||||||
|
int cid = int.parse(widget.chapter.cid);
|
||||||
|
reDraw = cid >= 220980;
|
||||||
|
// print('创建图片 cid $cid, reDraw $reDraw');
|
||||||
|
} catch (e) {}
|
||||||
|
return ImageWidget(
|
||||||
|
image: image,
|
||||||
|
index: index,
|
||||||
|
total: sourceList.length,
|
||||||
|
reSort: reDraw,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget indicatorBuilder(context, IndicatorStatus status) {
|
Widget indicatorBuilder(context, IndicatorStatus status) {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'dart:io';
|
import 'dart:ui' as ui;
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:extended_image/extended_image.dart';
|
import 'package:extended_image/extended_image.dart';
|
||||||
import 'package:flutter/material.dart' hide Image;
|
import 'package:flutter/material.dart' hide Image;
|
||||||
@ -14,12 +13,14 @@ class ImageWidget extends StatefulWidget {
|
|||||||
final int index;
|
final int index;
|
||||||
final int total;
|
final int total;
|
||||||
final String image;
|
final String image;
|
||||||
|
final bool reSort;
|
||||||
|
|
||||||
const ImageWidget({
|
const ImageWidget({
|
||||||
Key key,
|
Key key,
|
||||||
this.image,
|
this.image,
|
||||||
this.index,
|
this.index,
|
||||||
this.total,
|
this.total,
|
||||||
|
this.reSort = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -56,10 +57,13 @@ class _State extends State<ImageWidget> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
content: ExtendedImage(
|
content: ExtendedImage(
|
||||||
image: NetworkImageSSL(Http18Comic.instance, widget.image),
|
image: NetworkImageSSL(
|
||||||
|
Http18Comic.instance,
|
||||||
|
widget.image,
|
||||||
|
reSort: widget.reSort,
|
||||||
|
),
|
||||||
loadStateChanged: (ExtendedImageState state) {
|
loadStateChanged: (ExtendedImageState state) {
|
||||||
Widget widget;
|
Widget widget;
|
||||||
|
|
||||||
switch (state.extendedImageLoadState) {
|
switch (state.extendedImageLoadState) {
|
||||||
case LoadState.loading:
|
case LoadState.loading:
|
||||||
widget = SizedBox(
|
widget = SizedBox(
|
||||||
@ -94,17 +98,13 @@ class _State extends State<ImageWidget> {
|
|||||||
if (!viewerSwitch) return;
|
if (!viewerSwitch) return;
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
Platform.isAndroid
|
TransparentMaterialPageRoute(
|
||||||
? TransparentMaterialPageRoute(
|
|
||||||
builder: (_) => ActivityImageViewer(
|
builder: (_) => ActivityImageViewer(
|
||||||
url: this.widget.image,
|
url: this.widget.image,
|
||||||
heroTag: tag,
|
heroTag: tag,
|
||||||
))
|
reSort: widget.reSort,
|
||||||
: TransparentCupertinoPageRoute(
|
),
|
||||||
builder: (_) => ActivityImageViewer(
|
),
|
||||||
url: this.widget.image,
|
|
||||||
heroTag: tag,
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,33 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
|
|
||||||
class ImageIndexWidget extends StatefulWidget {
|
|
||||||
const ImageIndexWidget({key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
ImageIndexWidgetState createState() => ImageIndexWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImageIndexWidgetState extends State<ImageIndexWidget> {
|
|
||||||
int _current = 1;
|
|
||||||
int total;
|
|
||||||
|
|
||||||
void set(value, total) {
|
|
||||||
_current = value;
|
|
||||||
this.total = total;
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
String text;
|
|
||||||
if (total == null) {
|
|
||||||
text = '读取中';
|
|
||||||
} else {
|
|
||||||
text = '当前图片:$_current / $total';
|
|
||||||
}
|
|
||||||
return Text(text);
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,11 +6,13 @@ import 'package:weiman/crawler/http18Comic.dart';
|
|||||||
class ActivityImageViewer extends StatefulWidget {
|
class ActivityImageViewer extends StatefulWidget {
|
||||||
final String url;
|
final String url;
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
|
final bool reSort;
|
||||||
|
|
||||||
const ActivityImageViewer({
|
const ActivityImageViewer({
|
||||||
Key key,
|
Key key,
|
||||||
this.url,
|
this.url,
|
||||||
this.heroTag,
|
this.heroTag,
|
||||||
|
this.reSort = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -36,7 +38,11 @@ class _State extends State<ActivityImageViewer> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: ExtendedImage(
|
child: ExtendedImage(
|
||||||
image: NetworkImageSSL(Http18Comic.instance, widget.url),
|
image: NetworkImageSSL(
|
||||||
|
Http18Comic.instance,
|
||||||
|
widget.url,
|
||||||
|
reSort: widget.reSort,
|
||||||
|
),
|
||||||
enableSlideOutPage: true,
|
enableSlideOutPage: true,
|
||||||
mode: ExtendedImageMode.gesture,
|
mode: ExtendedImageMode.gesture,
|
||||||
onDoubleTap: (status) {
|
onDoubleTap: (status) {
|
||||||
|
@ -11,7 +11,7 @@ import 'package:weiman/provider/theme.dart';
|
|||||||
import 'package:weiman/activities/checkData.dart';
|
import 'package:weiman/activities/checkData.dart';
|
||||||
import 'package:weiman/activities/hot.dart';
|
import 'package:weiman/activities/hot.dart';
|
||||||
import 'package:weiman/activities/search/search.dart';
|
import 'package:weiman/activities/search/search.dart';
|
||||||
import 'package:weiman/activities/test.dart';
|
import 'package:weiman/activities/test2.dart';
|
||||||
import 'package:weiman/classes/book.dart';
|
import 'package:weiman/classes/book.dart';
|
||||||
import 'package:weiman/main.dart';
|
import 'package:weiman/main.dart';
|
||||||
import 'package:weiman/provider/favoriteData.dart';
|
import 'package:weiman/provider/favoriteData.dart';
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:weiman/widgets/animatedLogo.dart';
|
|
||||||
|
|
||||||
import 'package:weiman/classes/data.dart';
|
|
||||||
import 'package:weiman/crawler/http18Comic.dart';
|
|
||||||
import 'package:weiman/db/group.dart';
|
|
||||||
|
|
||||||
class ActivityTest extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('测试'),
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
AnimatedLogoWidget(width: 25,height: 30),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: read,
|
|
||||||
child: Text('读取'),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: clear,
|
|
||||||
child: Text('清空数据'),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: httpTest,
|
|
||||||
child: Text('Http请求参数测试'),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: bookKeys,
|
|
||||||
child: Text('Book keys'),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: dbClear,
|
|
||||||
child: Text('清空收藏'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void read() {
|
|
||||||
var books = Data.getFavorites();
|
|
||||||
print(jsonEncode(books));
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear() {
|
|
||||||
Data.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> httpTest() async {
|
|
||||||
final books = await Http18Comic.instance.searchBook('冲突');
|
|
||||||
print('搜索漫画 ${books[0].toJson()}');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> bookKeys() async {
|
|
||||||
final books = Group.bookBox.values.toList();
|
|
||||||
print('book keys ${Group.bookBox.keys}');
|
|
||||||
print('quick ${books.map((e) => e.quick).join(',')}');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dbClear() async {
|
|
||||||
await Group.groupBox.clear();
|
|
||||||
await Group.bookBox.clear();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:dio_http_cache/dio_http_cache.dart';
|
|
||||||
import 'package:extended_image/extended_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:weiman/crawler/http18Comic.dart';
|
|
||||||
import 'package:weiman/db/book.dart';
|
|
||||||
|
|
||||||
class ActivityTest extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
_ActivityTest createState() => _ActivityTest();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ActivityTest extends State<ActivityTest> {
|
|
||||||
LoadState state;
|
|
||||||
List<Book> books;
|
|
||||||
String html;
|
|
||||||
String error;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('测试'),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
children: [
|
|
||||||
RaisedButton(
|
|
||||||
child: Text('搜索《冲突》(有可能会使用缓存)'),
|
|
||||||
onPressed: state != LoadState.loading ? useCache : null,
|
|
||||||
),
|
|
||||||
RaisedButton(
|
|
||||||
child: Text('搜索《冲突》(不使用缓存)'),
|
|
||||||
onPressed: state != LoadState.loading ? noCache : null,
|
|
||||||
),
|
|
||||||
if (books != null) Text('成功搜索到 ${books.length} 本漫画'),
|
|
||||||
if (html != null) Text('网页内容:\n$html'),
|
|
||||||
if (error != null) Text('错误内容:\n$error'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
noCache() {
|
|
||||||
final dio = Dio(BaseOptions(baseUrl: 'http://18comic.vip'));
|
|
||||||
httpTest(dio);
|
|
||||||
}
|
|
||||||
|
|
||||||
useCache() {
|
|
||||||
httpTest(Http18Comic.instance.dio);
|
|
||||||
}
|
|
||||||
|
|
||||||
httpTest(Dio dio) async {
|
|
||||||
setState(() {
|
|
||||||
html = null;
|
|
||||||
error = null;
|
|
||||||
state = LoadState.loading;
|
|
||||||
books = null;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
final res = (await Future.wait([
|
|
||||||
dio.get(
|
|
||||||
'/search/photos',
|
|
||||||
queryParameters: {'page': 1, 'search_query': '冲突'},
|
|
||||||
options: buildCacheOptions(Duration(days: 3)),
|
|
||||||
),
|
|
||||||
Future.delayed(
|
|
||||||
Duration(seconds: 1),
|
|
||||||
),
|
|
||||||
]))[0];
|
|
||||||
// print('heades ${res.headers}');
|
|
||||||
html = res.data;
|
|
||||||
books = Http18Comic.parseBookList(res.data);
|
|
||||||
} catch (e) {
|
|
||||||
print('$e');
|
|
||||||
print('${e.toString()}');
|
|
||||||
this.error = e.toString();
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
state = LoadState.completed;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:weiman/crawler/http.dart';
|
import 'package:weiman/crawler/http.dart';
|
||||||
|
|
||||||
/// The dart:io implementation of [image_provider.NetworkImage].
|
/// The dart:io implementation of [image_provider.NetworkImage].
|
||||||
@ -19,6 +18,7 @@ class NetworkImageSSL extends ImageProvider<NetworkImage>
|
|||||||
this.scale = 1.0,
|
this.scale = 1.0,
|
||||||
this.headers,
|
this.headers,
|
||||||
this.timeout = 8,
|
this.timeout = 8,
|
||||||
|
this.reSort = false,
|
||||||
}) : assert(url != null),
|
}) : assert(url != null),
|
||||||
assert(scale != null);
|
assert(scale != null);
|
||||||
|
|
||||||
@ -34,6 +34,8 @@ class NetworkImageSSL extends ImageProvider<NetworkImage>
|
|||||||
@override
|
@override
|
||||||
final Map<String, String> headers;
|
final Map<String, String> headers;
|
||||||
|
|
||||||
|
final bool reSort;
|
||||||
|
|
||||||
static void init(ByteData data) {}
|
static void init(ByteData data) {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -69,7 +71,7 @@ class NetworkImageSSL extends ImageProvider<NetworkImage>
|
|||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
assert(key == this);
|
assert(key == this);
|
||||||
final Uint8List bytes = await http.getImage(url);
|
final Uint8List bytes = await http.getImage(url, reSort: reSort);
|
||||||
if (bytes.lengthInBytes == 0)
|
if (bytes.lengthInBytes == 0)
|
||||||
throw Exception('NetworkImage is an empty file: $url');
|
throw Exception('NetworkImage is an empty file: $url');
|
||||||
return decode(bytes);
|
return decode(bytes);
|
||||||
|
@ -9,7 +9,7 @@ import 'http18Comic.dart';
|
|||||||
|
|
||||||
final headers = {
|
final headers = {
|
||||||
'user-agent':
|
'user-agent':
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36',
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
|
||||||
'accept':
|
'accept':
|
||||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
||||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-HK;q=0.7',
|
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-HK;q=0.7',
|
||||||
@ -50,7 +50,7 @@ abstract class HttpBook {
|
|||||||
|
|
||||||
Future<ChapterContent> getChapterContent(Book book, Chapter chapter);
|
Future<ChapterContent> getChapterContent(Book book, Chapter chapter);
|
||||||
|
|
||||||
Future<List<int>> getImage(String url);
|
Future<List<int>> getImage(String url, {bool reSort = false});
|
||||||
|
|
||||||
Future<List<Book>> hotBooks([String type = '', int page]);
|
Future<List<Book>> hotBooks([String type = '', int page]);
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import 'package:weiman/provider/theme.dart';
|
|||||||
FirebaseAnalytics analytics;
|
FirebaseAnalytics analytics;
|
||||||
FirebaseAnalyticsObserver observer;
|
FirebaseAnalyticsObserver observer;
|
||||||
|
|
||||||
const bool isDevMode = false;
|
const bool isDevMode = !bool.fromEnvironment('dart.vm.product');
|
||||||
|
|
||||||
int version;
|
int version;
|
||||||
BoxDecoration border;
|
BoxDecoration border;
|
||||||
@ -37,6 +37,7 @@ String imageCacheDirPath;
|
|||||||
PackageInfo packageInfo;
|
PackageInfo packageInfo;
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
print("开发模式 $isDevMode");
|
||||||
FlutterError.onError = (FlutterErrorDetails details) {};
|
FlutterError.onError = (FlutterErrorDetails details) {};
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp();
|
await Firebase.initializeApp();
|
||||||
|
@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.1.2+2007
|
version: 1.1.4+2007
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.9.0 <3.0.0"
|
sdk: ">=2.9.0 <3.0.0"
|
||||||
@ -26,6 +26,7 @@ dependencies:
|
|||||||
|
|
||||||
dio: any
|
dio: any
|
||||||
dio_http_cache: any
|
dio_http_cache: any
|
||||||
|
image: any
|
||||||
intl: any
|
intl: any
|
||||||
async: any
|
async: any
|
||||||
http: any
|
http: any
|
||||||
|
Loading…
x
Reference in New Issue
Block a user