Merge branch 'main' into feature-replySave

This commit is contained in:
guozhigq
2024-07-26 22:53:27 +08:00
51 changed files with 1724 additions and 1232 deletions

View File

@ -66,7 +66,7 @@ class PBadge extends StatelessWidget {
border: Border.all(color: borderColor),
),
child: Text(
text!,
text ?? '',
style: TextStyle(fontSize: fs ?? fontSize, color: color),
),
);

View File

@ -1,130 +0,0 @@
// ignore_for_file: depend_on_referenced_packages
import 'dart:math';
import 'dart:ui' as ui show Image;
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
double get maxDragOffset => 100;
double hideHeight = maxDragOffset / 2.3;
double refreshHeight = maxDragOffset / 1.5;
class PullToRefreshHeader extends StatelessWidget {
const PullToRefreshHeader(
this.info,
this.lastRefreshTime, {
this.color,
super.key,
});
final PullToRefreshScrollNotificationInfo? info;
final DateTime? lastRefreshTime;
final Color? color;
@override
Widget build(BuildContext context) {
final PullToRefreshScrollNotificationInfo? infos = info;
if (infos == null) {
return const SizedBox();
}
String text = '';
if (infos.mode == PullToRefreshIndicatorMode.armed) {
text = 'Release to refresh';
} else if (infos.mode == PullToRefreshIndicatorMode.refresh ||
infos.mode == PullToRefreshIndicatorMode.snap) {
text = 'Loading...';
} else if (infos.mode == PullToRefreshIndicatorMode.done) {
text = 'Refresh completed.';
} else if (infos.mode == PullToRefreshIndicatorMode.drag) {
text = 'Pull to refresh';
} else if (infos.mode == PullToRefreshIndicatorMode.canceled) {
text = 'Cancel refresh';
}
final TextStyle ts = const TextStyle(
color: Colors.grey,
).copyWith(fontSize: 14);
final double dragOffset = info?.dragOffset ?? 0.0;
final DateTime time = lastRefreshTime ?? DateTime.now();
final double top = -hideHeight + dragOffset;
return Container(
height: dragOffset,
color: color ?? Colors.transparent,
// padding: EdgeInsets.only(top: dragOffset / 3),
// padding: EdgeInsets.only(bottom: 5.0),
child: Stack(
children: <Widget>[
Positioned(
left: 0.0,
right: 0.0,
top: top,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Container(
alignment: Alignment.centerRight,
margin: const EdgeInsets.only(right: 12.0),
child: RefreshImage(top, null),
),
),
Column(
children: <Widget>[
Text(text, style: ts),
Text(
'Last updated:${DateFormat('yyyy-MM-dd hh:mm').format(time)}',
style: ts.copyWith(fontSize: 14),
)
],
),
const Spacer(),
],
),
)
],
),
);
}
}
class RefreshImage extends StatelessWidget {
const RefreshImage(this.top, Key? key) : super(key: key);
final double top;
@override
Widget build(BuildContext context) {
const double imageSize = 30;
return ExtendedImage.asset(
'assets/flutterCandies_grey.png',
width: imageSize,
height: imageSize,
afterPaintImage: (Canvas canvas, Rect rect, ui.Image image, Paint paint) {
final double imageHeight = image.height.toDouble();
final double imageWidth = image.width.toDouble();
final Size size = rect.size;
final double y =
(1 - min(top / (refreshHeight - hideHeight), 1)) * imageHeight;
canvas.drawImageRect(
image,
Rect.fromLTWH(0.0, y, imageWidth, imageHeight - y),
Rect.fromLTWH(rect.left, rect.top + y / imageHeight * size.height,
size.width, (imageHeight - y) / imageHeight * size.height),
Paint()
..colorFilter =
const ColorFilter.mode(Color(0xFFea5504), BlendMode.srcIn)
..isAntiAlias = false
..filterQuality = FilterQuality.low,
);
//canvas.restore();
},
);
}
}

View File

