mod: merge main
This commit is contained in:
18
lib/pages/fav/controller.dart
Normal file
18
lib/pages/fav/controller.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class FavController extends GetxController {
|
||||
Rx<FavFolderData> favFolderData = FavFolderData().obs;
|
||||
|
||||
Future<dynamic> queryFavFolder() async {
|
||||
var res = await await UserHttp.userfavFolder(
|
||||
pn: 1,
|
||||
ps: 10,
|
||||
mid: GStrorage.user.get(UserBoxKey.userMid),
|
||||
);
|
||||
favFolderData.value = res['data'];
|
||||
return res;
|
||||
}
|
||||
}
|
||||
4
lib/pages/fav/index.dart
Normal file
4
lib/pages/fav/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library fav;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
75
lib/pages/fav/view.dart
Normal file
75
lib/pages/fav/view.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/pages/fav/index.dart';
|
||||
|
||||
class FavPage extends StatefulWidget {
|
||||
const FavPage({super.key});
|
||||
|
||||
@override
|
||||
State<FavPage> createState() => _FavPageState();
|
||||
}
|
||||
|
||||
class _FavPageState extends State<FavPage> {
|
||||
final FavController _favController = Get.put(FavController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
title: const Text('我的收藏'),
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: _favController.queryFavFolder(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => ListView.builder(
|
||||
itemCount: _favController.favFolderData.value.list!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
onTap: () => Get.toNamed(
|
||||
'/favDetail',
|
||||
arguments:
|
||||
_favController.favFolderData.value.list![index],
|
||||
parameters: {
|
||||
'mediaId': _favController
|
||||
.favFolderData.value.list![index].id
|
||||
.toString(),
|
||||
},
|
||||
),
|
||||
leading: const Icon(Icons.folder_special_outlined),
|
||||
minLeadingWidth: 0,
|
||||
title: Text(_favController
|
||||
.favFolderData.value.list![index].title!),
|
||||
subtitle: Text(
|
||||
'${_favController.favFolderData.value.list![index].mediaCount}个内容',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.fontSize),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return Text('请求中');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
50
lib/pages/favDetail/controller.dart
Normal file
50
lib/pages/favDetail/controller.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/user/fav_detail.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
|
||||
class FavDetailController extends GetxController {
|
||||
FavFolderItemData? item;
|
||||
Rx<FavDetailData> favDetailData = FavDetailData().obs;
|
||||
int? mediaId;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
item = Get.arguments;
|
||||
if (Get.parameters.keys.isNotEmpty) {
|
||||
mediaId = int.parse(Get.parameters['mediaId']!);
|
||||
}
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future<dynamic> queryUserFavFolderDetail() async {
|
||||
var res = await await UserHttp.userFavFolderDetail(
|
||||
pn: 1,
|
||||
ps: 15,
|
||||
mediaId: mediaId!,
|
||||
);
|
||||
favDetailData.value = res['data'];
|
||||
return res;
|
||||
}
|
||||
|
||||
onCancelFav(int id) async {
|
||||
var result = await VideoHttp.favVideo(
|
||||
aid: id.toString(), addIds: '', delIds: mediaId.toString());
|
||||
if (result['status']) {
|
||||
if (result['data']['prompt']) {
|
||||
List<FavDetailItemData> dataList = favDetailData.value.medias!;
|
||||
for (var i in dataList) {
|
||||
if (i.id == id) {
|
||||
dataList.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
favDetailData.value.medias = dataList;
|
||||
favDetailData.refresh();
|
||||
SmartDialog.showToast('取消收藏');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
lib/pages/favDetail/index.dart
Normal file
4
lib/pages/favDetail/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library favdetail;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
219
lib/pages/favDetail/view.dart
Normal file
219
lib/pages/favDetail/view.dart
Normal file
@ -0,0 +1,219 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/pages/favDetail/index.dart';
|
||||
|
||||
import 'widget/fav_video_card.dart';
|
||||
|
||||
class FavDetailPage extends StatefulWidget {
|
||||
const FavDetailPage({super.key});
|
||||
|
||||
@override
|
||||
State<FavDetailPage> createState() => _FavDetailPageState();
|
||||
}
|
||||
|
||||
class _FavDetailPageState extends State<FavDetailPage> {
|
||||
late final ScrollController _controller = ScrollController();
|
||||
final FavDetailController _favDetailController =
|
||||
Get.put(FavDetailController());
|
||||
late StreamController<bool> titleStreamC; // a
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
titleStreamC = StreamController<bool>();
|
||||
_controller.addListener(
|
||||
() {
|
||||
if (_controller.offset > 160) {
|
||||
titleStreamC.add(true);
|
||||
} else if (_controller.offset <= 160) {
|
||||
titleStreamC.add(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
controller: _controller,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 260 - MediaQuery.of(context).padding.top,
|
||||
pinned: true,
|
||||
title: StreamBuilder(
|
||||
stream: titleStreamC.stream,
|
||||
initialData: false,
|
||||
builder: (context, AsyncSnapshot snapshot) {
|
||||
return AnimatedOpacity(
|
||||
opacity: snapshot.data ? 1 : 0,
|
||||
curve: Curves.easeOut,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_favDetailController.item!.title!,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
'共${_favDetailController.item!.mediaCount!}条视频',
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// actions: [
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(Icons.more_vert),
|
||||
// ),
|
||||
// const SizedBox(width: 4)
|
||||
// ],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
top: kTextTabBarHeight +
|
||||
MediaQuery.of(context).padding.top +
|
||||
30,
|
||||
left: 20,
|
||||
right: 20),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 180,
|
||||
height: 110,
|
||||
child: NetworkImgLayer(
|
||||
width: 180,
|
||||
height: 110,
|
||||
src: _favDetailController.item!.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_favDetailController.item!.title!,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.fontSize,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_favDetailController.item!.upper!.name!,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
|
||||
child: Obx(
|
||||
() => Text(
|
||||
'共${_favDetailController.favDetailData.value.medias != null ? _favDetailController.favDetailData.value.medias!.length : '-'}条视频',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
letterSpacing: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _favDetailController.queryUserFavFolderDetail(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data;
|
||||
if (data['status']) {
|
||||
if (_favDetailController.item!.mediaCount == 0) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: Center(child: Text('没有内容')),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Obx(
|
||||
() => SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return FavVideoCardH(
|
||||
videoItem: _favDetailController
|
||||
.favDetailData.value.medias![index],
|
||||
);
|
||||
},
|
||||
childCount: _favDetailController
|
||||
.favDetailData.value.medias!.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: Center(child: Text('加载中')),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).padding.bottom + 20,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
165
lib/pages/favDetail/widget/fav_video_card.dart
Normal file
165
lib/pages/favDetail/widget/fav_video_card.dart
Normal file
@ -0,0 +1,165 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
|
||||
import '../controller.dart';
|
||||
|
||||
// 收藏视频卡片 - 水平布局
|
||||
class FavVideoCardH extends StatelessWidget {
|
||||
var videoItem;
|
||||
final FavDetailController _favDetailController =
|
||||
Get.put(FavDetailController());
|
||||
|
||||
FavVideoCardH({Key? key, required this.videoItem}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int id = videoItem.id;
|
||||
String heroTag = Utils.makeHeroTag(id);
|
||||
return Dismissible(
|
||||
movementDuration: const Duration(milliseconds: 300),
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Icon(Icons.clear_all_rounded),
|
||||
SizedBox(width: 6),
|
||||
Text('取消收藏')
|
||||
],
|
||||
)),
|
||||
direction: DismissDirection.endToStart,
|
||||
key: ValueKey<int>(videoItem.id),
|
||||
onDismissed: (DismissDirection direction) {
|
||||
_favDetailController.onCancelFav(videoItem.id);
|
||||
// widget.onDeleteNotice();
|
||||
},
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
Get.toNamed('/video?aid=$id',
|
||||
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 5, 12, 5),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
return SizedBox(
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
double PR =
|
||||
MediaQuery.of(context).devicePixelRatio;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
// src: videoItem['pic'] +
|
||||
// '@${(maxWidth * 2).toInt()}w',
|
||||
src: videoItem.pic + '@.webp',
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
// Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,),
|
||||
Positioned(
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 1, horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(4),
|
||||
color:
|
||||
Colors.black54.withOpacity(0.4)),
|
||||
child: Text(
|
||||
Utils.timeFormat(videoItem.duration!),
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Colors.white),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
VideoContent(videoItem: videoItem)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoContent extends StatelessWidget {
|
||||
final videoItem;
|
||||
const VideoContent({super.key, required this.videoItem});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
fontWeight: FontWeight.w500),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
videoItem.owner.name,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'gray',
|
||||
view: videoItem.cntInfo['play'],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku'])
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/api.dart';
|
||||
import 'package:pilipala/http/init.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/model_rec_video_item.dart';
|
||||
|
||||
class HomeController extends GetxController {
|
||||
@ -17,28 +16,27 @@ class HomeController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
queryRcmdFeed('init');
|
||||
// queryRcmdFeed('init');
|
||||
}
|
||||
|
||||
// 获取推荐
|
||||
Future queryRcmdFeed(type) async {
|
||||
var res = await Request().get(
|
||||
Api.recommendList,
|
||||
data: {'feed_version': "V3", 'ps': count, 'fresh_idx': _currentPage},
|
||||
var res = await VideoHttp.rcmdVideoList(
|
||||
ps: count,
|
||||
freshIdx: _currentPage,
|
||||
);
|
||||
List<RecVideoItemModel> list = [];
|
||||
for (var i in res.data['data']['item']) {
|
||||
list.add(RecVideoItemModel.fromJson(i));
|
||||
if (res['status']) {
|
||||
if (type == 'init') {
|
||||
videoList.value = res['data'];
|
||||
} else if (type == 'onRefresh') {
|
||||
videoList.insertAll(0, res['data']);
|
||||
} else if (type == 'onLoad') {
|
||||
videoList.addAll(res['data']);
|
||||
}
|
||||
_currentPage += 1;
|
||||
}
|
||||
if (type == 'init') {
|
||||
videoList.value = list;
|
||||
} else if (type == 'onRefresh') {
|
||||
videoList.insertAll(0, list);
|
||||
} else if (type == 'onLoad') {
|
||||
videoList.addAll(list);
|
||||
}
|
||||
_currentPage += 1;
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
@ -48,7 +46,6 @@ class HomeController extends GetxController {
|
||||
|
||||
// 上拉加载
|
||||
Future onLoad() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
queryRcmdFeed('onLoad');
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_v.dart';
|
||||
import 'package:pilipala/common/widgets/animated_dialog.dart';
|
||||
import 'package:pilipala/common/widgets/overlay_pop.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/video_card_v.dart';
|
||||
import './controller.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
@ -18,6 +19,7 @@ class HomePage extends StatefulWidget {
|
||||
class _HomePageState extends State<HomePage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final HomeController _homeController = Get.put(HomeController());
|
||||
Future? _futureBuilderFuture;
|
||||
List videoList = [];
|
||||
|
||||
@override
|
||||
@ -26,6 +28,7 @@ class _HomePageState extends State<HomePage>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = _homeController.queryRcmdFeed('init');
|
||||
_homeController.videoList.listen((value) {
|
||||
videoList = value;
|
||||
setState(() {});
|
||||
@ -71,37 +74,25 @@ class _HomePageState extends State<HomePage>
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.fromLTRB(
|
||||
StyleString.cardSpace, 0, StyleString.cardSpace, 8),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
// 行间距
|
||||
mainAxisSpacing: StyleString.cardSpace,
|
||||
// 列间距
|
||||
crossAxisSpacing: StyleString.cardSpace,
|
||||
// 列数
|
||||
crossAxisCount: _homeController.crossAxisCount,
|
||||
mainAxisExtent: MediaQuery.of(context).size.width /
|
||||
_homeController.crossAxisCount /
|
||||
StyleString.aspectRatio +
|
||||
72),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return videoList.isNotEmpty
|
||||
? VideoCardV(
|
||||
videoItem: videoList[index],
|
||||
longPress: () {
|
||||
_homeController.popupDialog =
|
||||
_createPopupDialog(videoList[index]);
|
||||
Overlay.of(context)
|
||||
.insert(_homeController.popupDialog!);
|
||||
},
|
||||
longPressEnd: () {
|
||||
_homeController.popupDialog?.remove();
|
||||
},
|
||||
)
|
||||
: const VideoCardVSkeleton();
|
||||
},
|
||||
childCount: videoList.isNotEmpty ? videoList.length : 10,
|
||||
),
|
||||
sliver: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(() => contentGrid(
|
||||
_homeController, _homeController.videoList));
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return contentGrid(_homeController, []);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const LoadingMore()
|
||||
@ -114,8 +105,44 @@ class _HomePageState extends State<HomePage>
|
||||
|
||||
OverlayEntry _createPopupDialog(videoItem) {
|
||||
return OverlayEntry(
|
||||
builder: (context) => AnimatedDialog(
|
||||
child: OverlayPop(videoItem: videoItem),
|
||||
builder: (context) => AnimatedDialog(
|
||||
child: OverlayPop(videoItem: videoItem),
|
||||
));
|
||||
}
|
||||
|
||||
Widget contentGrid(ctr, videoList) {
|
||||
return SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
// 行间距
|
||||
mainAxisSpacing: StyleString.cardSpace,
|
||||
// 列间距
|
||||
crossAxisSpacing: StyleString.cardSpace,
|
||||
// 列数
|
||||
crossAxisCount: ctr.crossAxisCount,
|
||||
mainAxisExtent: MediaQuery.of(context).size.width /
|
||||
ctr.crossAxisCount /
|
||||
StyleString.aspectRatio +
|
||||
70,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return videoList!.isNotEmpty
|
||||
?
|
||||
// VideoCardV(videoItem: videoList![index])
|
||||
VideoCardV(
|
||||
videoItem: videoList[index],
|
||||
longPress: () {
|
||||
_homeController.popupDialog =
|
||||
_createPopupDialog(videoList[index]);
|
||||
Overlay.of(context).insert(_homeController.popupDialog!);
|
||||
},
|
||||
longPressEnd: () {
|
||||
_homeController.popupDialog?.remove();
|
||||
},
|
||||
)
|
||||
: const VideoCardVSkeleton();
|
||||
},
|
||||
childCount: videoList!.isNotEmpty ? videoList!.length : 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/pages/mine/view.dart';
|
||||
|
||||
class HomeAppBar extends StatelessWidget {
|
||||
const HomeAppBar({super.key});
|
||||
@ -9,11 +13,7 @@ class HomeAppBar extends StatelessWidget {
|
||||
return SliverAppBar(
|
||||
// forceElevated: true,
|
||||
scrolledUnderElevation: 0,
|
||||
toolbarHeight: Platform.isAndroid
|
||||
? (MediaQuery.of(context).padding.top + 6)
|
||||
: Platform.isIOS
|
||||
? MediaQuery.of(context).padding.top - 2
|
||||
: kToolbarHeight,
|
||||
toolbarHeight: MediaQuery.of(context).padding.top,
|
||||
expandedHeight: kToolbarHeight + MediaQuery.of(context).padding.top,
|
||||
automaticallyImplyLeading: false,
|
||||
pinned: true,
|
||||
@ -29,19 +29,26 @@ class HomeAppBar extends StatelessWidget {
|
||||
title: const Text(
|
||||
'PiLiPaLa',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
fontFamily: 'ArchivoNarrow',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.notifications_none_rounded),
|
||||
icon: const Icon(CupertinoIcons.search, size: 22),
|
||||
),
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(CupertinoIcons.bell, size: 22),
|
||||
// ),
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
onPressed: () {
|
||||
Get.bottomSheet(const MinePage());
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.person, size: 22),
|
||||
),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/api.dart';
|
||||
import 'package:pilipala/http/init.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/model_hot_video_item.dart';
|
||||
|
||||
class HotController extends GetxController {
|
||||
@ -14,31 +12,24 @@ class HotController extends GetxController {
|
||||
bool flag = false;
|
||||
OverlayEntry? popupDialog;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
queryHotFeed('init');
|
||||
}
|
||||
|
||||
// 获取推荐
|
||||
Future queryHotFeed(type) async {
|
||||
var res = await Request().get(
|
||||
Api.hotList,
|
||||
data: {'pn': _currentPage, 'ps': _count},
|
||||
var res = await VideoHttp.hotVideoList(
|
||||
pn: _currentPage,
|
||||
ps: _count,
|
||||
);
|
||||
List<HotVideoItemModel> list = [];
|
||||
for (var i in res.data['data']['list']) {
|
||||
list.add(HotVideoItemModel.fromJson(i));
|
||||
if (res['status']) {
|
||||
if (type == 'init') {
|
||||
videoList.value = res['data'];
|
||||
} else if (type == 'onRefresh') {
|
||||
videoList.insertAll(0, res['data']);
|
||||
} else if (type == 'onLoad') {
|
||||
videoList.addAll(res['data']);
|
||||
}
|
||||
_currentPage += 1;
|
||||
}
|
||||
if (type == 'init') {
|
||||
videoList.value = list;
|
||||
} else if (type == 'onRefresh') {
|
||||
videoList.insertAll(0, list);
|
||||
} else if (type == 'onLoad') {
|
||||
videoList.addAll(list);
|
||||
}
|
||||
_currentPage += 1;
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
|
||||
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/animated_dialog.dart';
|
||||
import 'package:pilipala/common/widgets/overlay_pop.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/video_card_h.dart';
|
||||
import 'package:pilipala/pages/hot/controller.dart';
|
||||
import 'package:pilipala/pages/home/widgets/app_bar.dart';
|
||||
@ -16,6 +18,7 @@ class HotPage extends StatefulWidget {
|
||||
class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||
final HotController _hotController = Get.put(HotController());
|
||||
List videoList = [];
|
||||
Future? _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@ -23,11 +26,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hotController.videoList.listen((value) {
|
||||
videoList = value;
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_futureBuilderFuture = _hotController.queryHotFeed('init');
|
||||
_hotController.scrollController.addListener(
|
||||
() {
|
||||
if (_hotController.scrollController.position.pixels >=
|
||||
@ -54,20 +53,46 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
|
||||
controller: _hotController.scrollController,
|
||||
slivers: [
|
||||
const HomeAppBar(),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return VideoCardH(
|
||||
videoItem: videoList[index],
|
||||
longPress: () {
|
||||
_hotController.popupDialog =
|
||||
_createPopupDialog(videoList[index]);
|
||||
Overlay.of(context).insert(_hotController.popupDialog!);
|
||||
},
|
||||
longPressEnd: () {
|
||||
_hotController.popupDialog?.remove();
|
||||
},
|
||||
);
|
||||
}, childCount: videoList.length)),
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return VideoCardH(
|
||||
videoItem: _hotController.videoList[index],
|
||||
longPress: () {
|
||||
_hotController.popupDialog = _createPopupDialog(
|
||||
_hotController.videoList[index]);
|
||||
Overlay.of(context)
|
||||
.insert(_hotController.popupDialog!);
|
||||
},
|
||||
longPressEnd: () {
|
||||
_hotController.popupDialog?.remove();
|
||||
},
|
||||
);
|
||||
}, childCount: _hotController.videoList.length),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
}, childCount: 5),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).padding.bottom + 10,
|
||||
|
||||
@ -1,30 +1,97 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/pages/home/view.dart';
|
||||
import 'package:pilipala/pages/hot/view.dart';
|
||||
import 'package:pilipala/pages/mine/view.dart';
|
||||
import 'package:pilipala/pages/media/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class MainController extends GetxController {
|
||||
List<Widget> pages = <Widget>[
|
||||
const HomePage(),
|
||||
const HotPage(),
|
||||
const MinePage(),
|
||||
const MediaPage(),
|
||||
];
|
||||
List navigationBars = [
|
||||
RxList navigationBars = [
|
||||
{
|
||||
'icon': const Icon(Icons.home_outlined),
|
||||
'selectedIcon': const Icon(Icons.home),
|
||||
// 'icon': const Icon(Icons.home_outlined),
|
||||
// 'selectedIcon': const Icon(Icons.home),
|
||||
'icon': const Icon(
|
||||
CupertinoIcons.square_favorites_alt,
|
||||
size: 21,
|
||||
),
|
||||
'selectedIcon': const Icon(
|
||||
CupertinoIcons.square_favorites_alt_fill,
|
||||
size: 21,
|
||||
),
|
||||
'label': "推荐",
|
||||
},
|
||||
{
|
||||
'icon': const Icon(Icons.whatshot_outlined),
|
||||
'selectedIcon': const Icon(Icons.whatshot_rounded),
|
||||
// 'icon': const Icon(Icons.whatshot_outlined),
|
||||
// 'selectedIcon': const Icon(Icons.whatshot_rounded),
|
||||
'icon': const Icon(
|
||||
CupertinoIcons.flame,
|
||||
size: 20,
|
||||
),
|
||||
'selectedIcon': const Icon(
|
||||
CupertinoIcons.flame_fill,
|
||||
size: 20,
|
||||
),
|
||||
'label': "热门",
|
||||
},
|
||||
// {
|
||||
// 'icon': const Icon(
|
||||
// CupertinoIcons.person,
|
||||
// size: 21,
|
||||
// ),
|
||||
// 'selectedIcon': const Icon(
|
||||
// CupertinoIcons.person_fill,
|
||||
// size: 21,
|
||||
// ),
|
||||
// 'label': "我的",
|
||||
// },
|
||||
{
|
||||
'icon': const Icon(Icons.person_outline),
|
||||
'selectedIcon': const Icon(Icons.person),
|
||||
'label': "我的",
|
||||
// 'icon': const Icon(Icons.person_outline),
|
||||
// 'selectedIcon': const Icon(Icons.person),
|
||||
'icon': const Icon(
|
||||
CupertinoIcons.folder,
|
||||
size: 20,
|
||||
),
|
||||
'selectedIcon': const Icon(
|
||||
CupertinoIcons.folder_fill,
|
||||
size: 20,
|
||||
),
|
||||
'label': "媒体库",
|
||||
}
|
||||
];
|
||||
].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// readuUserFace();
|
||||
}
|
||||
|
||||
// 设置头像
|
||||
// readuUserFace() async {
|
||||
// Box user = GStrorage.user;
|
||||
// if (user.get(UserBoxKey.userFace) != null) {
|
||||
// navigationBars.last['icon'] =
|
||||
// navigationBars.last['selectedIcon'] = NetworkImgLayer(
|
||||
// width: 25,
|
||||
// height: 25,
|
||||
// type: 'avatar',
|
||||
// src: user.get(UserBoxKey.userFace),
|
||||
// );
|
||||
// navigationBars.last['label'] = '我';
|
||||
// }
|
||||
// }
|
||||
|
||||
// 重置
|
||||
// resetLast() {
|
||||
// navigationBars.last['icon'] = const Icon(Icons.person_outline);
|
||||
// navigationBars.last['selectedIcon'] = const Icon(Icons.person);
|
||||
// navigationBars.last['label'] = '我的';
|
||||
// }
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
final MainController _mainController = Get.put(MainController());
|
||||
final HomeController _homeController = Get.put(HomeController());
|
||||
final HotController _hotController = Get.put(HotController());
|
||||
PageController? _pageController;
|
||||
|
||||
late AnimationController? _animationController;
|
||||
late Animation<double>? _fadeAnimation;
|
||||
@ -36,6 +37,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
_slideAnimation =
|
||||
Tween(begin: 0.8, end: 1.0).animate(_animationController!);
|
||||
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
|
||||
_pageController = PageController(initialPage: selectedIndex);
|
||||
}
|
||||
|
||||
void setIndex(int value) async {
|
||||
@ -47,7 +49,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
_pageController!.jumpToPage(value);
|
||||
var currentPage = _mainController.pages[value];
|
||||
if (currentPage is HomePage) {
|
||||
if (_homeController.flag) {
|
||||
@ -98,23 +100,30 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
|
||||
reverseCurve: Curves.linear,
|
||||
),
|
||||
),
|
||||
child: IndexedStack(
|
||||
index: selectedIndex,
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
onPageChanged: (index) {
|
||||
selectedIndex = index;
|
||||
setState(() {});
|
||||
},
|
||||
children: _mainController.pages,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
elevation: 1,
|
||||
destinations: _mainController.navigationBars.map((e) {
|
||||
return NavigationDestination(
|
||||
icon: e['icon'],
|
||||
selectedIcon: e['selectedIcon'],
|
||||
label: e['label'],
|
||||
);
|
||||
}).toList(),
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: (value) => setIndex(value),
|
||||
bottomNavigationBar: Obx(
|
||||
() => NavigationBar(
|
||||
elevation: 1,
|
||||
destinations: _mainController.navigationBars.map((e) {
|
||||
return NavigationDestination(
|
||||
icon: e['icon'],
|
||||
selectedIcon: e['selectedIcon'],
|
||||
label: e['label'],
|
||||
);
|
||||
}).toList(),
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: (value) => setIndex(value),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
41
lib/pages/media/controller.dart
Normal file
41
lib/pages/media/controller.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class MediaController extends GetxController {
|
||||
Rx<FavFolderData> favFolderData = FavFolderData().obs;
|
||||
List list = [
|
||||
{
|
||||
'icon': Icons.file_download_outlined,
|
||||
'title': '离线缓存',
|
||||
'onTap': () {},
|
||||
},
|
||||
{
|
||||
'icon': Icons.history,
|
||||
'title': '观看记录',
|
||||
'onTap': () {},
|
||||
},
|
||||
{
|
||||
'icon': Icons.star_border,
|
||||
'title': '我的收藏',
|
||||
'onTap': () => Get.toNamed('/fav'),
|
||||
},
|
||||
{
|
||||
'icon': Icons.watch_later_outlined,
|
||||
'title': '稍候再看',
|
||||
'onTap': () => {},
|
||||
},
|
||||
];
|
||||
|
||||
Future<dynamic> queryFavFolder() async {
|
||||
var res = await await UserHttp.userfavFolder(
|
||||
pn: 1,
|
||||
ps: 5,
|
||||
mid: GStrorage.user.get(UserBoxKey.userMid),
|
||||
);
|
||||
favFolderData.value = res['data'];
|
||||
return res;
|
||||
}
|
||||
}
|
||||
4
lib/pages/media/index.dart
Normal file
4
lib/pages/media/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library media;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
242
lib/pages/media/view.dart
Normal file
242
lib/pages/media/view.dart
Normal file
@ -0,0 +1,242 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/pages/media/index.dart';
|
||||
|
||||
class MediaPage extends StatefulWidget {
|
||||
const MediaPage({super.key});
|
||||
|
||||
@override
|
||||
State<MediaPage> createState() => _MediaPageState();
|
||||
}
|
||||
|
||||
class _MediaPageState extends State<MediaPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final MediaController _mediaController = Get.put(MediaController());
|
||||
Future? _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = _mediaController.queryFavFolder();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color primary = Theme.of(context).colorScheme.primary;
|
||||
return Scaffold(
|
||||
appBar: AppBar(toolbarHeight: 30),
|
||||
body: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: null,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: Text(
|
||||
'媒体库',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (var i in _mediaController.list) ...[
|
||||
ListTile(
|
||||
onTap: () => i['onTap'](),
|
||||
dense: true,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Icon(
|
||||
i['icon'],
|
||||
color: primary,
|
||||
),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 15, top: 2, bottom: 2),
|
||||
minLeadingWidth: 0,
|
||||
title: Text(
|
||||
i['title'],
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
],
|
||||
favFolder()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget favFolder() {
|
||||
return Column(
|
||||
children: [
|
||||
Divider(
|
||||
height: 35,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
leading: null,
|
||||
dense: true,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Obx(
|
||||
() => Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '收藏夹 ',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.titleMedium!.fontSize,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (_mediaController.favFolderData.value.count != null)
|
||||
TextSpan(
|
||||
text: _mediaController.favFolderData.value.count
|
||||
.toString(),
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: IconButton(
|
||||
onPressed: () => _mediaController.queryFavFolder(),
|
||||
icon: const Icon(
|
||||
Icons.refresh,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
// const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 170,
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data;
|
||||
if (data['status']) {
|
||||
List favFolderList =
|
||||
_mediaController.favFolderData.value.list!;
|
||||
int favFolderCount =
|
||||
_mediaController.favFolderData.value.count!;
|
||||
bool flag = favFolderCount > favFolderList.length;
|
||||
return Obx(() => ListView.builder(
|
||||
itemCount: _mediaController
|
||||
.favFolderData.value.list!.length +
|
||||
(flag ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (flag && index == favFolderList.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 14, bottom: 35),
|
||||
child: Center(
|
||||
child: IconButton(
|
||||
onPressed: () => Get.toNamed('/fav'),
|
||||
icon: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 18,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
));
|
||||
} else {
|
||||
return FavFolderItem(
|
||||
item: _mediaController
|
||||
.favFolderData.value.list![index],
|
||||
index: index);
|
||||
}
|
||||
},
|
||||
scrollDirection: Axis.horizontal,
|
||||
));
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: 160,
|
||||
child: Center(child: Text(data['msg'])),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SizedBox();
|
||||
}
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FavFolderItem extends StatelessWidget {
|
||||
FavFolderItem({super.key, this.item, this.index});
|
||||
FavFolderItemData? item;
|
||||
int? index;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(left: index == 0 ? 20 : 0, right: 14),
|
||||
child: GestureDetector(
|
||||
onTap: () => Get.toNamed('/favDetail', arguments: item, parameters: {
|
||||
'mediaId': item!.id.toString(),
|
||||
}),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: 180,
|
||||
height: 110,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
offset: const Offset(4, -12), // 阴影与容器的距离
|
||||
blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。
|
||||
spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。
|
||||
),
|
||||
],
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return NetworkImgLayer(
|
||||
src: item!.cover,
|
||||
width: box.maxWidth,
|
||||
height: box.maxHeight,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' ${item!.title}',
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
Text(
|
||||
' 共${item!.mediaCount}条视频',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.copyWith(color: Theme.of(context).colorScheme.outline),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/user/info.dart';
|
||||
import 'package:pilipala/models/user/stat.dart';
|
||||
import 'package:pilipala/pages/main/controller.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class MineController extends GetxController {
|
||||
// 用户信息 头像、昵称、lv
|
||||
Rx<UserInfoData> userInfo = UserInfoData().obs;
|
||||
// 用户状态 动态、关注、粉丝
|
||||
Rx<UserStat> userStat = UserStat().obs;
|
||||
Box user = GStrorage.user;
|
||||
RxBool userLogin = false.obs;
|
||||
|
||||
onLogin() {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': 'https://passport.bilibili.com/h5-app/passport/login',
|
||||
'type': 'login',
|
||||
'pageTitle': '登录bilibili',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future queryUserInfo() async {
|
||||
if (user.get(UserBoxKey.userLogin) == null) {
|
||||
return {'status': false};
|
||||
}
|
||||
var res = await UserHttp.userInfo();
|
||||
if (res['status']) {
|
||||
if (res['data'].isLogin) {
|
||||
userInfo.value = res['data'];
|
||||
user.put(UserBoxKey.userName, res['data'].uname);
|
||||
user.put(UserBoxKey.userFace, res['data'].face);
|
||||
user.put(UserBoxKey.userMid, res['data'].mid);
|
||||
user.put(UserBoxKey.userLogin, true);
|
||||
userLogin.value = true;
|
||||
// Get.find<MainController>().readuUserFace();
|
||||
} else {
|
||||
resetUserInfo();
|
||||
}
|
||||
} else {
|
||||
resetUserInfo();
|
||||
// SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
await queryUserStatOwner();
|
||||
return res;
|
||||
}
|
||||
|
||||
Future queryUserStatOwner() async {
|
||||
var res = await UserHttp.userStatOwner();
|
||||
if (res['status']) {
|
||||
userStat.value = res['data'];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future resetUserInfo() async {
|
||||
userInfo.value = UserInfoData();
|
||||
userStat.value = UserStat();
|
||||
await user.delete(UserBoxKey.userName);
|
||||
await user.delete(UserBoxKey.userFace);
|
||||
await user.delete(UserBoxKey.userMid);
|
||||
await user.delete(UserBoxKey.userLogin);
|
||||
userLogin.value = false;
|
||||
// Get.find<MainController>().resetLast();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'controller.dart';
|
||||
|
||||
class MinePage extends StatefulWidget {
|
||||
const MinePage({super.key});
|
||||
@ -8,11 +13,352 @@ class MinePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MinePageState extends State<MinePage> {
|
||||
final MineController _mineController = Get.put(MineController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('我的'),
|
||||
automaticallyImplyLeading: false,
|
||||
scrolledUnderElevation: 0,
|
||||
elevation: 0,
|
||||
toolbarHeight: kTextTabBarHeight + 20,
|
||||
backgroundColor: Colors.transparent,
|
||||
title: null,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.changeThemeMode(ThemeMode.dark);
|
||||
},
|
||||
icon: Icon(
|
||||
Get.theme == ThemeData.light()
|
||||
? CupertinoIcons.moon
|
||||
: CupertinoIcons.sun_max,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Get.toNamed('/setting'),
|
||||
icon: const Icon(
|
||||
CupertinoIcons.slider_horizontal_3,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await _mineController.queryUserInfo();
|
||||
await _mineController.queryUserStatOwner();
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraint) {
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: SizedBox(
|
||||
height: constraint.maxHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
FutureBuilder(
|
||||
future: _mineController.queryUserInfo(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
print(snapshot.data);
|
||||
if (snapshot.data['status']) {
|
||||
return Obx(() => userInfoBuild());
|
||||
} else {
|
||||
return userInfoBuild();
|
||||
}
|
||||
} else {
|
||||
return userInfoBuild();
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget userInfoBuild() {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 5),
|
||||
GestureDetector(
|
||||
onTap: () => _mineController.onLogin(),
|
||||
child: ClipOval(
|
||||
child: Container(
|
||||
width: 85,
|
||||
height: 85,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
child: Center(
|
||||
child: _mineController.userInfo.value.face != null
|
||||
? NetworkImgLayer(
|
||||
src: _mineController.userInfo.value.face,
|
||||
width: 85,
|
||||
height: 85)
|
||||
: Image.asset('assets/images/loading.png'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_mineController.userInfo.value.uname ?? '点击头像登录',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Image.asset(
|
||||
'assets/images/lv/lv${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}.png',
|
||||
height: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text.rich(TextSpan(children: [
|
||||
TextSpan(
|
||||
text: '硬币: ',
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.outline)),
|
||||
TextSpan(
|
||||
text: (_mineController.userInfo.value.money ?? 'pilipala')
|
||||
.toString(),
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary)),
|
||||
]))
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
if (_mineController.userInfo.value.levelInfo != null) ...[
|
||||
LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return SizedBox(
|
||||
width: box.maxWidth,
|
||||
height: 24,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: SizedBox(
|
||||
height: 22,
|
||||
width: box.maxWidth *
|
||||
(1 -
|
||||
(_mineController
|
||||
.userInfo.value.levelInfo!.currentExp! /
|
||||
_mineController
|
||||
.userInfo.value.levelInfo!.nextExp!)),
|
||||
child: Center(
|
||||
child: Text(
|
||||
(_mineController
|
||||
.userInfo.value.levelInfo!.nextExp! -
|
||||
_mineController
|
||||
.userInfo.value.levelInfo!.currentExp!)
|
||||
.toString(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return Container(
|
||||
width: box.maxWidth,
|
||||
height: 1,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: box.maxWidth *
|
||||
(_mineController
|
||||
.userInfo.value.levelInfo!.currentExp! /
|
||||
_mineController
|
||||
.userInfo.value.levelInfo!.nextExp!),
|
||||
height: 1,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 30),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
TextStyle style = TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.titleMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold);
|
||||
return SizedBox(
|
||||
height: constraints.maxWidth / 3 * 0.6,
|
||||
child: GridView.count(
|
||||
primary: false,
|
||||
padding: const EdgeInsets.all(0),
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 1.67,
|
||||
children: <Widget>[
|
||||
InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
(_mineController.userStat.value.dynamicCount ??
|
||||
'-')
|
||||
.toString(),
|
||||
key: ValueKey<String>(_mineController
|
||||
.userStat.value.dynamicCount
|
||||
.toString()),
|
||||
style: style),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'动态',
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
(_mineController.userStat.value.following ??
|
||||
'-')
|
||||
.toString(),
|
||||
key: ValueKey<String>(_mineController
|
||||
.userStat.value.following
|
||||
.toString()),
|
||||
style: style),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'关注',
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
(_mineController.userStat.value.follower ?? '-')
|
||||
.toString(),
|
||||
key: ValueKey<String>(_mineController
|
||||
.userStat.value.follower
|
||||
.toString()),
|
||||
style: style),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'粉丝',
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActionItem extends StatelessWidget {
|
||||
Icon? icon;
|
||||
Function? onTap;
|
||||
String? text;
|
||||
|
||||
ActionItem({
|
||||
Key? key,
|
||||
this.icon,
|
||||
this.onTap,
|
||||
this.text,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon!.icon!),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
text!,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
72
lib/pages/preview/controller.dart
Normal file
72
lib/pages/preview/controller.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class PreviewController extends GetxController {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
RxInt initialPage = 0.obs;
|
||||
RxInt currentPage = 1.obs;
|
||||
RxList imgList = [].obs;
|
||||
bool storage = true;
|
||||
bool videos = true;
|
||||
bool photos = true;
|
||||
bool visiable = true;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (Get.arguments != null) {
|
||||
initialPage.value = Get.arguments['initialPage']!;
|
||||
currentPage.value = Get.arguments['initialPage']! + 1;
|
||||
imgList.value = Get.arguments['imgList'];
|
||||
}
|
||||
}
|
||||
|
||||
requestPermission() async {
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.storage,
|
||||
// Permission.photos
|
||||
].request();
|
||||
|
||||
final info = statuses[Permission.storage].toString();
|
||||
// final photosInfo = statuses[Permission.photos].toString();
|
||||
|
||||
print('授权状态:$info');
|
||||
}
|
||||
|
||||
// 图片保存
|
||||
void onSaveImg() async {
|
||||
var response = await Dio().get(imgList[initialPage.value],
|
||||
options: Options(responseType: ResponseType.bytes));
|
||||
final result = await ImageGallerySaver.saveImage(
|
||||
Uint8List.fromList(response.data),
|
||||
quality: 100,
|
||||
name: "pic_vvex${DateTime.now().toString().split('-').join()}");
|
||||
if (result != null) {
|
||||
if (result['isSuccess']) {
|
||||
print('已保存到相册');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片分享
|
||||
void onShareImg() async {
|
||||
requestPermission();
|
||||
var response = await Dio().get(imgList[initialPage.value],
|
||||
options: Options(responseType: ResponseType.bytes));
|
||||
final temp = await getTemporaryDirectory();
|
||||
String imgName =
|
||||
"pic_vvex${DateTime.now().toString().split('-').join()}.jpg";
|
||||
var path = '${temp.path}/$imgName';
|
||||
File(path).writeAsBytesSync(response.data);
|
||||
Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]);
|
||||
}
|
||||
}
|
||||
4
lib/pages/preview/index.dart
Normal file
4
lib/pages/preview/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library preview;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
183
lib/pages/preview/view.dart
Normal file
183
lib/pages/preview/view.dart
Normal file
@ -0,0 +1,183 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:pilipala/common/widgets/appbar.dart';
|
||||
import 'controller.dart';
|
||||
|
||||
typedef DoubleClickAnimationListener = void Function();
|
||||
|
||||
class ImagePreview extends StatefulWidget {
|
||||
const ImagePreview({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ImagePreviewState createState() => _ImagePreviewState();
|
||||
}
|
||||
|
||||
class _ImagePreviewState extends State<ImagePreview>
|
||||
with TickerProviderStateMixin {
|
||||
final PreviewController _previewController = Get.put(PreviewController());
|
||||
late AnimationController animationController;
|
||||
late AnimationController _doubleClickAnimationController;
|
||||
Animation<double>? _doubleClickAnimation;
|
||||
late DoubleClickAnimationListener _doubleClickAnimationListener;
|
||||
List<double> doubleTapScales = <double>[1.0, 2.0];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
animationController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 400));
|
||||
_doubleClickAnimationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 250), vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController.dispose();
|
||||
_doubleClickAnimationController.dispose();
|
||||
clearGestureDetailsCache();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBarWidget(
|
||||
controller: animationController,
|
||||
visible: _previewController.visiable,
|
||||
child: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
title: Obx(
|
||||
() => Text.rich(
|
||||
TextSpan(children: [
|
||||
TextSpan(text: _previewController.currentPage.toString()),
|
||||
const TextSpan(text: ' / '),
|
||||
TextSpan(text: _previewController.imgList.length.toString()),
|
||||
]),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
tooltip: 'action',
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
value: 'share',
|
||||
onTap: _previewController.onShareImg,
|
||||
child: const Text('分享'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'save',
|
||||
onTap: _previewController.onSaveImg,
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () {
|
||||
_previewController.visiable = !_previewController.visiable;
|
||||
setState(() {});
|
||||
},
|
||||
child: ExtendedImageGesturePageView.builder(
|
||||
controller: ExtendedPageController(
|
||||
initialPage: _previewController.initialPage.value,
|
||||
pageSpacing: 0,
|
||||
),
|
||||
onPageChanged: (int index) {
|
||||
_previewController.initialPage.value = index;
|
||||
_previewController.currentPage.value = index + 1;
|
||||
},
|
||||
canScrollPage: (GestureDetails? gestureDetails) =>
|
||||
gestureDetails!.totalScale! <= 1.0,
|
||||
preloadPagesCount: 2,
|
||||
itemCount: _previewController.imgList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ExtendedImage.network(
|
||||
_previewController.imgList[index],
|
||||
fit: BoxFit.contain,
|
||||
mode: ExtendedImageMode.gesture,
|
||||
onDoubleTap: (ExtendedImageGestureState state) {
|
||||
final Offset? pointerDownPosition = state.pointerDownPosition;
|
||||
final double? begin = state.gestureDetails!.totalScale;
|
||||
double end;
|
||||
|
||||
//remove old
|
||||
_doubleClickAnimation
|
||||
?.removeListener(_doubleClickAnimationListener);
|
||||
|
||||
//stop pre
|
||||
_doubleClickAnimationController.stop();
|
||||
|
||||
//reset to use
|
||||
_doubleClickAnimationController.reset();
|
||||
|
||||
if (begin == doubleTapScales[0]) {
|
||||
end = doubleTapScales[1];
|
||||
} else {
|
||||
end = doubleTapScales[0];
|
||||
}
|
||||
|
||||
_doubleClickAnimationListener = () {
|
||||
state.handleDoubleTap(
|
||||
scale: _doubleClickAnimation!.value,
|
||||
doubleTapPosition: pointerDownPosition);
|
||||
};
|
||||
_doubleClickAnimation = _doubleClickAnimationController
|
||||
.drive(Tween<double>(begin: begin, end: end));
|
||||
|
||||
_doubleClickAnimation!
|
||||
.addListener(_doubleClickAnimationListener);
|
||||
|
||||
_doubleClickAnimationController.forward();
|
||||
},
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
if (state.extendedImageLoadState == LoadState.loading) {
|
||||
final ImageChunkEvent? loadingProgress =
|
||||
state.loadingProgress;
|
||||
final double? progress =
|
||||
loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 150.0,
|
||||
child: LinearProgressIndicator(value: progress),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Text('${((progress ?? 0.0) * 100).toInt()}%'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
initGestureConfigHandler: (ExtendedImageState state) {
|
||||
return GestureConfig(
|
||||
inPageView: true,
|
||||
initialScale: 1.0,
|
||||
maxScale: 5.0,
|
||||
animationMaxScale: 6.0,
|
||||
initialAlignment: InitialAlignment.center,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _previewController.onSaveImg(),
|
||||
child: const Icon(Icons.save_alt_rounded),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
lib/pages/setting/controller.dart
Normal file
23
lib/pages/setting/controller.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/init.dart';
|
||||
import 'package:pilipala/pages/mine/controller.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
class SettingController extends GetxController {
|
||||
Box user = GStrorage.user;
|
||||
RxBool userLogin = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
|
||||
}
|
||||
|
||||
loginOut() async {
|
||||
await Request.removeCookie();
|
||||
await Get.find<MineController>().resetUserInfo();
|
||||
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
|
||||
}
|
||||
}
|
||||
4
lib/pages/setting/index.dart
Normal file
4
lib/pages/setting/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library setting;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
36
lib/pages/setting/view.dart
Normal file
36
lib/pages/setting/view.dart
Normal file
@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/pages/setting/index.dart';
|
||||
|
||||
class SettingPage extends StatefulWidget {
|
||||
const SettingPage({super.key});
|
||||
|
||||
@override
|
||||
State<SettingPage> createState() => _SettingPageState();
|
||||
}
|
||||
|
||||
class _SettingPageState extends State<SettingPage> {
|
||||
final SettingController _settingController = Get.put(SettingController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('设置'),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Obx(
|
||||
() => Visibility(
|
||||
visible: _settingController.userLogin.value,
|
||||
child: ListTile(
|
||||
onTap: () => _settingController.loginOut(),
|
||||
dense: false,
|
||||
title: const Text('退出登录'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class VideoDetailController extends GetxController {
|
||||
int tabInitialIndex = 0;
|
||||
// tabs
|
||||
RxList<String> tabs = <String>['简介', '评论'].obs;
|
||||
|
||||
// 视频aid
|
||||
String aid = Get.parameters['aid']!;
|
||||
|
||||
// 是否预渲染 骨架屏
|
||||
bool preRender = false;
|
||||
|
||||
// 视频详情 上个页面传入
|
||||
Map videoItem = {};
|
||||
|
||||
// 请求状态
|
||||
RxBool isLoading = false.obs;
|
||||
|
||||
String heroTag = '';
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (Get.arguments.isNotEmpty) {
|
||||
if (Get.arguments.containsKey('videoItem')) {
|
||||
preRender = true;
|
||||
var args = Get.arguments['videoItem'];
|
||||
if (args.pic != null && args.pic != '') {
|
||||
videoItem['pic'] = args.pic;
|
||||
}
|
||||
}
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
231
lib/pages/video/detail/introduction/controller.dart
Normal file
231
lib/pages/video/detail/introduction/controller.dart
Normal file
@ -0,0 +1,231 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/controller.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class VideoIntroController extends GetxController {
|
||||
// 视频aid
|
||||
String aid = Get.parameters['aid']!;
|
||||
|
||||
// 是否预渲染 骨架屏
|
||||
bool preRender = false;
|
||||
|
||||
// 视频详情 上个页面传入
|
||||
Map? videoItem = {};
|
||||
|
||||
// 请求状态
|
||||
RxBool isLoading = false.obs;
|
||||
|
||||
// 视频详情 请求返回
|
||||
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
|
||||
|
||||
// 请求返回的信息
|
||||
String responseMsg = '请求异常';
|
||||
|
||||
// up主粉丝数
|
||||
Map userStat = {'follower': '-'};
|
||||
|
||||
// 是否点赞
|
||||
RxBool hasLike = false.obs;
|
||||
// 是否投币
|
||||
RxBool hasCoin = false.obs;
|
||||
// 是否收藏
|
||||
RxBool hasFav = false.obs;
|
||||
Box user = GStrorage.user;
|
||||
bool userLogin = false;
|
||||
Rx<FavFolderData> favFolderData = FavFolderData().obs;
|
||||
List addMediaIdsNew = [];
|
||||
List delMediaIdsNew = [];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (Get.arguments.isNotEmpty) {
|
||||
if (Get.arguments.containsKey('videoItem')) {
|
||||
preRender = true;
|
||||
var args = Get.arguments['videoItem'];
|
||||
videoItem!['pic'] = args.pic;
|
||||
videoItem!['title'] = args.title;
|
||||
if (args.stat != null) {
|
||||
videoItem!['stat'] = args.stat;
|
||||
}
|
||||
videoItem!['pubdate'] = args.pubdate;
|
||||
videoItem!['owner'] = args.owner;
|
||||
}
|
||||
}
|
||||
userLogin = user.get(UserBoxKey.userLogin) != null;
|
||||
}
|
||||
|
||||
// 获取视频简介
|
||||
Future queryVideoIntro() async {
|
||||
var result = await VideoHttp.videoIntro(aid: aid);
|
||||
if (result['status']) {
|
||||
videoDetail.value = result['data']!;
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
|
||||
.tabs
|
||||
.value = ['简介', '评论 ${result['data']!.stat!.reply}'];
|
||||
} else {
|
||||
responseMsg = result['msg'];
|
||||
}
|
||||
// 获取到粉丝数再返回
|
||||
await queryUserStat();
|
||||
if (userLogin) {
|
||||
// 获取点赞状态
|
||||
queryHasLikeVideo();
|
||||
// 获取投币状态
|
||||
queryHasCoinVideo();
|
||||
// 获取收藏状态
|
||||
queryHasFavVideo();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取up主粉丝数
|
||||
Future queryUserStat() async {
|
||||
var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);
|
||||
if (result['status']) {
|
||||
userStat = result['data'];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取点赞状态
|
||||
Future queryHasLikeVideo() async {
|
||||
var result = await VideoHttp.hasLikeVideo(aid: aid);
|
||||
// data num 被点赞标志 0:未点赞 1:已点赞
|
||||
hasLike.value = result["data"] == 1 ? true : false;
|
||||
}
|
||||
|
||||
// 获取投币状态
|
||||
Future queryHasCoinVideo() async {
|
||||
var result = await VideoHttp.hasCoinVideo(aid: aid);
|
||||
hasCoin.value = result["data"]['multiply'] == 0 ? false : true;
|
||||
}
|
||||
|
||||
// 获取收藏状态
|
||||
Future queryHasFavVideo() async {
|
||||
var result = await VideoHttp.hasFavVideo(aid: aid);
|
||||
hasFav.value = result["data"]['favoured'];
|
||||
}
|
||||
|
||||
// 一键三连
|
||||
Future actionOneThree() async {
|
||||
if (hasLike.value && hasCoin.value && hasFav.value) {
|
||||
// 已点赞、投币、收藏
|
||||
SmartDialog.showToast('🙏 UP已经收到了~');
|
||||
return false;
|
||||
}
|
||||
SmartDialog.show(
|
||||
useSystem: true,
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('提示'),
|
||||
content: const Text('一键三连 给UP送温暖'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => SmartDialog.dismiss(),
|
||||
child: const Text('点错了')),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
var result = await VideoHttp.oneThree(aid: aid);
|
||||
if (result['status']) {
|
||||
hasLike.value = result["data"]["like"];
|
||||
hasCoin.value = result["data"]["coin"];
|
||||
hasFav.value = result["data"]["fav"];
|
||||
SmartDialog.showToast('三连成功 🎉');
|
||||
} else {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
SmartDialog.dismiss();
|
||||
},
|
||||
child: const Text('确认'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// (取消)点赞
|
||||
Future actionLikeVideo() async {
|
||||
var result = await VideoHttp.likeVideo(aid: aid, type: !hasLike.value);
|
||||
if (result['status']) {
|
||||
hasLike.value = result["data"] == 1 ? true : false;
|
||||
if (hasLike.value) {
|
||||
SmartDialog.showToast('已点赞 👍');
|
||||
} else {
|
||||
SmartDialog.showToast('取消赞');
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
// 投币
|
||||
Future actionCoinVideo() async {
|
||||
print('投币');
|
||||
}
|
||||
|
||||
// (取消)收藏
|
||||
Future actionFavVideo() async {
|
||||
try {
|
||||
for (var i in favFolderData.value.list!.toList()) {
|
||||
if (i.favState == 1) {
|
||||
addMediaIdsNew.add(i.id);
|
||||
} else {
|
||||
delMediaIdsNew.add(i.id);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
var result = await VideoHttp.favVideo(
|
||||
aid: aid,
|
||||
addIds: addMediaIdsNew.join(','),
|
||||
delIds: delMediaIdsNew.join(','));
|
||||
if (result['status']) {
|
||||
if (result['data']['prompt']) {
|
||||
addMediaIdsNew = [];
|
||||
delMediaIdsNew = [];
|
||||
Get.back();
|
||||
// 重新获取收藏状态
|
||||
queryHasFavVideo();
|
||||
SmartDialog.showToast('✅ 操作成功');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分享视频
|
||||
Future actionShareVideo() async {
|
||||
print('分享视频');
|
||||
}
|
||||
|
||||
Future queryVideoInFolder() async {
|
||||
var result = await VideoHttp.videoInFolder(
|
||||
mid: user.get(UserBoxKey.userMid), rid: aid);
|
||||
if (result['status']) {
|
||||
favFolderData.value = result['data'];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 选择文件夹
|
||||
onChoose(bool checkValue, int index) {
|
||||
List<FavFolderItemData> datalist = favFolderData.value.list!;
|
||||
for (var i = 0; i < datalist.length; i++) {
|
||||
if (i == index) {
|
||||
datalist[i].favState = checkValue == true ? 1 : 0;
|
||||
datalist[i].mediaCount = checkValue == true
|
||||
? datalist[i].mediaCount! + 1
|
||||
: datalist[i].mediaCount! - 1;
|
||||
}
|
||||
}
|
||||
favFolderData.value.list = datalist;
|
||||
favFolderData.refresh();
|
||||
}
|
||||
}
|
||||
4
lib/pages/video/detail/introduction/index.dart
Normal file
4
lib/pages/video/detail/introduction/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library video_intro_panel;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
541
lib/pages/video/detail/introduction/view.dart
Normal file
541
lib/pages/video/detail/introduction/view.dart
Normal file
@ -0,0 +1,541 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/pages/fav/index.dart';
|
||||
import 'package:pilipala/pages/favDetail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/widgets/expandable_section.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/controller.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class VideoIntroPanel extends StatefulWidget {
|
||||
const VideoIntroPanel({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<VideoIntroPanel> createState() => _VideoIntroPanelState();
|
||||
}
|
||||
|
||||
class _VideoIntroPanelState extends State<VideoIntroPanel>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final VideoIntroController videoIntroController =
|
||||
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
|
||||
VideoDetailData? videoDetail;
|
||||
|
||||
// 添加页面缓存
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
videoIntroController.videoDetail.listen((value) {
|
||||
videoDetail = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
videoIntroController.onClose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: videoIntroController.queryVideoIntro(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data['status']) {
|
||||
// 请求成功
|
||||
// return _buildView(context, false, videoDetail);
|
||||
return VideoInfo(loadingStatus: false, videoDetail: videoDetail);
|
||||
} else {
|
||||
// 请求错误
|
||||
return HttpError(
|
||||
errMsg: snapshot.data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return VideoInfo(loadingStatus: true, videoDetail: videoDetail);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoInfo extends StatefulWidget {
|
||||
bool loadingStatus = false;
|
||||
VideoDetailData? videoDetail;
|
||||
|
||||
VideoInfo({Key? key, required this.loadingStatus, this.videoDetail})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<VideoInfo> createState() => _VideoInfoState();
|
||||
}
|
||||
|
||||
class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
Map videoItem = Get.put(VideoIntroController()).videoItem!;
|
||||
final VideoIntroController videoIntroController =
|
||||
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
|
||||
bool isExpand = false;
|
||||
|
||||
/// 手动控制动画的控制器
|
||||
late AnimationController? _manualController;
|
||||
|
||||
/// 手动控制
|
||||
late Animation<double>? _manualAnimation;
|
||||
|
||||
final FavController _favController = Get.put(FavController());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
/// 不设置重复,使用代码控制进度,动画时间1秒
|
||||
_manualController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
);
|
||||
_manualAnimation =
|
||||
Tween<double>(begin: 0.5, end: 1.5).animate(_manualController!);
|
||||
}
|
||||
|
||||
showFavBottomSheet() {
|
||||
Get.bottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
Container(
|
||||
height: 450,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
AppBar(
|
||||
toolbarHeight: 50,
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
elevation: 1,
|
||||
title: Text(
|
||||
'选择文件夹',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => videoIntroController.actionFavVideo(),
|
||||
child: const Text('完成'),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: FutureBuilder(
|
||||
future: videoIntroController.queryVideoInFolder(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => ListView.builder(
|
||||
itemCount: videoIntroController
|
||||
.favFolderData.value.list!.length +
|
||||
1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return const SizedBox(height: 10);
|
||||
} else {
|
||||
return ListTile(
|
||||
onTap: () => videoIntroController.onChoose(
|
||||
videoIntroController.favFolderData.value
|
||||
.list![index - 1].favState !=
|
||||
1,
|
||||
index - 1),
|
||||
dense: true,
|
||||
leading:
|
||||
const Icon(Icons.folder_special_outlined),
|
||||
minLeadingWidth: 0,
|
||||
title: Text(videoIntroController.favFolderData
|
||||
.value.list![index - 1].title!),
|
||||
subtitle: Text(
|
||||
'${videoIntroController.favFolderData.value.list![index - 1].mediaCount}个内容',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.fontSize),
|
||||
),
|
||||
trailing: Transform.scale(
|
||||
scale: 0.9,
|
||||
child: Checkbox(
|
||||
value: videoIntroController
|
||||
.favFolderData
|
||||
.value
|
||||
.list![index - 1]
|
||||
.favState ==
|
||||
1,
|
||||
onChanged: (bool? checkValue) =>
|
||||
videoIntroController.onChoose(
|
||||
checkValue!, index - 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return Text('请求中');
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
persistent: false,
|
||||
backgroundColor: Theme.of(context).bottomSheetTheme.backgroundColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 20),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: !widget.loadingStatus || videoItem.isNotEmpty
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableRegion(
|
||||
magnifierConfiguration: const TextMagnifierConfiguration(),
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: MaterialTextSelectionControls(),
|
||||
child: Text(
|
||||
!widget.loadingStatus
|
||||
? widget.videoDetail!.title
|
||||
: videoItem['title'],
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
splashColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
onTap: () {
|
||||
_manualController!.animateTo(isExpand ? 0 : 0.5);
|
||||
setState(() {
|
||||
isExpand = !isExpand;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 2),
|
||||
StatView(
|
||||
theme: 'gray',
|
||||
view: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.view
|
||||
: videoItem['stat'].view,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
StatDanMu(
|
||||
theme: 'gray',
|
||||
danmu: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.danmaku
|
||||
: videoItem['stat'].danmaku,
|
||||
size: 'medium',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
Utils.dateFormat(
|
||||
!widget.loadingStatus
|
||||
? widget.videoDetail!.pubdate
|
||||
: videoItem['pubdate'],
|
||||
formatType: 'detail'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
const Spacer(),
|
||||
RotationTransition(
|
||||
turns: _manualAnimation!,
|
||||
child: SizedBox(
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
onPressed: () {
|
||||
/// 0.5代表 180弧度
|
||||
_manualController!
|
||||
.animateTo(isExpand ? 0 : 0.5);
|
||||
setState(() {
|
||||
isExpand = !isExpand;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
FontAwesomeIcons.angleUp,
|
||||
size: 15,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 简介 默认收起
|
||||
if (!widget.loadingStatus)
|
||||
ExpandedSection(
|
||||
expand: isExpand,
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
height: 1.5,
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium?.fontSize,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: SelectableRegion(
|
||||
magnifierConfiguration:
|
||||
const TextMagnifierConfiguration(),
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: MaterialTextSelectionControls(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.videoDetail!.bvid!),
|
||||
Text(widget.videoDetail!.desc!),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_actionGrid(context, videoIntroController),
|
||||
Divider(
|
||||
height: 26,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
type: 'avatar',
|
||||
src: !widget.loadingStatus
|
||||
? widget.videoDetail!.owner!.face
|
||||
: videoItem['owner'].face,
|
||||
width: 38,
|
||||
height: 38,
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(!widget.loadingStatus
|
||||
? widget.videoDetail!.owner!.name
|
||||
: videoItem['owner'].name),
|
||||
// const SizedBox(width: 10),
|
||||
Text(
|
||||
widget.loadingStatus
|
||||
? '- 粉丝'
|
||||
: '${Utils.numFormat(videoIntroController.userStat['follower'])}粉丝',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
AnimatedOpacity(
|
||||
opacity: widget.loadingStatus ? 0 : 1,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
child: const Text('关注'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
height: 26,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
// const SizedBox(height: 10),
|
||||
],
|
||||
)
|
||||
: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 喜欢 投币 分享
|
||||
Widget _actionGrid(BuildContext context, videoIntroController) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
height: constraints.maxWidth / 5 * 0.8,
|
||||
child: GridView.count(
|
||||
primary: false,
|
||||
padding: const EdgeInsets.all(0),
|
||||
crossAxisCount: 5,
|
||||
childAspectRatio: 1.25,
|
||||
children: <Widget>[
|
||||
// ActionItem(
|
||||
// icon: const Icon(FontAwesomeIcons.s),
|
||||
// selectIcon: const Icon(FontAwesomeIcons.s),
|
||||
// onTap: () => {},
|
||||
// selectStatus: true,
|
||||
// loadingStatus: false,
|
||||
// text: '三连',
|
||||
// ),
|
||||
// Column(
|
||||
// children: [],
|
||||
// ),
|
||||
InkWell(
|
||||
onTap: () => videoIntroController.actionOneThree(),
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Image.asset(
|
||||
'assets/images/logo/logo_big.png',
|
||||
width: 10,
|
||||
height: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.thumbsUp),
|
||||
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
|
||||
onTap: () => videoIntroController.actionLikeVideo(),
|
||||
selectStatus: videoIntroController.hasLike.value,
|
||||
loadingStatus: widget.loadingStatus,
|
||||
text: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.like!.toString()
|
||||
: '-'),
|
||||
),
|
||||
// ActionItem(
|
||||
// icon: const Icon(FontAwesomeIcons.thumbsDown),
|
||||
// selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown),
|
||||
// onTap: () => {},
|
||||
// selectStatus: false,
|
||||
// loadingStatus: widget.loadingStatus,
|
||||
// text: '不喜欢'),
|
||||
Obx(
|
||||
() => ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.b),
|
||||
selectIcon: const Icon(FontAwesomeIcons.b),
|
||||
onTap: () => videoIntroController.actionCoinVideo(),
|
||||
selectStatus: videoIntroController.hasCoin.value,
|
||||
loadingStatus: widget.loadingStatus,
|
||||
text: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.coin!.toString()
|
||||
: '-'),
|
||||
),
|
||||
Obx(
|
||||
() => ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.star),
|
||||
selectIcon: const Icon(FontAwesomeIcons.star),
|
||||
onTap: () => showFavBottomSheet(),
|
||||
selectStatus: videoIntroController.hasFav.value,
|
||||
loadingStatus: widget.loadingStatus,
|
||||
text: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.favorite!.toString()
|
||||
: '-'),
|
||||
),
|
||||
ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.shareFromSquare),
|
||||
onTap: () => videoIntroController.actionShareVideo(),
|
||||
selectStatus: false,
|
||||
loadingStatus: widget.loadingStatus,
|
||||
text: !widget.loadingStatus
|
||||
? widget.videoDetail!.stat!.share!.toString()
|
||||
: '-'),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ActionItem extends StatelessWidget {
|
||||
Icon? icon;
|
||||
Icon? selectIcon;
|
||||
Function? onTap;
|
||||
bool? loadingStatus;
|
||||
String? text;
|
||||
bool selectStatus = false;
|
||||
|
||||
ActionItem({
|
||||
Key? key,
|
||||
this.icon,
|
||||
this.selectIcon,
|
||||
this.onTap,
|
||||
this.loadingStatus,
|
||||
this.text,
|
||||
required this.selectStatus,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => onTap!(),
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
selectStatus
|
||||
? Icon(selectIcon!.icon!,
|
||||
size: 21, color: Theme.of(context).primaryColor)
|
||||
: Icon(icon!.icon!,
|
||||
size: 21, color: Theme.of(context).colorScheme.outline),
|
||||
const SizedBox(height: 4),
|
||||
AnimatedOpacity(
|
||||
opacity: loadingStatus! ? 0 : 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
text ?? '',
|
||||
style: TextStyle(
|
||||
color: selectStatus
|
||||
? Theme.of(context).primaryColor
|
||||
: Theme.of(context).colorScheme.outline,
|
||||
fontSize: Theme.of(context).textTheme.labelSmall?.fontSize),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
0
lib/pages/video/detail/player/controller.dart
Normal file
0
lib/pages/video/detail/player/controller.dart
Normal file
4
lib/pages/video/detail/player/index.dart
Normal file
4
lib/pages/video/detail/player/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library video_player;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
0
lib/pages/video/detail/player/view.dart
Normal file
0
lib/pages/video/detail/player/view.dart
Normal file
11
lib/pages/video/detail/related/controller.dart
Normal file
11
lib/pages/video/detail/related/controller.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
|
||||
class ReleatedController extends GetxController {
|
||||
// 视频aid
|
||||
String aid = Get.parameters['aid']!;
|
||||
// 推荐视频列表
|
||||
List relatedVideoList = [];
|
||||
|
||||
Future<dynamic> queryRelatedVideo() => VideoHttp.relatedVideoList(aid: aid);
|
||||
}
|
||||
4
lib/pages/video/detail/related/index.dart
Normal file
4
lib/pages/video/detail/related/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library releated_video_panel;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
54
lib/pages/video/detail/related/view.dart
Normal file
54
lib/pages/video/detail/related/view.dart
Normal file
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/video_card_v.dart';
|
||||
import './controller.dart';
|
||||
|
||||
class RelatedVideoPanel extends StatefulWidget {
|
||||
const RelatedVideoPanel({super.key});
|
||||
|
||||
@override
|
||||
State<RelatedVideoPanel> createState() => _RelatedVideoPanelState();
|
||||
}
|
||||
|
||||
class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
|
||||
final ReleatedController _releatedController =
|
||||
Get.put(ReleatedController(), tag: Get.arguments['heroTag']);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: _releatedController.queryRelatedVideo(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data!['status']) {
|
||||
// 请求成功
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
if (index == snapshot.data['data'].length) {
|
||||
return SizedBox(height: MediaQuery.of(context).padding.bottom);
|
||||
} else {
|
||||
return VideoCardH(
|
||||
videoItem: snapshot.data['data'][index],
|
||||
);
|
||||
}
|
||||
}, childCount: snapshot.data['data'].length + 1));
|
||||
} else {
|
||||
// 请求错误
|
||||
return const Center(
|
||||
child: Text('出错了'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
}, childCount: 5),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
117
lib/pages/video/detail/reply/controller.dart
Normal file
117
lib/pages/video/detail/reply/controller.dart
Normal file
@ -0,0 +1,117 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/reply.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/common/reply_type.dart';
|
||||
import 'package:pilipala/models/video/reply/data.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
|
||||
class VideoReplyController extends GetxController {
|
||||
VideoReplyController(
|
||||
this.aid,
|
||||
this.rpid,
|
||||
this.level
|
||||
);
|
||||
final ScrollController scrollController = ScrollController();
|
||||
// 视频aid 请求时使用的oid
|
||||
String? aid;
|
||||
// 层级 2为楼中楼
|
||||
String? level;
|
||||
// rpid 请求楼中楼回复
|
||||
String? rpid;
|
||||
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
|
||||
// 当前页
|
||||
int currentPage = 0;
|
||||
bool isLoadingMore = false;
|
||||
RxBool noMore = false.obs;
|
||||
RxBool autoFocus = false.obs;
|
||||
// 当前回复的回复
|
||||
ReplyItemModel? currentReplyItem;
|
||||
// 回复来源
|
||||
String replySource = 'main';
|
||||
// 根评论 id 回复楼中楼回复使用
|
||||
int? rPid;
|
||||
// 默认回复主楼
|
||||
String replyLevel = '0';
|
||||
|
||||
Future queryReplyList({type = 'init'}) async {
|
||||
isLoadingMore = true;
|
||||
var res = level == '1'
|
||||
? await ReplyHttp.replyList(
|
||||
oid: aid!, pageNum: currentPage + 1, type: 1)
|
||||
: await ReplyHttp.replyReplyList(
|
||||
oid: aid!, root: rpid!, pageNum: currentPage + 1, type: 1);
|
||||
if (res['status']) {
|
||||
res['data'] = ReplyData.fromJson(res['data']);
|
||||
if (res['data'].replies.isNotEmpty) {
|
||||
currentPage = currentPage + 1;
|
||||
noMore.value = false;
|
||||
} else {
|
||||
if (currentPage == 0) {
|
||||
} else {
|
||||
noMore.value = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (res['data'].replies.length >= res['data'].page.count) {
|
||||
noMore.value = true;
|
||||
}
|
||||
if (type == 'init') {
|
||||
List<ReplyItemModel> replies = res['data'].replies;
|
||||
// 添加置顶回复
|
||||
if (res['data'].upper.top != null) {
|
||||
bool flag = false;
|
||||
for (var i = 0; i < res['data'].topReplies.length; i++) {
|
||||
if (res['data'].topReplies[i].rpid == res['data'].upper.top.rpid) {
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
if (!flag) {
|
||||
replies.insert(0, res['data'].upper.top);
|
||||
}
|
||||
}
|
||||
replies.insertAll(0, res['data'].topReplies);
|
||||
res['data'].replies = replies;
|
||||
replyList.value = res['data'].replies!;
|
||||
} else {
|
||||
replyList.addAll(res['data'].replies!);
|
||||
res['data'].replies.addAll(replyList);
|
||||
}
|
||||
}
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
// 上拉加载
|
||||
Future onLoad() async {
|
||||
queryReplyList(type: 'onLoad');
|
||||
}
|
||||
|
||||
wakeUpReply() {
|
||||
autoFocus.value = true;
|
||||
}
|
||||
|
||||
// 发表评论
|
||||
Future submitReplyAdd() async {
|
||||
print('replyLevel: $replyLevel');
|
||||
// print('rpid: $rpid');
|
||||
// print('currentReplyItem!.rpid: ${currentReplyItem!.rpid}');
|
||||
|
||||
|
||||
var result = await VideoHttp.replyAdd(
|
||||
type: ReplyType.video,
|
||||
oid: int.parse(aid!),
|
||||
root: replyLevel == '0' ? 0 : replyLevel == '1' ? currentReplyItem!.rpid : rPid,
|
||||
parent: replyLevel == '0' ? 0 : replyLevel == '1' ? currentReplyItem!.rpid : currentReplyItem!.rpid,
|
||||
message: replyLevel == '2' ? ' 回复 @${currentReplyItem!.member!.uname!} : 2楼31' : '2楼31',
|
||||
);
|
||||
if(result['status']){
|
||||
SmartDialog.showToast(result['data']['success_toast']);
|
||||
}else{
|
||||
SmartDialog.showToast(result['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
4
lib/pages/video/detail/reply/index.dart
Normal file
4
lib/pages/video/detail/reply/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library video_reply_panel;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
280
lib/pages/video/detail/reply/view.dart
Normal file
280
lib/pages/video/detail/reply/view.dart
Normal file
@ -0,0 +1,280 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/skeleton/video_reply.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'controller.dart';
|
||||
import 'widgets/reply_item.dart';
|
||||
|
||||
class VideoReplyPanel extends StatefulWidget {
|
||||
int oid;
|
||||
int rpid;
|
||||
String? level;
|
||||
VideoReplyPanel({
|
||||
this.oid = 0,
|
||||
this.rpid = 0,
|
||||
this.level,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VideoReplyPanel> createState() => _VideoReplyPanelState();
|
||||
}
|
||||
|
||||
class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
|
||||
late VideoReplyController _videoReplyController;
|
||||
late AnimationController fabAnimationCtr;
|
||||
late AnimationController replyAnimationCtl;
|
||||
|
||||
// List<ReplyItemModel>? replyList;
|
||||
Future? _futureBuilderFuture;
|
||||
bool _isFabVisible = true;
|
||||
String replyLevel = '1';
|
||||
|
||||
// 添加页面缓存
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
replyLevel = widget.level ?? '1';
|
||||
if (widget.level != null && widget.level == '2') {
|
||||
_videoReplyController = Get.put(
|
||||
VideoReplyController(
|
||||
widget.oid.toString(), widget.rpid.toString(), '2'),
|
||||
tag: widget.rpid.toString());
|
||||
_videoReplyController.rPid = widget.rpid;
|
||||
} else {
|
||||
_videoReplyController = Get.put(
|
||||
VideoReplyController(Get.parameters['aid']!, '', '1'),
|
||||
tag: Get.arguments['heroTag']);
|
||||
}
|
||||
// if(replyLevel != ''){
|
||||
// _videoReplyController.replyLevel = replyLevel;
|
||||
// }
|
||||
print(
|
||||
'_videoReplyController.replyLevel: ${_videoReplyController.replyLevel}');
|
||||
|
||||
fabAnimationCtr = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 300));
|
||||
replyAnimationCtl = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 500));
|
||||
|
||||
_futureBuilderFuture = _videoReplyController.queryReplyList();
|
||||
_videoReplyController.scrollController.addListener(
|
||||
() {
|
||||
if (_videoReplyController.scrollController.position.pixels >=
|
||||
_videoReplyController.scrollController.position.maxScrollExtent -
|
||||
300) {
|
||||
if (!_videoReplyController.isLoadingMore) {
|
||||
_videoReplyController.onLoad();
|
||||
}
|
||||
}
|
||||
|
||||
final ScrollDirection direction =
|
||||
_videoReplyController.scrollController.position.userScrollDirection;
|
||||
if (direction == ScrollDirection.forward) {
|
||||
_showFab();
|
||||
} else if (direction == ScrollDirection.reverse) {
|
||||
_hideFab();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showFab() {
|
||||
if (!_isFabVisible) {
|
||||
_isFabVisible = true;
|
||||
fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _hideFab() {
|
||||
if (_isFabVisible) {
|
||||
_isFabVisible = false;
|
||||
fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
void _showReply(source, {ReplyItemModel? replyItem, replyLevel}) async {
|
||||
// source main 直接回复 floor 楼中楼回复
|
||||
if (source == 'floor') {
|
||||
_videoReplyController.currentReplyItem = replyItem;
|
||||
_videoReplyController.replySource = source;
|
||||
_videoReplyController.replyLevel = replyLevel ?? '1';
|
||||
} else {
|
||||
_videoReplyController.replyLevel = '0';
|
||||
}
|
||||
|
||||
replyAnimationCtl.forward();
|
||||
await Future.delayed(const Duration(microseconds: 100));
|
||||
_videoReplyController.wakeUpReply();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: implement dispose
|
||||
super.dispose();
|
||||
fabAnimationCtr.dispose();
|
||||
_videoReplyController.scrollController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
setState(() {});
|
||||
_videoReplyController.currentPage = 0;
|
||||
return await _videoReplyController.queryReplyList();
|
||||
},
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
controller: _videoReplyController.scrollController,
|
||||
key: const PageStorageKey<String>('评论'),
|
||||
slivers: <Widget>[
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
() => SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index ==
|
||||
_videoReplyController.replyList.length) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
60,
|
||||
child: Center(
|
||||
child: Obx(() => Text(
|
||||
_videoReplyController.noMore.value
|
||||
? '没有更多了'
|
||||
: '加载中')),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ReplyItem(
|
||||
replyItem: _videoReplyController
|
||||
.replyList[index],
|
||||
weakUpReply: (replyItem, replyLevel) =>
|
||||
_showReply(
|
||||
'floor',
|
||||
replyItem: replyItem,
|
||||
replyLevel: replyLevel,
|
||||
),
|
||||
replyLevel: replyLevel);
|
||||
}
|
||||
},
|
||||
childCount:
|
||||
_videoReplyController.replyList.length + 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 5),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 14,
|
||||
right: 14,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
// begin: const Offset(0, 2),
|
||||
// 评论内容为空/不足一屏
|
||||
begin: const Offset(0, 0),
|
||||
end: const Offset(0, 0),
|
||||
).animate(CurvedAnimation(
|
||||
parent: fabAnimationCtr,
|
||||
curve: Curves.easeInOut,
|
||||
)),
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () => _showReply('main'),
|
||||
tooltip: '发表评论',
|
||||
child: const Icon(Icons.reply),
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 2),
|
||||
end: const Offset(0, 0),
|
||||
).animate(CurvedAnimation(
|
||||
parent: replyAnimationCtl,
|
||||
curve: Curves.easeInOut,
|
||||
)),
|
||||
child: Container(
|
||||
height: 100 + MediaQuery.of(context).padding.bottom,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: _videoReplyController.autoFocus.value,
|
||||
child: const TextField(
|
||||
autofocus: true,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: "友善评论", border: InputBorder.none),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
_videoReplyController.submitReplyAdd(),
|
||||
child: const Text('发送'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
689
lib/pages/video/detail/reply/widgets/reply_item.dart
Normal file
689
lib/pages/video/detail/reply/widgets/reply_item.dart
Normal file
@ -0,0 +1,689 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class ReplyItem extends StatelessWidget {
|
||||
ReplyItem({super.key, this.replyItem, this.weakUpReply, this.replyLevel});
|
||||
ReplyItemModel? replyItem;
|
||||
Function? weakUpReply;
|
||||
String? replyLevel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 6, 8, 0),
|
||||
child: content(context),
|
||||
),
|
||||
// Divider(
|
||||
// height: 1,
|
||||
// indent: 52,
|
||||
// endIndent: 10,
|
||||
// color: Theme.of(context).dividerColor.withOpacity(0.08),
|
||||
// )
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget lfAvtar(context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 5),
|
||||
child: Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: replyItem!.member!.avatar,
|
||||
width: 34,
|
||||
height: 34,
|
||||
type: 'avatar',
|
||||
),
|
||||
if (replyItem!.member!.officialVerify != null &&
|
||||
replyItem!.member!.officialVerify!['type'] == 0)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.offline_bolt,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
// child:
|
||||
// NetworkImgLayer(
|
||||
// src: replyItem!.member!.avatar,
|
||||
// width: 30,
|
||||
// height: 30,
|
||||
// type: 'avatar',
|
||||
// ),
|
||||
);
|
||||
}
|
||||
|
||||
Widget content(context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// 头像、昵称
|
||||
GestureDetector(
|
||||
// onTap: () =>
|
||||
// Get.toNamed('/member/${reply.userName}', parameters: {
|
||||
// 'memberAvatar': reply.avatar,
|
||||
// 'heroTag': reply.userName + heroTag,
|
||||
// }),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
image: replyItem!.member!.userSailing!.cardbg != null
|
||||
? DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: NetworkImage(
|
||||
replyItem!.member!.userSailing!.cardbg!['image'],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
lfAvtar(context),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
replyItem!.member!.uname!,
|
||||
style: TextStyle(
|
||||
color: replyItem!.isUp! ||
|
||||
replyItem!.member!.vip!['vipType'] > 0
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.outline,
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Image.asset(
|
||||
'assets/images/lv/lv${replyItem!.member!.level}.png',
|
||||
height: 11,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
if (replyItem!.isUp!) UpTag(),
|
||||
],
|
||||
),
|
||||
if (replyItem!.member!.userSailing!.cardbg != null &&
|
||||
replyItem!.member!.userSailing!.cardbg!['fan']['number'] >
|
||||
0)
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: Get.size.width / 7 * 5.6,
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontFamily: 'fansCard',
|
||||
fontSize: 9,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('NO.'),
|
||||
Text(
|
||||
replyItem!.member!.userSailing!.cardbg!['fan']
|
||||
['num_desc'],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// title
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 0, left: 45, right: 6, bottom: 6),
|
||||
child: SelectableRegion(
|
||||
magnifierConfiguration: const TextMagnifierConfiguration(),
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: MaterialTextSelectionControls(),
|
||||
child: Text.rich(
|
||||
style: const TextStyle(height: 1.65),
|
||||
TextSpan(
|
||||
children: [
|
||||
if (replyItem!.isTop!)
|
||||
WidgetSpan(child: UpTag(tagText: 'TOP')),
|
||||
buildContent(context, replyItem!.content!),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 操作区域
|
||||
bottonAction(context, replyItem!.replyControl),
|
||||
const SizedBox(height: 3),
|
||||
if (replyItem!.replies!.isNotEmpty) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2, bottom: 12),
|
||||
child: ReplyItemRow(
|
||||
replies: replyItem!.replies,
|
||||
replyControl: replyItem!.replyControl,
|
||||
f_rpid: replyItem!.rpid,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 感谢、回复、复制
|
||||
Widget bottonAction(context, replyControl) {
|
||||
var color = Theme.of(context).colorScheme.outline;
|
||||
return Row(
|
||||
children: [
|
||||
const SizedBox(width: 48),
|
||||
if (replyItem!.cardLabel!.isNotEmpty &&
|
||||
replyItem!.cardLabel!.contains('热评'))
|
||||
Text(
|
||||
'热评 • ',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.copyWith(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
Text(
|
||||
Utils.dateFormat(replyItem!.ctime),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.copyWith(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
if (replyItem!.replyControl != null &&
|
||||
replyItem!.replyControl!.location != '')
|
||||
Text(
|
||||
' • ${replyItem!.replyControl!.location!}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.copyWith(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
const Spacer(),
|
||||
if (replyItem!.upAction!.like!)
|
||||
Icon(Icons.favorite, color: Colors.red[400], size: 18),
|
||||
SizedBox(
|
||||
height: 28,
|
||||
width: 42,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
child: Text('回复', style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium),
|
||||
onPressed: () => weakUpReply!(replyItem, replyLevel),
|
||||
)),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: TextButton(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
FontAwesomeIcons.thumbsUp,
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
replyItem!.like.toString(),
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelSmall!.fontSize),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class ReplyItemRow extends StatelessWidget {
|
||||
ReplyItemRow({
|
||||
super.key,
|
||||
this.replies,
|
||||
this.replyControl,
|
||||
this.f_rpid,
|
||||
});
|
||||
List? replies;
|
||||
ReplyControl? replyControl;
|
||||
int? f_rpid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isShow = replyControl!.isShow!;
|
||||
int extraRow = replyControl != null && isShow ? 1 : 0;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 42, right: 4, top: 0),
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
animationDuration: Duration.zero,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: replies!.length + extraRow,
|
||||
itemBuilder: (context, index) {
|
||||
if (extraRow == 1 && index == replies!.length) {
|
||||
// 有楼中楼回复,在最后显示
|
||||
return InkWell(
|
||||
onTap: () => replyReply(context),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
),
|
||||
children: [
|
||||
if (replyControl!.upReply!)
|
||||
const TextSpan(text: 'up主等人 '),
|
||||
TextSpan(
|
||||
text: replyControl!.entryText!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return InkWell(
|
||||
onTap: () {},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
8,
|
||||
index == 0 && (extraRow == 1 || replies!.length > 1)
|
||||
? 8
|
||||
: 5,
|
||||
8,
|
||||
5),
|
||||
child: Text.rich(
|
||||
overflow: extraRow == 1
|
||||
? TextOverflow.ellipsis
|
||||
: TextOverflow.visible,
|
||||
maxLines: extraRow == 1 ? 2 : null,
|
||||
TextSpan(
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
replyReply(context);
|
||||
},
|
||||
children: [
|
||||
TextSpan(
|
||||
text: replies![index].member.uname + ' ',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall!
|
||||
.fontSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => {
|
||||
print('跳转至用户主页'),
|
||||
},
|
||||
),
|
||||
if (replies![index].isUp)
|
||||
WidgetSpan(
|
||||
child: UpTag(),
|
||||
),
|
||||
buildContent(context, replies![index].content),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void replyReply(context) {
|
||||
Get.bottomSheet(
|
||||
barrierColor: Colors.transparent,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
Container(
|
||||
height: Get.size.height - Get.size.width * 9 / 16 - 45,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
elevation: 1,
|
||||
title: Text(
|
||||
'评论详情',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: VideoReplyPanel(
|
||||
oid: replies!.first.oid,
|
||||
rpid: f_rpid!,
|
||||
level: '2',
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
persistent: false,
|
||||
backgroundColor: Theme.of(context).bottomSheetTheme.backgroundColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InlineSpan buildContent(BuildContext context, content) {
|
||||
if (content.emote.isEmpty &&
|
||||
content.atNameToMid.isEmpty &&
|
||||
content.jumpUrl.isEmpty &&
|
||||
content.vote.isEmpty &&
|
||||
content.pictures.isEmpty) {
|
||||
return TextSpan(text: content.message,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = ()=> {
|
||||
print('点击')
|
||||
},);
|
||||
}
|
||||
List<InlineSpan> spanChilds = [];
|
||||
// 匹配表情
|
||||
String matchEmote = content.message.splitMapJoin(
|
||||
RegExp(r"\[.*?\]"),
|
||||
onMatch: (Match match) {
|
||||
String matchStr = match[0]!;
|
||||
int size = content.emote[matchStr]['meta']['size'];
|
||||
if (content.emote.isNotEmpty) {
|
||||
if (content.emote.keys.contains(matchStr)) {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
src: content.emote[matchStr]['url'],
|
||||
type: 'emote',
|
||||
width: size * 20,
|
||||
height: size * 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
spanChilds.add(TextSpan(text: matchStr));
|
||||
return matchStr;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String str) {
|
||||
// 匹配@用户
|
||||
String matchMember = str;
|
||||
if (content.atNameToMid.isNotEmpty) {
|
||||
matchMember = str.splitMapJoin(
|
||||
RegExp(r"@.*( |:)"),
|
||||
onMatch: (Match match) {
|
||||
if (match[0] != null) {
|
||||
content.atNameToMid.forEach((key, value) {
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: '@$key ',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => {
|
||||
print('跳转至用户主页'),
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String str) {
|
||||
spanChilds.add(TextSpan(text: str));
|
||||
return str;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
matchMember = str;
|
||||
}
|
||||
|
||||
// 匹配 jumpUrl
|
||||
String matchUrl = matchMember;
|
||||
if (content.jumpUrl.isNotEmpty) {
|
||||
List urlKeys = content.jumpUrl.keys.toList();
|
||||
matchUrl = matchMember.splitMapJoin(
|
||||
RegExp("(?:${urlKeys.join("|")})"),
|
||||
onMatch: (Match match) {
|
||||
String matchStr = match[0]!;
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: content.jumpUrl[matchStr]['title'],
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => {
|
||||
print('Url 点击'),
|
||||
},
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: Icon(
|
||||
FontAwesomeIcons.magnifyingGlass,
|
||||
size: 9,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
alignment: PlaceholderAlignment.top,
|
||||
),
|
||||
);
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String str) {
|
||||
spanChilds.add(TextSpan(text: str));
|
||||
return str;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
str = matchUrl.splitMapJoin(
|
||||
RegExp(r"\d{1,2}:\d{1,2}"),
|
||||
onMatch: (Match match) {
|
||||
String matchStr = match[0]!;
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: ' $matchStr ',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => {
|
||||
print('time 点击'),
|
||||
},
|
||||
),
|
||||
);
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (str) {
|
||||
return str;
|
||||
},
|
||||
);
|
||||
|
||||
if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) {
|
||||
spanChilds.add(TextSpan(text: str));
|
||||
}
|
||||
return str;
|
||||
},
|
||||
);
|
||||
|
||||
// 图片渲染
|
||||
if (content.pictures.isNotEmpty) {
|
||||
List picList = [];
|
||||
int len = content.pictures.length;
|
||||
if (len == 1) {
|
||||
Map pictureItem = content.pictures.first;
|
||||
picList.add(pictureItem['img_src']);
|
||||
spanChilds.add(const TextSpan(text: '\n'));
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed('/preview',
|
||||
arguments: {'initialPage': 0, 'imgList': picList});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: NetworkImgLayer(
|
||||
src: pictureItem['img_src'],
|
||||
width: box.maxWidth / 2,
|
||||
height: box.maxWidth *
|
||||
0.5 *
|
||||
pictureItem['img_height'] /
|
||||
pictureItem['img_width'],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (len > 1) {
|
||||
List<Widget> list = [];
|
||||
for (var i = 0; i < len; i++) {
|
||||
picList.add(content.pictures[i]['img_src']);
|
||||
list.add(
|
||||
LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed('/preview',
|
||||
arguments: {'initialPage': i, 'imgList': picList});
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
src: content.pictures[i]['img_src'],
|
||||
width: box.maxWidth,
|
||||
height: box.maxWidth,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
double maxWidth = box.maxWidth;
|
||||
double crossCount = len < 3 ? 2 : 3;
|
||||
double height = maxWidth /
|
||||
crossCount *
|
||||
(len % crossCount == 0
|
||||
? len ~/ crossCount
|
||||
: len ~/ crossCount + 1) +
|
||||
6;
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
height: height,
|
||||
child: GridView(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// 子Item排列规则
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//横轴元素个数
|
||||
crossAxisCount: crossCount.toInt(),
|
||||
//纵轴间距
|
||||
mainAxisSpacing: 4.0,
|
||||
//横轴间距
|
||||
crossAxisSpacing: 4.0,
|
||||
//子组件宽高长度比例
|
||||
// childAspectRatio: 1,
|
||||
),
|
||||
//GridView中使用的子Widegt
|
||||
children: list,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// spanChilds.add(TextSpan(text: matchMember));
|
||||
return TextSpan(children: spanChilds);
|
||||
}
|
||||
|
||||
class UpTag extends StatelessWidget {
|
||||
String? tagText;
|
||||
UpTag({super.key, this.tagText = 'UP'});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color primary = Theme.of(context).colorScheme.primary;
|
||||
return Container(
|
||||
width: 24,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: tagText == 'UP' ? primary : null,
|
||||
border: Border.all(color: primary)),
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
child: Center(
|
||||
child: Text(
|
||||
tagText!,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: tagText == 'UP'
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,136 @@
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/related/index.dart';
|
||||
|
||||
class VideoDetailPage extends StatefulWidget {
|
||||
const VideoDetailPage({super.key});
|
||||
const VideoDetailPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<VideoDetailPage> createState() => _VideoDetailPageState();
|
||||
}
|
||||
|
||||
class _VideoDetailPageState extends State<VideoDetailPage> {
|
||||
final VideoDetailController videoDetailController =
|
||||
Get.put(VideoDetailController(), tag: Get.arguments['heroTag']);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('videoDetail'),
|
||||
final double statusBarHeight = MediaQuery.of(context).padding.top;
|
||||
final double pinnedHeaderHeight = statusBarHeight +
|
||||
kToolbarHeight +
|
||||
MediaQuery.of(context).size.width * 9 / 16;
|
||||
return DefaultTabController(
|
||||
initialIndex: videoDetailController.tabInitialIndex,
|
||||
length: videoDetailController.tabs.length, // tab的数量.
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
body: ExtendedNestedScrollView(
|
||||
headerSliverBuilder:
|
||||
(BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverAppBar(
|
||||
title: const Text("视频详情"),
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
expandedHeight: MediaQuery.of(context).size.width * 9 / 16,
|
||||
collapsedHeight: MediaQuery.of(context).size.width * 9 / 16,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
double PR = MediaQuery.of(context).devicePixelRatio;
|
||||
return Hero(
|
||||
tag: videoDetailController.heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoDetailController.videoItem['pic'],
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
pinnedHeaderSliverHeightBuilder: () {
|
||||
return pinnedHeaderHeight;
|
||||
},
|
||||
onlyOneScrollInBody: true,
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
width: 280,
|
||||
margin: const EdgeInsets.only(left: 20),
|
||||
child: Obx(
|
||||
() => TabBar(
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: videoDetailController.tabs
|
||||
.map((String name) => Tab(text: name))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 弹幕开关
|
||||
// const Spacer(),
|
||||
// Flexible(
|
||||
// flex: 2,
|
||||
// child: Container(
|
||||
// height: 50,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return const CustomScrollView(
|
||||
key: PageStorageKey<String>('简介'),
|
||||
slivers: <Widget>[
|
||||
VideoIntroPanel(),
|
||||
RelatedVideoPanel(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
VideoReplyPanel()
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
83
lib/pages/video/detail/widgets/expandable_section.dart
Normal file
83
lib/pages/video/detail/widgets/expandable_section.dart
Normal file
@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ExpandedSection extends StatefulWidget {
|
||||
final Widget child;
|
||||
final bool expand;
|
||||
double begin = 0.0;
|
||||
double end = 1.0;
|
||||
|
||||
ExpandedSection(
|
||||
{this.expand = false,
|
||||
required this.child,
|
||||
required this.begin,
|
||||
required this.end});
|
||||
|
||||
@override
|
||||
_ExpandedSectionState createState() => _ExpandedSectionState();
|
||||
}
|
||||
|
||||
class _ExpandedSectionState extends State<ExpandedSection>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController expandController;
|
||||
late Animation<double> animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
prepareAnimations();
|
||||
_runExpandCheck();
|
||||
}
|
||||
|
||||
///Setting up the animation
|
||||
// void prepareAnimations() {
|
||||
// expandController = AnimationController(
|
||||
// vsync: this, duration: const Duration(milliseconds: 500));
|
||||
// animation = CurvedAnimation(
|
||||
// parent: expandController,
|
||||
// curve: Curves.fastOutSlowIn,
|
||||
// );
|
||||
// }
|
||||
|
||||
void prepareAnimations() {
|
||||
expandController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 400));
|
||||
Animation<double> curve = CurvedAnimation(
|
||||
parent: expandController,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
animation = Tween(begin: widget.begin, end: widget.end).animate(curve);
|
||||
// animation = CurvedAnimation(
|
||||
// parent: expandController,
|
||||
// curve: Curves.fastOutSlowIn,
|
||||
// );
|
||||
}
|
||||
|
||||
void _runExpandCheck() {
|
||||
if (widget.expand) {
|
||||
expandController.forward();
|
||||
} else {
|
||||
expandController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ExpandedSection oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_runExpandCheck();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
expandController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizeTransition(
|
||||
axisAlignment: -1.0,
|
||||
sizeFactor: animation,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
85
lib/pages/webview/controller.dart
Normal file
85
lib/pages/webview/controller.dart
Normal file
@ -0,0 +1,85 @@
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/constants.dart';
|
||||
import 'package:pilipala/http/init.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/pages/home/index.dart';
|
||||
import 'package:pilipala/pages/mine/index.dart';
|
||||
import 'package:pilipala/utils/cookie.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class WebviewController extends GetxController {
|
||||
String url = '';
|
||||
String type = '';
|
||||
String pageTitle = '';
|
||||
final WebViewController controller = WebViewController();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
url = Get.parameters['url']!;
|
||||
type = Get.parameters['type']!;
|
||||
pageTitle = Get.parameters['pageTitle']!;
|
||||
|
||||
webviewInit();
|
||||
if (type == 'login') {
|
||||
controller.clearCache();
|
||||
controller.clearLocalStorage();
|
||||
WebViewCookieManager().clearCookies();
|
||||
controller.setUserAgent(Request().headerUa('mob'));
|
||||
}
|
||||
}
|
||||
|
||||
webviewInit() {
|
||||
controller
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
// 页面加载
|
||||
onProgress: (int progress) {
|
||||
// Update loading bar.
|
||||
},
|
||||
onPageStarted: (String url) {},
|
||||
// 加载完成
|
||||
onPageFinished: (String url) async {
|
||||
if (url.startsWith(
|
||||
'https://passport.bilibili.com/web/sso/exchange_cookie') ||
|
||||
url.startsWith('https://m.bilibili.com/')) {
|
||||
try {
|
||||
var cookies =
|
||||
await WebviewCookieManager().getCookies(HttpString.baseUrl);
|
||||
var apiCookies =
|
||||
await WebviewCookieManager().getCookies(HttpString.baseUrl);
|
||||
await SetCookie.onSet(cookies, HttpString.baseUrl);
|
||||
await SetCookie.onSet(apiCookies, HttpString.baseApiUrl);
|
||||
await UserHttp.userInfo();
|
||||
var result = await UserHttp.userInfo();
|
||||
print('网页登录: $result');
|
||||
if (result['status'] && result['data'].isLogin) {
|
||||
SmartDialog.showToast('登录成功');
|
||||
Box user = GStrorage.user;
|
||||
user.put(UserBoxKey.userLogin, true);
|
||||
Get.find<MineController>().userInfo.value = result['data'];
|
||||
Get.find<HomeController>().queryRcmdFeed('onRefresh');
|
||||
Get.back();
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
onWebResourceError: (WebResourceError error) {},
|
||||
onNavigationRequest: (NavigationRequest request) {
|
||||
if (request.url.startsWith('https://www.youtube.com/')) {
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(url));
|
||||
}
|
||||
}
|
||||
4
lib/pages/webview/index.dart
Normal file
4
lib/pages/webview/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library webview;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
29
lib/pages/webview/view.dart
Normal file
29
lib/pages/webview/view.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'controller.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class WebviewPage extends StatefulWidget {
|
||||
const WebviewPage({super.key});
|
||||
|
||||
@override
|
||||
State<WebviewPage> createState() => _WebviewPageState();
|
||||
}
|
||||
|
||||
class _WebviewPageState extends State<WebviewPage> {
|
||||
final WebviewController _webviewController = Get.put(WebviewController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
_webviewController.pageTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
body: WebViewWidget(controller: _webviewController.controller),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user