@ -215,9 +215,8 @@ class VideoContent extends StatelessWidget {
children: [
if (videoItem.goto == 'bangumi')
_buildBadge(videoItem.bangumiBadge, 'line', 9),
if (videoItem.rcmdReason?.content != null &&
videoItem.rcmdReason.content != '')
_buildBadge(videoItem.rcmdReason.content, 'color'),
if (videoItem.rcmdReason != null)
_buildBadge(videoItem.rcmdReason, 'color'),
if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9),
if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'),
Expanded(

View File

@ -96,7 +96,14 @@ class MemberHttp {
'dm_img_str': dmImgStr.substring(0, dmImgStr.length - 2),
'dm_cover_img_str': dmCoverImgStr.substring(0, dmCoverImgStr.length - 2),
'dm_img_inter': '{"ds":[],"wh":[0,0,0],"of":[0,0,0]}',
...order == 'charge'
? {
'order': 'pubdate',
'special_type': 'charging',
}
: {}
});
var res = await Request().get(
Api.memberArchive,
data: params,

View File

@ -70,47 +70,43 @@ class VideoHttp {
// 添加额外的loginState变量模拟未登录状态
static Future rcmdVideoListApp(
{bool loginStatus = true, required int freshIdx}) async {
try {
var res = await Request().get(
Api.recommendListApp,
data: {
'idx': freshIdx,
'flush': '5',
'column': '4',
'device': 'pad',
'device_type': 0,
'device_name': 'vivo',
'pull': freshIdx == 0 ? 'true' : 'false',
'appkey': Constants.appKey,
'access_key': loginStatus
? (localCache.get(LocalCacheKey.accessKey,
defaultValue: {})['value'] ??
'')
: ''
},
);
if (res.data['code'] == 0) {
List<RecVideoItemAppModel> list = [];
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' &&
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) {
RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem);
}
var res = await Request().get(
Api.recommendListApp,
data: {
'idx': freshIdx,
'flush': '5',
'column': '4',
'device': 'pad',
'device_type': 0,
'device_name': 'vivo',
'pull': freshIdx == 0 ? 'true' : 'false',
'appkey': Constants.appKey,
'access_key': loginStatus
? (localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
'')
: ''
},
);
if (res.data['code'] == 0) {
List<RecVideoItemAppModel> list = [];
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' &&
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) {
RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem);
}
}
return {'status': true, 'data': list};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
} catch (err) {
return {'status': false, 'data': [], 'msg': err.toString()};
return {'status': true, 'data': list};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}

View File

@ -34,7 +34,7 @@ class RecVideoItemAppModel {
String? title;
int? isFollowed;
RcmdOwner? owner;
RcmdReason? rcmdReason;
String? rcmdReason;
String? goto;
int? param;
String? uri;
@ -64,17 +64,11 @@ class RecVideoItemAppModel {
//duration = json['cover_right_text'];
title = json['title'];
owner = RcmdOwner.fromJson(json);
rcmdReason = json['rcmd_reason_style'] != null
? RcmdReason.fromJson(json['rcmd_reason_style'])
: null;
rcmdReason = json['bottom_rcmd_reason'] ?? json['top_rcmd_reason'];
// 由于app端api并不会直接返回与owner的关注状态
// 所以借用推荐原因是否为“已关注”、“新关注”等判别关注状态从而与web端接口等效
RegExp regex = RegExp(r'已关注|新关注');
isFollowed = rcmdReason != null &&
rcmdReason!.content != null &&
regex.hasMatch(rcmdReason!.content!)
? 1
: 0;
isFollowed = regex.hasMatch(rcmdReason ?? '') ? 1 : 0;
// 如果是就无需再显示推荐原因交由view统一处理即可
if (isFollowed == 1) {
rcmdReason = null;

View File

@ -1,19 +1,12 @@
import 'package:hive/hive.dart';
part 'model_owner.g.dart';
@HiveType(typeId: 3)
class Owner {
Owner({
this.mid,
this.name,
this.face,
});
@HiveField(0)
int? mid;
@HiveField(1)
String? name;
@HiveField(2)
String? face;
Owner.fromJson(Map<String, dynamic> json) {

View File

@ -1,47 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'model_owner.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class OwnerAdapter extends TypeAdapter<Owner> {
@override
final int typeId = 3;
@override
Owner read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Owner(
mid: fields[0] as int?,
name: fields[1] as String?,
face: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, Owner obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.mid)
..writeByte(1)
..write(obj.name)
..writeByte(2)
..write(obj.face);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is OwnerAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -1,9 +1,5 @@
import './model_owner.dart';
import 'package:hive/hive.dart';
part 'model_rec_video_item.g.dart';
@HiveType(typeId: 0)
class RecVideoItemModel {
RecVideoItemModel({
this.id,
@ -21,32 +17,19 @@ class RecVideoItemModel {
this.rcmdReason,
});
@HiveField(0)
int? id = -1;
@HiveField(1)
String? bvid = '';
@HiveField(2)
int? cid = -1;
@HiveField(3)
String? goto = '';
@HiveField(4)
String? uri = '';
@HiveField(5)
String? pic = '';
@HiveField(6)
String? title = '';
@HiveField(7)
int? duration = -1;
@HiveField(8)
int? pubdate = -1;
@HiveField(9)
Owner? owner;
@HiveField(10)
Stat? stat;
@HiveField(11)
int? isFollowed;
@HiveField(12)
RcmdReason? rcmdReason;
String? rcmdReason;
RecVideoItemModel.fromJson(Map<String, dynamic> json) {
id = json["id"];
@ -61,26 +44,20 @@ class RecVideoItemModel {
owner = Owner.fromJson(json["owner"]);
stat = Stat.fromJson(json["stat"]);
isFollowed = json["is_followed"] ?? 0;
rcmdReason = json["rcmd_reason"] != null
? RcmdReason.fromJson(json["rcmd_reason"])
: RcmdReason(content: '');
rcmdReason = json["rcmd_reason"]?['content'];
}
}
@HiveType(typeId: 1)
class Stat {
Stat({
this.view,
this.like,
this.danmu,
});
@HiveField(0)
int? view;
@HiveField(1)
int? like;
@HiveField(2)
int? danmu;
int? view;
int? like;
int? danmu;
Stat.fromJson(Map<String, dynamic> json) {
// 无需在model中转换以保留原始数据在view层处理即可
view = json["view"];
@ -88,20 +65,3 @@ class Stat {
danmu = json['danmaku'];
}
}
@HiveType(typeId: 2)
class RcmdReason {
RcmdReason({
this.reasonType,
this.content,
});
@HiveField(0)
int? reasonType;
@HiveField(1)
String? content = '';
RcmdReason.fromJson(Map<String, dynamic> json) {
reasonType = json["reason_type"];
content = json["content"] ?? '';
}
}

View File

@ -1,154 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'model_rec_video_item.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RecVideoItemModelAdapter extends TypeAdapter<RecVideoItemModel> {
@override
final int typeId = 0;
@override
RecVideoItemModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RecVideoItemModel(
id: fields[0] as int?,
bvid: fields[1] as String?,
cid: fields[2] as int?,
goto: fields[3] as String?,
uri: fields[4] as String?,
pic: fields[5] as String?,
title: fields[6] as String?,
duration: fields[7] as int?,
pubdate: fields[8] as int?,
owner: fields[9] as Owner?,
stat: fields[10] as Stat?,
isFollowed: fields[11] as int?,
rcmdReason: fields[12] as RcmdReason?,
);
}
@override
void write(BinaryWriter writer, RecVideoItemModel obj) {
writer
..writeByte(13)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.bvid)
..writeByte(2)
..write(obj.cid)
..writeByte(3)
..write(obj.goto)
..writeByte(4)
..write(obj.uri)
..writeByte(5)
..write(obj.pic)
..writeByte(6)
..write(obj.title)
..writeByte(7)
..write(obj.duration)
..writeByte(8)
..write(obj.pubdate)
..writeByte(9)
..write(obj.owner)
..writeByte(10)
..write(obj.stat)
..writeByte(11)
..write(obj.isFollowed)
..writeByte(12)
..write(obj.rcmdReason);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RecVideoItemModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class StatAdapter extends TypeAdapter<Stat> {
@override
final int typeId = 1;
@override
Stat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Stat(
view: fields[0] as int?,
like: fields[1] as int?,
danmu: fields[2] as int?,
);
}
@override
void write(BinaryWriter writer, Stat obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.view)
..writeByte(1)
..write(obj.like)
..writeByte(2)
..write(obj.danmu);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is StatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdReasonAdapter extends TypeAdapter<RcmdReason> {
@override
final int typeId = 2;
@override
RcmdReason read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdReason(
reasonType: fields[0] as int?,
content: fields[1] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdReason obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.reasonType)
..writeByte(1)
..write(obj.content);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdReasonAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -1,14 +1,8 @@
import 'package:hive/hive.dart';
part 'hot.g.dart';
@HiveType(typeId: 6)
class HotSearchModel {
HotSearchModel({
this.list,
});
@HiveField(0)
List<HotSearchItem>? list;
HotSearchModel.fromJson(Map<String, dynamic> json) {
@ -18,7 +12,6 @@ class HotSearchModel {
}
}
@HiveType(typeId: 7)
class HotSearchItem {
HotSearchItem({
this.keyword,
@ -27,14 +20,10 @@ class HotSearchItem {
this.icon,
});
@HiveField(0)
String? keyword;
@HiveField(1)
String? showName;
// 4/5热 11话题 8普通 7直播
@HiveField(2)
int? wordType;
@HiveField(3)
String? icon;
HotSearchItem.fromJson(Map<String, dynamic> json) {

View File

@ -1,84 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hot.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class HotSearchModelAdapter extends TypeAdapter<HotSearchModel> {
@override
final int typeId = 6;
@override
HotSearchModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return HotSearchModel(
list: (fields[0] as List?)?.cast<HotSearchItem>(),
);
}
@override
void write(BinaryWriter writer, HotSearchModel obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.list);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HotSearchModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class HotSearchItemAdapter extends TypeAdapter<HotSearchItem> {
@override
final int typeId = 7;
@override
HotSearchItem read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return HotSearchItem(
keyword: fields[0] as String?,
showName: fields[1] as String?,
wordType: fields[2] as int?,
icon: fields[3] as String?,
);
}
@override
void write(BinaryWriter writer, HotSearchItem obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.keyword)
..writeByte(1)
..write(obj.showName)
..writeByte(2)
..write(obj.wordType)
..writeByte(3)
..write(obj.icon);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HotSearchItemAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -1,8 +1,3 @@
import 'package:hive/hive.dart';
part 'stat.g.dart';
@HiveType(typeId: 1)
class UserStat {
UserStat({
this.following,
@ -10,11 +5,8 @@ class UserStat {
this.dynamicCount,
});
@HiveField(0)
int? following;
@HiveField(1)
int? follower;
@HiveField(2)
int? dynamicCount;
UserStat.fromJson(Map<String, dynamic> json) {

View File

@ -1,47 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'stat.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserStatAdapter extends TypeAdapter<UserStat> {
@override
final int typeId = 1;
@override
UserStat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserStat(
following: fields[0] as int?,
follower: fields[1] as int?,
dynamicCount: fields[2] as int?,
);
}
@override
void write(BinaryWriter writer, UserStat obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.following)
..writeByte(1)
..write(obj.follower)
..writeByte(2)
..write(obj.dynamicCount);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserStatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -6,9 +6,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
@ -16,7 +14,6 @@ import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class DynamicsController extends GetxController {
int page = 1;
@ -282,4 +279,11 @@ class DynamicsController extends GetxController {
dynamicsList.value = <DynamicItemModel>[];
queryFollowDynamic();
}
// 点击up主
void onTapUp(data) {
mid.value = data.mid;
upInfo.value = data;
onSelectUp(data.mid);
}
}

View File

@ -106,7 +106,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
}
// 查看二级评论
void replyReply(replyItem) {
void replyReply(replyItem, currentReply, loadMore) {
int oid = replyItem.oid;
int rpid = replyItem.rpid!;
Get.to(
@ -125,6 +125,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
source: 'dynamic',
replyType: ReplyType.values[replyType],
firstFloor: replyItem,
loadMore: loadMore,
),
),
);
@ -324,8 +325,10 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
replyItem: replyList[index],
showReplyRow: true,
replyLevel: '1',
replyReply: (replyItem) =>
replyReply(replyItem),
replyReply:
(replyItem, currentReply, loadMore) =>
replyReply(replyItem,
currentReply, loadMore),
replyType: ReplyType.values[replyType],
addReply: (replyItem) {
replyList[index]

View File

@ -0,0 +1,46 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
class UpDynamicsController extends GetxController {
UpDynamicsController(this.upInfo);
UpItem upInfo;
RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;
RxBool isLoadingDynamic = false.obs;
String? offset = '';
int page = 1;
Future queryFollowDynamic({type = 'init'}) async {
if (type == 'init') {
dynamicsList.clear();
}
// 下拉刷新数据渲染时会触发onLoad
if (type == 'onLoad' && page == 1) {
return;
}
isLoadingDynamic.value = true;
var res = await DynamicsHttp.followDynamic(
page: type == 'init' ? 1 : page,
type: 'all',
offset: offset,
mid: upInfo.mid,
);
isLoadingDynamic.value = false;
if (res['status']) {
if (type == 'onLoad' && res['data'].items.isEmpty) {
SmartDialog.showToast('没有更多了');
return;
}
if (type == 'init') {
dynamicsList.value = res['data'].items;
} else {
dynamicsList.addAll(res['data'].items);
}
offset = res['data'].offset;
page++;
}
return res;
}
}

View File

@ -1,4 +1,4 @@
library preview;
library up_dynamics;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,151 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/utils/feed_back.dart';
import '../controller.dart';
import 'index.dart';
class OverlayPanel extends StatefulWidget {
const OverlayPanel({super.key, required this.ctr, required this.upInfo});
final DynamicsController ctr;
final UpItem upInfo;
@override
State<OverlayPanel> createState() => _OverlayPanelState();
}
class _OverlayPanelState extends State<OverlayPanel>
with SingleTickerProviderStateMixin {
static const itemPadding = EdgeInsets.symmetric(horizontal: 6, vertical: 0);
final PageController pageController = PageController();
late double contentWidth = 50;
late List<UpItem> upList;
late RxInt currentMid = (-1).obs;
TabController? _tabController;
@override
void initState() {
super.initState();
upList = widget.ctr.upData.value.upList!
.map<UpItem>((element) => element)
.toList();
upList.removeAt(0);
_tabController = TabController(length: upList.length, vsync: this);
currentMid.value = widget.upInfo.mid!;
pageController.addListener(() {
int index = pageController.page!.round();
int mid = upList[index].mid!;
if (mid != currentMid.value) {
currentMid.value = mid;
_tabController?.animateTo(index,
duration: Duration.zero, curve: Curves.linear);
onClickUp(upList[index], index, type: 'pageChange');
}
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
int index =
upList.indexWhere((element) => element.mid == widget.upInfo.mid);
pageController.jumpToPage(index);
onClickUp(widget.upInfo, index);
_tabController?.animateTo(index,
duration: Duration.zero, curve: Curves.linear);
onClickUp(upList[index], index, type: 'pageChange');
});
}
void onClickUp(data, i, {type = 'click'}) {
if (type == 'click') {
pageController.jumpToPage(i);
}
}
@override
Widget build(BuildContext context) {
return Container(
width: Get.width,
height: Get.height,
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.fromLTRB(
0,
MediaQuery.of(context).padding.top + 4,
0,
MediaQuery.of(context).padding.bottom + 4,
),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
SizedBox(
height: 50,
child: TabBar(
controller: _tabController,
dividerColor: Colors.transparent,
automaticIndicatorColorAdjustment: false,
tabAlignment: TabAlignment.start,
padding: const EdgeInsets.only(left: 12, right: 12),
indicatorPadding: EdgeInsets.zero,
indicatorSize: TabBarIndicatorSize.label,
indicator: const BoxDecoration(),
labelPadding: itemPadding,
indicatorWeight: 1,
isScrollable: true,
tabs: upList.map((e) => Tab(child: upItemBuild(e))).toList(),
onTap: (index) {
feedBack();
EasyThrottle.throttle(
'follow', const Duration(milliseconds: 200), () {
onClickUp(upList[index], index);
});
},
),
),
Expanded(
child: PageView.builder(
itemCount: upList.length,
controller: pageController,
itemBuilder: (BuildContext context, int index) {
return Container(
clipBehavior: Clip.antiAlias,
margin: const EdgeInsets.fromLTRB(10, 12, 10, 0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
),
child: UpDyanmicsPage(upInfo: upList[index], ctr: widget.ctr),
);
},
),
),
],
),
);
}
Widget upItemBuild(data) {
return Obx(
() => AnimatedOpacity(
opacity: currentMid == data.mid ? 1 : 0.3,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: currentMid == data.mid ? 1 : 0.9,
child: NetworkImgLayer(
width: contentWidth,
height: contentWidth,
src: data.face,
type: 'avatar',
),
),
),
);
}
}

View File

@ -0,0 +1,178 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/pages/dynamics/up_dynamic/index.dart';
import '../index.dart';
import '../widgets/dynamic_panel.dart';
class UpDyanmicsPage extends StatefulWidget {
final UpItem upInfo;
final DynamicsController ctr;
const UpDyanmicsPage({
required this.upInfo,
required this.ctr,
Key? key,
}) : super(key: key);
@override
State<UpDyanmicsPage> createState() => _UpDyanmicsPageState();
}
class _UpDyanmicsPageState extends State<UpDyanmicsPage>
with AutomaticKeepAliveClientMixin {
late UpDynamicsController _upDynamicsController;
final ScrollController scrollController = ScrollController();
late Future _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_upDynamicsController = Get.put(UpDynamicsController(widget.upInfo),
tag: widget.upInfo.mid.toString());
_futureBuilderFuture = _upDynamicsController.queryFollowDynamic();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'queryFollowDynamic', const Duration(seconds: 1), () {
_upDynamicsController.queryFollowDynamic(type: 'onLoad');
});
}
},
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return CustomScrollView(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
height: 50,
padding: const EdgeInsets.fromLTRB(20, 4, 4, 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.onSurface,
width: 0.1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.upInfo.uname!,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
)
],
),
),
),
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map? data = snapshot.data;
if (data != null && data['status']) {
List<DynamicItemModel> list =
_upDynamicsController.dynamicsList;
return Obx(
() {
if (list.isEmpty) {
if (_upDynamicsController.isLoadingDynamic.value) {
return skeleton();
} else {
return const NoData();
}
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return DynamicPanel(item: list[index]);
},
childCount: list.length,
),
);
}
},
);
} else {
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {},
);
}
} else {
// 骨架屏
return skeleton();
}
},
),
],
);
}
Widget skeleton() {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const DynamicCardSkeleton();
}, childCount: 5),
);
}
}
class _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
_MySliverPersistentHeaderDelegate({required this.child});
final double _minExtent = 50;
final double _maxExtent = 50;
final Widget child;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return child;
}
@override
double get maxExtent => _maxExtent;
@override
double get minExtent => _minExtent;
@override
bool shouldRebuild(covariant _MySliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}

View File

@ -3,13 +3,13 @@ import 'dart:async';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/plugin/pl_popup/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/utils/route_push.dart';
@ -18,6 +18,7 @@ import 'package:pilipala/utils/storage.dart';
import '../mine/controller.dart';
import 'controller.dart';
import 'widgets/dynamic_panel.dart';
import 'up_dynamic/route_panel.dart';
import 'widgets/up_panel.dart';
class DynamicsPage extends StatefulWidget {
@ -202,7 +203,21 @@ class _DynamicsPageState extends State<DynamicsPage>
}
Map data = snapshot.data;
if (data['status']) {
return Obx(() => UpPanel(_dynamicsController.upData.value));
return Obx(
() => UpPanel(
upData: _dynamicsController.upData.value,
onClickUpCb: (data) {
// _dynamicsController.onTapUp(data);
Navigator.push(
context,
PlPopupRoute(
child: OverlayPanel(
ctr: _dynamicsController, upInfo: data),
),
);
},
),
);
} else {
return const SliverToBoxAdapter(
child: SizedBox(height: 80),

View File

@ -1,11 +1,11 @@
// 内容
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/plugin/pl_gallery/index.dart';
import 'rich_node_panel.dart';
// ignore: must_be_immutable
@ -59,17 +59,15 @@ class _ContentState extends State<Content> {
(pictureItem.height != null && pictureItem.width != null
? pictureItem.height! / pictureItem.width!
: 1);
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: 0, imgList: picList);
},
);
return Hero(
tag: pictureItem.url!,
placeholderBuilder:
(BuildContext context, Size heroSize, Widget child) {
return child;
},
child: Container(
child: GestureDetector(
onTap: () => onPreviewImg(picList, 1, context),
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
width: box.maxWidth / 2,
@ -91,7 +89,9 @@ class _ContentState extends State<Content> {
)
: const SizedBox(),
],
)),
),
),
),
);
},
),
@ -102,26 +102,23 @@ class _ContentState extends State<Content> {
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(pics[i].url!);
}
for (var i = 0; i < len; i++) {
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
double maxWidth = box.maxWidth.truncateToDouble();
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
},
child: NetworkImgLayer(
src: pics[i].url,
width: maxWidth,
height: maxWidth,
origAspectRatio:
pics[i].width!.toInt() / pics[i].height!.toInt(),
return Hero(
tag: picList[i],
child: GestureDetector(
onTap: () => onPreviewImg(picList, i, context),
child: NetworkImgLayer(
src: pics[i].url,
width: maxWidth,
height: maxWidth,
origAspectRatio:
pics[i].width!.toInt() / pics[i].height!.toInt(),
),
),
);
},
@ -163,6 +160,43 @@ class _ContentState extends State<Content> {
);
}
void onPreviewImg(picList, initIndex, context) {
Navigator.of(context).push(
HeroDialogRoute<void>(
builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList,
initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
),
),
);
}
@override
Widget build(BuildContext context) {
TextStyle authorStyle =

View File

@ -1,9 +1,47 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/plugin/pl_gallery/index.dart';
void onPreviewImg(currentUrl, picList, initIndex, context) {
Navigator.of(context).push(
HeroDialogRoute<void>(
builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList,
initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
),
),
);
}
Widget picWidget(item, context) {
String type = item.modules.moduleDynamic.major.type;
@ -21,25 +59,25 @@ Widget picWidget(item, context) {
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(pictures[i].src ?? pictures[i].url);
}
for (var i = 0; i < len; i++) {
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
return Hero(
tag: picList[i],
placeholderBuilder:
(BuildContext context, Size heroSize, Widget child) {
return child;
},
child: NetworkImgLayer(
src: pictures[i].src ?? pictures[i].url,
width: box.maxWidth,
height: box.maxWidth,
child: GestureDetector(
onTap: () => onPreviewImg(picList[i], picList, i, context),
child: NetworkImgLayer(
src: pictures[i].src ?? pictures[i].url,
width: box.maxWidth,
height: box.maxWidth,
),
),
// ),
);
},
),

View File

@ -4,13 +4,18 @@ import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/dynamics/controller.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart';
class UpPanel extends StatefulWidget {
final FollowUpModel upData;
const UpPanel(this.upData, {Key? key}) : super(key: key);
final Function? onClickUpCb;
const UpPanel({
super.key,
required this.upData,
this.onClickUpCb,
});
@override
State<UpPanel> createState() => _UpPanelState();
@ -33,27 +38,25 @@ class _UpPanelState extends State<UpPanel> {
void onClickUp(data, i) {
currentMid = data.mid;
Get.find<DynamicsController>().mid.value = data.mid;
Get.find<DynamicsController>().upInfo.value = data;
Get.find<DynamicsController>().onSelectUp(data.mid);
int liveLen = liveList.length;
int upLen = upList.length;
double itemWidth = contentWidth + itemPadding.horizontal;
double screenWidth = MediaQuery.sizeOf(context).width;
double moveDistance = 0.0;
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
}
data.hasUpdate = false;
scrollController.animateTo(
moveDistance,
duration: const Duration(milliseconds: 200),
curve: Curves.linear,
);
setState(() {});
widget.onClickUpCb?.call(data);
// int liveLen = liveList.length;
// int upLen = upList.length;
// double itemWidth = contentWidth + itemPadding.horizontal;
// double screenWidth = MediaQuery.sizeOf(context).width;
// double moveDistance = 0.0;
// if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
// } else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
// moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
// } else {
// moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
// }
// data.hasUpdate = false;
// scrollController.animateTo(
// moveDistance,
// duration: const Duration(milliseconds: 200),
// curve: Curves.linear,
// );
// setState(() {});
}
@override

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -54,9 +55,16 @@ class _HomePageState extends State<HomePage>
toolbarHeight: 0,
elevation: 0,
backgroundColor: Colors.transparent,
systemOverlayStyle: Theme.of(context).brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
systemOverlayStyle: Platform.isAndroid
? SystemUiOverlayStyle(
statusBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
)
: Theme.of(context).brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
),
body: Column(
children: [

View File

@ -27,6 +27,7 @@ class MainController extends GetxController {
RxBool userLogin = false.obs;
late Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
late bool enableGradientBg;
bool imgPreviewStatus = false;
@override
void onInit() {

View File

@ -236,9 +236,11 @@ class FavFolderItem extends StatelessWidget {
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(), 'heroTag': heroTag}),
onTap: () => Get.toNamed('/favDetail', arguments: item, parameters: {
'mediaId': item!.id.toString(),
'heroTag': heroTag,
'isOwner': '1',
}),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -9,12 +9,14 @@ class MemberArchiveController extends GetxController {
int pn = 1;
int count = 0;
RxMap<String, String> currentOrder = <String, String>{}.obs;
List<Map<String, String>> orderList = [
RxList<Map<String, String>> orderList = [
{'type': 'pubdate', 'label': '最新发布'},
{'type': 'click', 'label': '最多播放'},
{'type': 'stow', 'label': '最多收藏'},
];
{'type': 'charge', 'label': '充电专属'},
].obs;
RxList<VListItemModel> archivesList = <VListItemModel>[].obs;
RxBool isLoading = false.obs;
@override
void onInit() {
@ -27,6 +29,8 @@ class MemberArchiveController extends GetxController {
Future getMemberArchive(type) async {
if (type == 'init') {
pn = 1;
archivesList.clear();
isLoading.value = true;
}
var res = await MemberHttp.memberArchive(
mid: mid,
@ -43,6 +47,7 @@ class MemberArchiveController extends GetxController {
count = res['data'].page['count'];
pn += 1;
}
isLoading.value = false;
return res;
}

View File

@ -1,6 +1,8 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/utils/utils.dart';
import '../../common/widgets/http_error.dart';
@ -47,14 +49,29 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
appBar: AppBar(
titleSpacing: 0,
centerTitle: false,
title: Text('他的投稿', style: Theme.of(context).textTheme.titleMedium),
title: Obx(
() => Text(
'他的投稿 - ${_memberArchivesController.currentOrder['label']}',
style: Theme.of(context).textTheme.titleMedium),
),
actions: [
Obx(
() => TextButton.icon(
icon: const Icon(Icons.sort, size: 20),
onPressed: _memberArchivesController.toggleSort,
label: Text(_memberArchivesController.currentOrder['label']!),
),
// Obx(
PopupMenuButton(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
// 这里处理选择逻辑
_memberArchivesController.currentOrder.value = value;
_memberArchivesController.getMemberArchive('init');
},
itemBuilder: (BuildContext context) =>
_memberArchivesController.orderList.map(
(e) {
return PopupMenuItem(
value: e,
child: Text(e['label']!),
);
},
).toList(),
),
const SizedBox(width: 6),
],
@ -85,7 +102,14 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
childCount: list.length,
),
)
: const SliverToBoxAdapter(),
: _memberArchivesController.isLoading.value
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
)
: const NoData(),
);
} else {
return HttpError(
@ -100,7 +124,11 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
);
}
} else {
return const SliverToBoxAdapter();
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
},
),

View File

@ -209,7 +209,7 @@ class LikeItem extends StatelessWidget {
style: TextStyle(color: outline),
),
TextSpan(
text: '赞了我的评论',
text: '赞了我的${item.item!.business}',
style: TextStyle(color: outline),
),
])),

View File

@ -1,50 +0,0 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.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;
String currentImgUrl = '';
requestPermission() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.storage,
// Permission.photos
].request();
statuses[Permission.storage].toString();
// final photosInfo = statuses[Permission.photos].toString();
}
// 图片分享
void onShareImg() async {
SmartDialog.showLoading();
var response = await Dio().get(imgList[initialPage.value],
options: Options(responseType: ResponseType.bytes));
final temp = await getTemporaryDirectory();
SmartDialog.dismiss();
String imgName =
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
var path = '${temp.path}/$imgName';
File(path).writeAsBytesSync(response.data);
Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]);
}
void onChange(int index) {
initialPage.value = index;
currentPage.value = index + 1;
currentImgUrl = imgList[index];
}
}

View File

@ -1,290 +0,0 @@
// ignore_for_file: library_private_types_in_public_api
import 'dart:io';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:extended_image/extended_image.dart';
import 'package:pilipala/utils/download.dart';
import 'controller.dart';
import 'package:status_bar_control/status_bar_control.dart';
typedef DoubleClickAnimationListener = void Function();
class ImagePreview extends StatefulWidget {
final int? initialPage;
final List<String>? imgList;
const ImagePreview({
Key? key,
this.initialPage,
this.imgList,
}) : 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];
bool _dismissDisabled = false;
@override
void initState() {
super.initState();
_previewController.initialPage.value = widget.initialPage!;
_previewController.currentPage.value = widget.initialPage! + 1;
_previewController.imgList.value = widget.imgList!;
_previewController.currentImgUrl = widget.imgList![widget.initialPage!];
// animationController = AnimationController(
// vsync: this, duration: const Duration(milliseconds: 400));
setStatusBar();
_doubleClickAnimationController = AnimationController(
duration: const Duration(milliseconds: 250), vsync: this);
}
onOpenMenu() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () {
_previewController.onShareImg();
Get.back();
},
dense: true,
title: const Text('分享', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () {
Clipboard.setData(
ClipboardData(text: _previewController.currentImgUrl))
.then((value) {
Get.back();
SmartDialog.showToast('已复制到粘贴板');
}).catchError((err) {
SmartDialog.showNotify(
msg: err.toString(),
notifyType: NotifyType.error,
);
});
},
dense: true,
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () {
Get.back();
DownloadUtils.downloadImg(_previewController.currentImgUrl);
},
dense: true,
title: const Text('保存到手机', style: TextStyle(fontSize: 14)),
),
],
),
);
},
);
}
// 隐藏状态栏,避免遮挡图片内容
setStatusBar() async {
if (Platform.isIOS || Platform.isAndroid) {
await StatusBarControl.setHidden(true,
animation: StatusBarAnimation.SLIDE);
}
}
@override
void dispose() {
// animationController.dispose();
try {
StatusBarControl.setHidden(false, animation: StatusBarAnimation.SLIDE);
} catch (_) {}
_doubleClickAnimationController.dispose();
clearGestureDetailsCache();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
primary: false,
extendBody: true,
appBar: AppBar(
primary: false,
toolbarHeight: 0,
backgroundColor: Colors.black,
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
body: Stack(
children: [
GestureDetector(
onLongPress: () => onOpenMenu(),
child: ExtendedImageGesturePageView.builder(
controller: ExtendedPageController(
initialPage: _previewController.initialPage.value,
pageSpacing: 0,
),
onPageChanged: (int index) => _previewController.onChange(index),
canScrollPage: (GestureDetails? gestureDetails) =>
gestureDetails!.totalScale! <= 1.0,
itemCount: widget.imgList!.length,
itemBuilder: (BuildContext context, int index) {
return ExtendedImage.network(
widget.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]) {
setState(() {
_dismissDisabled = true;
});
end = doubleTapScales[1];
} else {
setState(() {
_dismissDisabled = false;
});
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();
},
// ignore: body_might_complete_normally_nullable
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,
color: Colors.white,
),
),
// 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,
);
},
);
},
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: EdgeInsets.only(
left: 20,
right: 20,
bottom: MediaQuery.of(context).padding.bottom + 30),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black87,
],
tileMode: TileMode.mirror,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
widget.imgList!.length > 1
? Obx(
() => Text.rich(
textAlign: TextAlign.center,
TextSpan(
style: const TextStyle(
color: Colors.white, fontSize: 16),
children: [
TextSpan(
text: _previewController.currentPage
.toString()),
const TextSpan(text: ' / '),
TextSpan(
text:
widget.imgList!.length.toString()),
]),
),
)
: const SizedBox(),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close, color: Colors.white),
),
],
)),
),
],
),
);
}
}

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -39,9 +40,16 @@ class _RankPageState extends State<RankPage>
toolbarHeight: 0,
elevation: 0,
backgroundColor: Colors.transparent,
systemOverlayStyle: Theme.of(context).brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
systemOverlayStyle: Platform.isAndroid
? SystemUiOverlayStyle(
statusBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
)
: Theme.of(context).brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
),
body: Column(
children: [

View File

@ -162,7 +162,7 @@ class VideoDetailController extends GetxController
);
}
showReplyReplyPanel(oid, fRpid, firstFloor) {
showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {
replyReplyBottomSheetCtr =
scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return VideoReplyReplyPanel(
@ -175,6 +175,8 @@ class VideoDetailController extends GetxController
replyType: ReplyType.video,
source: 'videoDetail',
sheetHeight: sheetHeight.value,
currentReply: currentReply,
loadMore: loadMore,
);
});
replyReplyBottomSheetCtr?.closed.then((value) {

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/constants.dart';
@ -196,45 +197,38 @@ class VideoIntroController extends GetxController {
SmartDialog.showToast('账号未登录');
return;
}
if (hasCoin.value) {
SmartDialog.showToast('已投过币了');
return;
}
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('选择投币个数'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
content: StatefulBuilder(builder: (context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [1, 2]
.map(
(e) => RadioListTile(
value: e,
title: Text('$e枚'),
groupValue: _tempThemeValue,
onChanged: (value) async {
_tempThemeValue = value!;
setState(() {});
var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue);
if (res['status']) {
SmartDialog.showToast('投币成功');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
content: Column(
mainAxisSize: MainAxisSize.min,
children: [1, 2]
.map(
(e) => ListTile(
title: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text('$e '),
),
)
.toList(),
);
}),
onTap: () async {
var res =
await VideoHttp.coinVideo(bvid: bvid, multiply: e);
if (res['status']) {
SmartDialog.showToast('投币成功');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + e;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
),
)
.toList(),
),
);
});
}

View File

@ -112,7 +112,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
}
// 展示二级回复
void replyReply(replyItem) {
void replyReply(replyItem, currentReply, loadMore) {
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
if (replyItem != null) {
@ -120,7 +120,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
videoDetailCtr.fRpid = replyItem.rpid!;
videoDetailCtr.firstFloor = replyItem;
videoDetailCtr.showReplyReplyPanel(
replyItem.oid, replyItem.rpid!, replyItem);
replyItem.oid, replyItem.rpid!, replyItem, currentReply, loadMore);
}
}
@ -232,8 +232,10 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
.replyList[index],
showReplyRow: true,
replyLevel: replyLevel,
replyReply: (replyItem) =>
replyReply(replyItem),
replyReply: (replyItem, currentReply,
loadMore) =>
replyReply(replyItem, currentReply,
loadMore),
replyType: ReplyType.video,
);
}

View File

@ -1,4 +1,5 @@
import 'package:appscheme/appscheme.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -9,12 +10,12 @@ import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/index.dart';
import 'package:pilipala/plugin/pl_gallery/index.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/url_utils.dart';
import 'package:pilipala/utils/utils.dart';
@ -47,7 +48,7 @@ class ReplyItem extends StatelessWidget {
onTap: () {
feedBack();
if (replyReply != null) {
replyReply!(replyItem);
replyReply!(replyItem, null, replyItem!.replies!.isNotEmpty);
}
},
onLongPress: () {
@ -360,9 +361,13 @@ class ReplyItemRow extends StatelessWidget {
for (int i = 0; i < replies!.length; i++) ...[
InkWell(
// 一楼点击评论展开评论详情
// onTap: () {
// replyReply?.call(replyItem);
// },
onTap: () {
replyReply?.call(
replyItem,
replies![i],
replyItem!.replies!.isNotEmpty,
);
},
onLongPress: () {
feedBack();
showModalBottomSheet(
@ -430,7 +435,8 @@ class ReplyItemRow extends StatelessWidget {
if (extraRow == 1)
InkWell(
// 一楼点击【共xx条回复】展开评论详情
onTap: () => replyReply!(replyItem),
onTap: () => replyReply?.call(replyItem, null, true),
onLongPress: () => {},
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(8, 5, 8, 8),
@ -533,9 +539,59 @@ InlineSpan buildContent(
spanChilds.add(
TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap = () =>
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem),
// recognizer: TapGestureRecognizer()
// ..onTap = () => replyReply?.call(
// replyItem.root == 0 ? replyItem : fReplyItem,
// replyItem,
// fReplyItem!.replies!.isNotEmpty,
// ),
),
);
}
void onPreviewImg(picList, initIndex) {
final MainController mainController = Get.find<MainController>();
mainController.imgPreviewStatus = true;
Navigator.of(context).push(
HeroDialogRoute<void>(
builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList,
initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
final MainController mainController =
Get.find<MainController>();
mainController.imgPreviewStatus = false;
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
onDismissed: (int value) {
print('onDismissed');
final MainController mainController = Get.find<MainController>();
mainController.imgPreviewStatus = false;
},
),
),
);
}
@ -831,38 +887,33 @@ InlineSpan buildContent(
.truncateToDouble();
} catch (_) {}
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (BuildContext context) {
return ImagePreview(initialPage: 0, imgList: picList);
},
);
},
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
width: box.maxWidth / 2,
height: height,
child: Stack(
children: [
Positioned.fill(
child: NetworkImgLayer(
src: pictureItem['img_src'],
width: box.maxWidth / 2,
height: height,
return Hero(
tag: picList[0],
child: GestureDetector(
onTap: () => onPreviewImg(picList, 0),
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
width: box.maxWidth / 2,
height: height,
child: Stack(
children: [
Positioned.fill(
child: NetworkImgLayer(
src: picList[0],
width: box.maxWidth / 2,
height: height,
),
),
),
height > Get.size.height * 0.9
? const PBadge(
text: '长图',
right: 8,
bottom: 8,
)
: const SizedBox(),
],
height > Get.size.height * 0.9
? const PBadge(
text: '长图',
right: 8,
bottom: 8,
)
: const SizedBox(),
],
),
),
),
);
@ -874,25 +925,22 @@ InlineSpan buildContent(
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(content.pictures[i]['img_src']);
}
for (var i = 0; i < len; i++) {
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
},
child: NetworkImgLayer(
src: content.pictures[i]['img_src'],
width: box.maxWidth,
height: box.maxWidth,
origAspectRatio: content.pictures[i]['img_width'] /
content.pictures[i]['img_height']),
return Hero(
tag: picList[i],
child: GestureDetector(
onTap: () => onPreviewImg(picList, i),
child: NetworkImgLayer(
src: picList[i],
width: box.maxWidth,
height: box.maxWidth,
origAspectRatio: content.pictures[i]['img_width'] /
content.pictures[i]['img_height']),
),
);
},
),

View File

@ -26,7 +26,7 @@ class VideoReplyReplyController extends GetxController {
currentPage = 0;
}
Future queryReplyList({type = 'init'}) async {
Future queryReplyList({type = 'init', currentReply}) async {
if (type == 'init') {
currentPage = 0;
}
@ -63,6 +63,17 @@ class VideoReplyReplyController extends GetxController {
// res['data'].replies.addAll(replyList);
}
}
if (replyList.isNotEmpty && currentReply != null) {
int indexToRemove =
replyList.indexWhere((item) => currentReply.rpid == item.rpid);
// 如果找到了指定ID的项则移除
if (indexToRemove != -1) {
replyList.removeAt(indexToRemove);
}
if (currentPage == 1 && type == 'init') {
replyList.insert(0, currentReply);
}
}
isLoadingMore = false;
return res;
}

View File

@ -20,6 +20,8 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.source,
this.replyType,
this.sheetHeight,
this.currentReply,
this.loadMore = true,
super.key,
});
final int? oid;
@ -29,6 +31,8 @@ class VideoReplyReplyPanel extends StatefulWidget {
final String? source;
final ReplyType? replyType;
final double? sheetHeight;
final dynamic currentReply;
final bool loadMore;
@override
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
@ -63,7 +67,9 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
},
);
_futureBuilderFuture = _videoReplyReplyController.queryReplyList();
_futureBuilderFuture = _videoReplyReplyController.queryReplyList(
currentReply: widget.currentReply,
);
}
void replyReply(replyItem) {}
@ -107,7 +113,9 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
onRefresh: () async {
setState(() {});
_videoReplyReplyController.currentPage = 0;
return await _videoReplyReplyController.queryReplyList();
return await _videoReplyReplyController.queryReplyList(
currentReply: widget.currentReply,
);
},
child: CustomScrollView(
controller: _videoReplyReplyController.scrollController,
@ -134,84 +142,102 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
),
),
],
FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
// 请求成功
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index ==
_videoReplyReplyController
.replyList.length) {
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
height: MediaQuery.of(context)
.padding
.bottom +
100,
child: Center(
child: Obx(
() => Text(
widget.loadMore
? FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
// 请求成功
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index ==
_videoReplyReplyController
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
.replyList.length) {
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
height: MediaQuery.of(context)
.padding
.bottom +
100,
child: Center(
child: Obx(
() => Text(
_videoReplyReplyController
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
),
),
),
);
} else {
return ReplyItem(
replyItem: _videoReplyReplyController
.replyList[index],
replyLevel: '2',
showReplyRow: false,
addReply: (replyItem) {
_videoReplyReplyController.replyList
.add(replyItem);
);
} else {
return ReplyItem(
replyItem:
_videoReplyReplyController
.replyList[index],
replyLevel: '2',
showReplyRow: false,
addReply: (replyItem) {
_videoReplyReplyController
.replyList
.add(replyItem);
},
replyType: widget.replyType,
replyReply: (replyItem) =>
replyReply(replyItem),
);
}
},
replyType: widget.replyType,
replyReply: (replyItem) =>
replyReply(replyItem),
);
}
},
childCount: _videoReplyReplyController
.replyList.length +
1,
childCount: _videoReplyReplyController
.replyList.length +
1,
),
),
);
} else {
// 请求错误
return HttpError(
errMsg: data?['msg'] ?? '请求错误',
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return const VideoReplySkeleton();
}, childCount: 8),
);
}
},
)
: SliverToBoxAdapter(
child: SizedBox(
height: 200,
child: Center(
child: Text(
'还没有评论',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
),
);
} else {
// 请求错误
return HttpError(
errMsg: data?['msg'] ?? '请求错误',
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return const VideoReplySkeleton();
}, childCount: 8),
);
}
},
)
),
)
],
),
),

View File

@ -15,6 +15,7 @@ import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/bangumi/introduction/index.dart';
import 'package:pilipala/pages/danmaku/view.dart';
import 'package:pilipala/pages/main/index.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';
@ -240,6 +241,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
// 离开当前页面时
void didPushNext() async {
final MainController mainController = Get.find<MainController>();
if (mainController.imgPreviewStatus) {
return;
}
/// 开启
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)
as bool) {
@ -259,6 +265,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
// 返回当前页面时
void didPopNext() async {
final MainController mainController = Get.find<MainController>();
if (mainController.imgPreviewStatus) {
return;
}
if (plPlayerController != null &&
plPlayerController!.videoPlayerController != null) {
setState(() {

View File

@ -214,7 +214,7 @@ class SessionItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo.mid);
final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo?.mid ?? 0);
final content = sessionItem.lastMsg.content;
final msgStatus = sessionItem.lastMsg.msgStatus;
@ -228,7 +228,7 @@ class SessionItem extends StatelessWidget {
'talkerId': sessionItem.talkerId.toString(),
'name': sessionItem.accountInfo.name,
'face': sessionItem.accountInfo.face,
'mid': sessionItem.accountInfo.mid.toString(),
'mid': (sessionItem.accountInfo?.mid ?? 0).toString(),
'heroTag': heroTag,
},
);

View File

@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
/// A widget used to dismiss its [child].
///
/// Similar to [Dismissible] with some adjustments.
class CustomDismissible extends StatefulWidget {
const CustomDismissible({
required this.child,
this.onDismissed,
this.dismissThreshold = 0.2,
this.enabled = true,
Key? key,
}) : super(key: key);
final Widget child;
final double dismissThreshold;
final VoidCallback? onDismissed;
final bool enabled;
@override
State<CustomDismissible> createState() => _CustomDismissibleState();
}
class _CustomDismissibleState extends State<CustomDismissible>
with SingleTickerProviderStateMixin {
late AnimationController _animateController;
late Animation<Offset> _moveAnimation;
late Animation<double> _scaleAnimation;
late Animation<Decoration> _opacityAnimation;
double _dragExtent = 0;
bool _dragUnderway = false;
bool get _isActive => _dragUnderway || _animateController.isAnimating;
@override
void initState() {
super.initState();
_animateController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_updateMoveAnimation();
}
@override
void dispose() {
_animateController.dispose();
super.dispose();
}
void _updateMoveAnimation() {
final double end = _dragExtent.sign;
_moveAnimation = _animateController.drive(
Tween<Offset>(
begin: Offset.zero,
end: Offset(0, end),
),
);
_scaleAnimation = _animateController.drive(Tween<double>(
begin: 1,
end: 0.5,
));
_opacityAnimation = DecorationTween(
begin: const BoxDecoration(color: Color(0xFF000000)),
end: const BoxDecoration(color: Color(0x00000000)),
).animate(_animateController);
}
void _handleDragStart(DragStartDetails details) {
_dragUnderway = true;
if (_animateController.isAnimating) {
_dragExtent =
_animateController.value * context.size!.height * _dragExtent.sign;
_animateController.stop();
} else {
_dragExtent = 0.0;
_animateController.value = 0.0;
}
setState(_updateMoveAnimation);
}
void _handleDragUpdate(DragUpdateDetails details) {
if (!_isActive || _animateController.isAnimating) {
return;
}
final double delta = details.primaryDelta!;
final double oldDragExtent = _dragExtent;
if (_dragExtent + delta < 0) {
_dragExtent += delta;
} else if (_dragExtent + delta > 0) {
_dragExtent += delta;
}
if (oldDragExtent.sign != _dragExtent.sign) {
setState(_updateMoveAnimation);
}
if (!_animateController.isAnimating) {
_animateController.value = _dragExtent.abs() / context.size!.height;
}
}
void _handleDragEnd(DragEndDetails details) {
if (!_isActive || _animateController.isAnimating) {
return;
}
_dragUnderway = false;
if (_animateController.isCompleted) {
return;
}
if (!_animateController.isDismissed) {
// if the dragged value exceeded the dismissThreshold, call onDismissed
// else animate back to initial position.
if (_animateController.value > widget.dismissThreshold) {
widget.onDismissed?.call();
} else {
_animateController.reverse();
}
}
}
@override
Widget build(BuildContext context) {
final Widget content = DecoratedBoxTransition(
decoration: _opacityAnimation,
child: SlideTransition(
position: _moveAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: widget.child,
),
),
);
return GestureDetector(
behavior: HitTestBehavior.translucent,
onVerticalDragStart: widget.enabled ? _handleDragStart : null,
onVerticalDragUpdate: widget.enabled ? _handleDragUpdate : null,
onVerticalDragEnd: widget.enabled ? _handleDragEnd : null,
child: content,
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
/// A [PageRoute] with a semi transparent background.
///
/// Similar to calling [showDialog] except it can be used with a [Navigator] to
/// show a [Hero] animation.
class HeroDialogRoute<T> extends PageRoute<T> {
HeroDialogRoute({
required this.builder,
this.onBackgroundTap,
}) : super();
final WidgetBuilder builder;
/// Called when the background is tapped.
final VoidCallback? onBackgroundTap;
@override
bool get opaque => false;
@override
bool get barrierDismissible => true;
@override
String? get barrierLabel => null;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
@override
bool get maintainState => true;
@override
Color? get barrierColor => null;
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
child: child,
);
}
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
final Widget child = builder(context);
final Widget result = Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: child,
);
return result;
}
}

View File

@ -0,0 +1,6 @@
library pl_gallery;
export './hero_dialog_route.dart';
export './custom_dismissible.dart';
export './interactiveviewer_gallery.dart';
export './interactive_viewer_boundary.dart';

View File

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
/// A callback for the [InteractiveViewerBoundary] that is called when the scale
/// changed.
typedef ScaleChanged = void Function(double scale);
/// Builds an [InteractiveViewer] and provides callbacks that are called when a
/// horizontal boundary has been hit.
///
/// The callbacks are called when an interaction ends by listening to the
/// [InteractiveViewer.onInteractionEnd] callback.
class InteractiveViewerBoundary extends StatefulWidget {
const InteractiveViewerBoundary({
required this.child,
required this.boundaryWidth,
this.controller,
this.onScaleChanged,
this.onLeftBoundaryHit,
this.onRightBoundaryHit,
this.onNoBoundaryHit,
this.maxScale,
this.minScale,
Key? key,
}) : super(key: key);
final Widget child;
/// The max width this widget can have.
///
/// If the [InteractiveViewer] can take up the entire screen width, this
/// should be set to `MediaQuery.of(context).size.width`.
final double boundaryWidth;
/// The [TransformationController] for the [InteractiveViewer].
final TransformationController? controller;
/// Called when the scale changed after an interaction ended.
final ScaleChanged? onScaleChanged;
/// Called when the left boundary has been hit after an interaction ended.
final VoidCallback? onLeftBoundaryHit;
/// Called when the right boundary has been hit after an interaction ended.
final VoidCallback? onRightBoundaryHit;
/// Called when no boundary has been hit after an interaction ended.
final VoidCallback? onNoBoundaryHit;
final double? maxScale;
final double? minScale;
@override
InteractiveViewerBoundaryState createState() =>
InteractiveViewerBoundaryState();
}
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary> {
TransformationController? _controller;
double? _scale;
@override
void initState() {
super.initState();
_controller = widget.controller ?? TransformationController();
}
@override
void dispose() {
_controller!.dispose();
super.dispose();
}
void _updateBoundaryDetection() {
final double scale = _controller!.value.row0[0];
if (_scale != scale) {
// the scale changed
_scale = scale;
widget.onScaleChanged?.call(scale);
}
if (scale <= 1.01) {
// cant hit any boundaries when the child is not scaled
return;
}
final double xOffset = _controller!.value.row0[3];
final double boundaryWidth = widget.boundaryWidth;
final double boundaryEnd = boundaryWidth * scale;
final double xPos = boundaryEnd + xOffset;
if (boundaryEnd.round() == xPos.round()) {
// left boundary hit
widget.onLeftBoundaryHit?.call();
} else if (boundaryWidth.round() == xPos.round()) {
// right boundary hit
widget.onRightBoundaryHit?.call();
} else {
widget.onNoBoundaryHit?.call();
}
}
@override
Widget build(BuildContext context) {
return InteractiveViewer(
maxScale: widget.maxScale!,
minScale: widget.minScale!,
transformationController: _controller,
onInteractionEnd: (_) => _updateBoundaryDetection(),
child: widget.child,
);
}
}

View File

@ -0,0 +1,399 @@
library interactiveviewer_gallery;
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pilipala/utils/download.dart';
import 'package:share_plus/share_plus.dart';
import 'package:status_bar_control/status_bar_control.dart';
import 'custom_dismissible.dart';
import 'interactive_viewer_boundary.dart';
/// Builds a carousel controlled by a [PageView] for the tweet media sources.
///
/// Used for showing a full screen view of the [TweetMedia] sources.
///
/// The sources can be panned and zoomed interactively using an
/// [InteractiveViewer].
/// An [InteractiveViewerBoundary] is used to detect when the boundary of the
/// source is hit after zooming in to disable or enable the swiping gesture of
/// the [PageView].
///
typedef IndexedFocusedWidgetBuilder = Widget Function(
BuildContext context, int index, bool isFocus, bool enablePageView);
typedef IndexedTagStringBuilder = String Function(int index);
class InteractiveviewerGallery<T> extends StatefulWidget {
const InteractiveviewerGallery({
required this.sources,
required this.initIndex,
required this.itemBuilder,
this.maxScale = 4.5,
this.minScale = 1.0,
this.onPageChanged,
this.onDismissed,
Key? key,
}) : super(key: key);
/// The sources to show.
final List<T> sources;
/// The index of the first source in [sources] to show.
final int initIndex;
/// The item content
final IndexedFocusedWidgetBuilder itemBuilder;
final double maxScale;
final double minScale;
final ValueChanged<int>? onPageChanged;
final ValueChanged<int>? onDismissed;
@override
State<InteractiveviewerGallery> createState() =>
_InteractiveviewerGalleryState();
}
class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
with SingleTickerProviderStateMixin {
PageController? _pageController;
TransformationController? _transformationController;
/// The controller to animate the transformation value of the
/// [InteractiveViewer] when it should reset.
late AnimationController _animationController;
Animation<Matrix4>? _animation;
/// `true` when an source is zoomed in and not at the at a horizontal boundary
/// to disable the [PageView].
bool _enablePageView = true;
/// `true` when an source is zoomed in to disable the [CustomDismissible].
bool _enableDismiss = true;
late Offset _doubleTapLocalPosition;
int? currentIndex;
@override
void initState() {
super.initState();
_pageController = PageController(initialPage: widget.initIndex);
_transformationController = TransformationController();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
)
..addListener(() {
_transformationController!.value =
_animation?.value ?? Matrix4.identity();
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed && !_enableDismiss) {
setState(() {
_enableDismiss = true;
});
}
});
currentIndex = widget.initIndex;
setStatusBar();
}
setStatusBar() async {
if (Platform.isIOS || Platform.isAndroid) {
await StatusBarControl.setHidden(true,
animation: StatusBarAnimation.FADE);
}
}
@override
void dispose() {
_pageController!.dispose();
_animationController.dispose();
try {
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
} catch (_) {}
super.dispose();
}
/// When the source gets scaled up, the swipe up / down to dismiss gets
/// disabled.
///
/// When the scale resets, the dismiss and the page view swiping gets enabled.
void _onScaleChanged(double scale) {
final bool initialScale = scale <= widget.minScale;
if (initialScale) {
if (!_enableDismiss) {
setState(() {
_enableDismiss = true;
});
}
if (!_enablePageView) {
setState(() {
_enablePageView = true;
});
}
} else {
if (_enableDismiss) {
setState(() {
_enableDismiss = false;
});
}
if (_enablePageView) {
setState(() {
_enablePageView = false;
});
}
}
}
/// When the left boundary has been hit after scaling up the source, the page
/// view swiping gets enabled if it has a page to swipe to.
void _onLeftBoundaryHit() {
if (!_enablePageView && _pageController!.page!.floor() > 0) {
setState(() {
_enablePageView = true;
});
}
}
/// When the right boundary has been hit after scaling up the source, the page
/// view swiping gets enabled if it has a page to swipe to.
void _onRightBoundaryHit() {
if (!_enablePageView &&
_pageController!.page!.floor() < widget.sources.length - 1) {
setState(() {
_enablePageView = true;
});
}
}
/// When the source has been scaled up and no horizontal boundary has been hit,
/// the page view swiping gets disabled.
void _onNoBoundaryHit() {
if (_enablePageView) {
setState(() {
_enablePageView = false;
});
}
}
/// When the page view changed its page, the source will animate back into the
/// original scale if it was scaled up.
///
/// Additionally the swipe up / down to dismiss gets enabled.
void _onPageChanged(int page) {
setState(() {
currentIndex = page;
});
widget.onPageChanged?.call(page);
if (_transformationController!.value != Matrix4.identity()) {
// animate the reset for the transformation of the interactive viewer
_animation = Matrix4Tween(
begin: _transformationController!.value,
end: Matrix4.identity(),
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animationController.forward(from: 0);
}
}
@override
Widget build(BuildContext context) {
return InteractiveViewerBoundary(
controller: _transformationController,
boundaryWidth: MediaQuery.of(context).size.width,
onScaleChanged: _onScaleChanged,
onLeftBoundaryHit: _onLeftBoundaryHit,
onRightBoundaryHit: _onRightBoundaryHit,
onNoBoundaryHit: _onNoBoundaryHit,
maxScale: widget.maxScale,
minScale: widget.minScale,
child: Stack(children: [
CustomDismissible(
onDismissed: () {
Navigator.of(context).pop();
widget.onDismissed?.call(_pageController!.page!.floor());
},
enabled: _enableDismiss,
child: PageView.builder(
onPageChanged: _onPageChanged,
controller: _pageController,
physics:
_enablePageView ? null : const NeverScrollableScrollPhysics(),
itemCount: widget.sources.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onDoubleTapDown: (TapDownDetails details) {
_doubleTapLocalPosition = details.localPosition;
},
onDoubleTap: onDoubleTap,
child: widget.itemBuilder(
context,
index,
index == currentIndex,
_enablePageView,
),
);
},
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.fromLTRB(
12, 8, 20, MediaQuery.of(context).padding.bottom + 8),
decoration: _enablePageView
? BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.3)
],
),
)
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () {
Navigator.of(context).pop();
widget.onDismissed?.call(_pageController!.page!.floor());
},
),
widget.sources.length > 1
? Text(
"${currentIndex! + 1}/${widget.sources.length}",
style: const TextStyle(color: Colors.white),
)
: const SizedBox(),
PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
value: 0,
onTap: () => onShareImg(widget.sources[currentIndex!]),
child: const Text("分享图片"),
),
PopupMenuItem(
value: 1,
onTap: () {
Clipboard.setData(ClipboardData(
text:
widget.sources[currentIndex!].toString()))
.then((value) {
SmartDialog.showToast('已复制到粘贴板');
}).catchError((err) {
SmartDialog.showNotify(
msg: err.toString(),
notifyType: NotifyType.error,
);
});
},
child: const Text("复制图片"),
),
PopupMenuItem(
value: 2,
onTap: () {
DownloadUtils.downloadImg(
widget.sources[currentIndex!]);
},
child: const Text("保存图片"),
),
];
},
child: const Icon(Icons.more_horiz, color: Colors.white),
),
],
),
),
),
]),
);
}
// 图片分享
void onShareImg(String imgUrl) async {
SmartDialog.showLoading();
var response = await Dio()
.get(imgUrl, options: Options(responseType: ResponseType.bytes));
final temp = await getTemporaryDirectory();
SmartDialog.dismiss();
String imgName =
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
var path = '${temp.path}/$imgName';
File(path).writeAsBytesSync(response.data);
Share.shareXFiles([XFile(path)], subject: imgUrl);
}
onDoubleTap() {
Matrix4 matrix = _transformationController!.value.clone();
double currentScale = matrix.row0.x;
double targetScale = widget.minScale;
if (currentScale <= widget.minScale) {
targetScale = widget.maxScale * 0.7;
}
double offSetX = targetScale == 1.0
? 0.0
: -_doubleTapLocalPosition.dx * (targetScale - 1);
double offSetY = targetScale == 1.0
? 0.0
: -_doubleTapLocalPosition.dy * (targetScale - 1);
matrix = Matrix4.fromList([
targetScale,
matrix.row1.x,
matrix.row2.x,
matrix.row3.x,
matrix.row0.y,
targetScale,
matrix.row2.y,
matrix.row3.y,
matrix.row0.z,
matrix.row1.z,
targetScale,
matrix.row3.z,
offSetX,
offSetY,
matrix.row2.w,
matrix.row3.w
]);
_animation = Matrix4Tween(
begin: _transformationController!.value,
end: matrix,
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animationController
.forward(from: 0)
.whenComplete(() => _onScaleChanged(targetScale));
}
}

View File

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
class PlPopupRoute extends PopupRoute<void> {
PlPopupRoute({
this.backgroudColor,
this.alignment = Alignment.center,
required this.child,
this.onClick,
});
/// backgroudColor
final Color? backgroudColor;
/// child'alignment, default value: [Alignment.center]
final Alignment alignment;
/// child
final Widget child;
/// backgroudView action
final Function? onClick;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
@override
bool get barrierDismissible => false;
@override
Color get barrierColor => Colors.black54;
@override
String? get barrierLabel => null;
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return child;
}
}

View File

@ -70,14 +70,6 @@ class Routes {
CustomGetPage(name: '/hot', page: () => const HotPage()),
// 视频详情
CustomGetPage(name: '/video', page: () => const VideoDetailPage()),
// 图片预览
// GetPage(
// name: '/preview',
// page: () => const ImagePreview(),
// transition: Transition.fade,
// transitionDuration: const Duration(milliseconds: 300),
// showCupertinoParallax: false,
// ),
//
CustomGetPage(name: '/webview', page: () => const WebviewPage()),
// 设置

View File

@ -1,8 +1,6 @@
import 'dart:io';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pilipala/models/model_owner.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/user/info.dart';
import '../models/common/gesture_mode.dart';
import 'global_data.dart';
@ -54,11 +52,8 @@ class GStrorage {
}
static void regAdapter() {
Hive.registerAdapter(OwnerAdapter());
Hive.registerAdapter(UserInfoDataAdapter());
Hive.registerAdapter(LevelInfoAdapter());
Hive.registerAdapter(HotSearchModelAdapter());
Hive.registerAdapter(HotSearchItemAdapter());
}
static Future<void> close() async {

View File

@ -441,22 +441,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.1"
extended_image:
dependency: "direct main"
description:
name: extended_image
sha256: d7f091d068fcac7246c4b22a84b8dac59a62e04d29a5c172710c696e67a22f94
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.2.0"
extended_image_library:
dependency: transitive
description:
name: extended_image_library
sha256: c9caee8fe9b6547bd41c960c4f2d1ef8e34321804de6a1777f1d614a24247ad6
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.4"
extended_list:
dependency: transitive
description:
@ -726,14 +710,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
http_client_helper:
dependency: transitive
description:
name: http_client_helper
sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
http_multi_server:
dependency: transitive
description:

View File

@ -49,7 +49,6 @@ dependencies:
# 图片
cached_network_image: ^3.3.0
extended_image: ^8.2.0
saver_gallery: ^3.0.1
# 存储