Merge branch 'design'

This commit is contained in:
guozhigq
2024-10-20 22:59:18 +08:00
36 changed files with 1353 additions and 640 deletions

View File

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

View File

@ -144,6 +144,7 @@ class EpisodeItem {
this.link,
this.longTitle,
this.pubTime,
this.pubdate,
this.pv,
this.releaseDate,
this.rights,
@ -155,6 +156,7 @@ class EpisodeItem {
this.subtitle,
this.title,
this.vid,
this.stat,
});
int? aid;
@ -173,6 +175,7 @@ class EpisodeItem {
String? link;
String? longTitle;
int? pubTime;
int? pubdate;
int? pv;
String? releaseDate;
Map? rights;
@ -184,6 +187,7 @@ class EpisodeItem {
String? subtitle;
String? title;
String? vid;
String? stat;
EpisodeItem.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
@ -202,6 +206,7 @@ class EpisodeItem {
link = json['link'];
longTitle = json['long_title'];
pubTime = json['pub_time'];
pubdate = json['pub_time'];
pv = json['pv'];
releaseDate = json['release_date'];
rights = json['rights'];
@ -211,7 +216,7 @@ class EpisodeItem {
skip = json['skip'];
status = json['status'];
subtitle = json['subtitle'];
title = json['title'];
title = json['long_title'];
vid = json['vid'];
}
}

View File

@ -101,6 +101,7 @@ class ReplyReplyData {
this.page,
this.config,
this.replies,
this.root,
this.topReplies,
this.upper,
});
@ -108,6 +109,7 @@ class ReplyReplyData {
ReplyPage? page;
ReplyConfig? config;
late List<ReplyItemModel>? replies;
ReplyItemModel? root;
late List<ReplyItemModel>? topReplies;
ReplyUpper? upper;
@ -120,6 +122,9 @@ class ReplyReplyData {
(item) => ReplyItemModel.fromJson(item, json['upper']['mid']))
.toList()
: [];
root = json['root'] != null
? ReplyItemModel.fromJson(json['root'], false)
: null;
topReplies = json['top_replies'] != null
? json['top_replies']
.map<ReplyItemModel>((item) => ReplyItemModel.fromJson(

View File

@ -377,6 +377,7 @@ class Part {
int? page;
String? from;
String? pagePart;
String? title;
int? duration;
String? vid;
String? weblink;
@ -389,6 +390,7 @@ class Part {
this.page,
this.from,
this.pagePart,
this.title,
this.duration,
this.vid,
this.weblink,
@ -406,6 +408,7 @@ class Part {
page = json["page"];
from = json["from"];
pagePart = json["part"];
title = json["part"];
duration = json["duration"];
vid = json["vid"];
weblink = json["weblink"];
@ -649,6 +652,9 @@ class EpisodeItem {
Part? page;
String? bvid;
String? cover;
int? pubdate;
int? duration;
Stat? stat;
EpisodeItem.fromJson(Map<String, dynamic> json) {
seasonId = json['season_id'];
@ -661,6 +667,9 @@ class EpisodeItem {
page = Part.fromJson(json['page']);
bvid = json['bvid'];
cover = json['arc']['pic'];
pubdate = json['arc']['pubdate'];
duration = json['arc']['duration'];
stat = Stat.fromJson(json['arc']['stat']);
}
}

View File

@ -302,14 +302,13 @@ class BangumiIntroController extends GetxController {
episodes: episodes,
currentCid: videoDetailCtr.cid.value,
dataType: dataType,
context: Get.context!,
sheetHeight: Get.size.height,
isFullScreen: true,
changeFucCall: (item, index) {
changeSeasonOrbangu(item.bvid, item.cid, item.aid, item.cover);
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),
).buildShowContent(),
);
}

View File

@ -146,17 +146,34 @@ class _BangumiInfoState extends State<BangumiInfo> {
}
// 收藏
showFavBottomSheet() {
showFavBottomSheet() async {
if (bangumiIntroController.userInfo.mid == null) {
SmartDialog.showToast('账号未登录');
return;
}
showModalBottomSheet(
context: context,
useRootNavigator: true,
final mediaQueryData = MediaQuery.of(context);
final contentHeight = mediaQueryData.size.height - kToolbarHeight;
final double initialChildSize =
(contentHeight - Get.width * 9 / 16) / contentHeight;
await showModalBottomSheet(
context: Get.context!,
useSafeArea: true,
isScrollControlled: true,
builder: (BuildContext context) {
return FavPanel(ctr: bangumiIntroController);
return DraggableScrollableSheet(
initialChildSize: initialChildSize,
minChildSize: 0,
maxChildSize: 1,
snap: true,
expand: false,
snapSizes: [initialChildSize],
builder: (BuildContext context, ScrollController scrollController) {
return FavPanel(
ctr: bangumiIntroController,
scrollController: scrollController,
);
},
);
},
);
}

View File

@ -151,7 +151,6 @@ class _BangumiPanelState extends State<BangumiPanel> {
changeFucCall: changeFucCall,
sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.bangumiEpisode,
context: context,
).show(context);
},
child: Text(

View File

@ -11,6 +11,7 @@ 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/global_data_cache.dart';
import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/storage.dart';
@ -90,8 +91,13 @@ class _DynamicsPageState extends State<DynamicsPage>
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
if (_dynamicsController.mid.value != -1 &&
_dynamicsController.upInfo.value.uname != null) {
final mid = _dynamicsController.mid.value;
final uname = _dynamicsController.upInfo.value.uname;
if (mid == -1 || uname == null) {
return const SizedBox();
}
return SizedBox(
height: 36,
child: AnimatedSwitcher(
@ -102,20 +108,17 @@ class _DynamicsPageState extends State<DynamicsPage>
scale: animation, child: child);
},
child: Text(
'${_dynamicsController.upInfo.value.uname!}的动态',
key: ValueKey<String>(
_dynamicsController.upInfo.value.uname!),
'$uname的动态',
key: ValueKey<String>(uname),
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelLarge!
.fontSize,
)),
),
),
),
);
} else {
return const SizedBox();
}
}),
Obx(
() => _dynamicsController.userLogin.value
@ -207,14 +210,19 @@ class _DynamicsPageState extends State<DynamicsPage>
() => UpPanel(
upData: _dynamicsController.upData.value,
onClickUpCb: (data) {
// _dynamicsController.onTapUp(data);
if (GlobalDataCache().enableDynamicSwitch) {
Navigator.push(
context,
PlPopupRoute(
child: OverlayPanel(
ctr: _dynamicsController, upInfo: data),
ctr: _dynamicsController,
upInfo: data,
),
),
);
} else {
_dynamicsController.onTapUp(data);
}
},
),
);

View File

@ -66,7 +66,15 @@ Widget liveRcmdPanel(item, context, {floor = 1}) {
},
child: LayoutBuilder(builder: (context, box) {
double width = box.maxWidth;
return Stack(
return Container(
margin: floor == 1
? const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace)
: EdgeInsets.zero,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius)),
child: Stack(
children: [
Hero(
tag: liveRcmd.roomId.toString(),
@ -79,16 +87,16 @@ Widget liveRcmdPanel(item, context, {floor = 1}) {
),
PBadge(
text: watchedShow['text_large'],
top: 6,
right: 56,
top: 8.0,
right: 62.0,
bottom: null,
left: null,
type: 'gray',
),
PBadge(
text: liveStatus == 1 ? '直播中' : '直播结束',
top: 6,
right: 6,
top: 8.0,
right: 10.0,
bottom: null,
left: null,
),
@ -136,6 +144,7 @@ Widget liveRcmdPanel(item, context, {floor = 1}) {
),
),
],
),
);
}),
),

View File

@ -5,6 +5,7 @@ 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/utils/feed_back.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/utils.dart';
class UpPanel extends StatefulWidget {
@ -23,7 +24,7 @@ class UpPanel extends StatefulWidget {
class _UpPanelState extends State<UpPanel> {
final ScrollController scrollController = ScrollController();
int currentMid = -1;
RxInt currentMid = (-1).obs;
late double contentWidth = 56;
List<UpItem> upList = [];
List<LiveUserItem> liveList = [];
@ -37,26 +38,36 @@ class _UpPanelState extends State<UpPanel> {
}
void onClickUp(data, i) {
currentMid = data.mid;
currentMid.value = data.mid;
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(() {});
}
void onClickUpAni(data, i) {
final screenWidth = MediaQuery.sizeOf(context).width;
final itemWidth = contentWidth + itemPadding.horizontal;
final liveLen = liveList.length;
final upLen = upList.length;
currentMid.value = data.mid;
widget.onClickUpCb?.call(data);
double moveDistance = 0.0;
final totalItemsWidth = itemWidth * (upLen + liveLen);
if (totalItemsWidth > screenWidth) {
if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {
moveDistance = totalItemsWidth + 46 - screenWidth;
}
}
data.hasUpdate = false;
scrollController.animateTo(
moveDistance,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
@override
@ -144,14 +155,17 @@ class _UpPanelState extends State<UpPanel> {
}
Widget upItemBuild(data, i) {
bool isCurrent = currentMid == data.mid || currentMid == -1;
return InkWell(
onTap: () {
feedBack();
if (data.type == 'up') {
EasyThrottle.throttle('follow', const Duration(milliseconds: 300),
() {
if (GlobalDataCache().enableDynamicSwitch) {
onClickUp(data, i);
} else {
onClickUpAni(data, i);
}
});
} else if (data.type == 'live') {
LiveItemModel liveItem = LiveItemModel.fromJson({
@ -177,8 +191,11 @@ class _UpPanelState extends State<UpPanel> {
},
child: Padding(
padding: itemPadding,
child: AnimatedOpacity(
opacity: isCurrent ? 1 : 0.3,
child: Obx(
() => AnimatedOpacity(
opacity: currentMid.value == data.mid || currentMid.value == -1
? 1
: 0.3,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Column(
@ -222,11 +239,13 @@ class _UpPanelState extends State<UpPanel> {
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(
color: currentMid == data.mid
color: currentMid.value == data.mid
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize),
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
),
),
),
@ -234,6 +253,7 @@ class _UpPanelState extends State<UpPanel> {
),
),
),
),
);
}
}

View File

@ -78,7 +78,15 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
],
LayoutBuilder(builder: (context, box) {
double width = box.maxWidth;
return Stack(
return Container(
margin: floor == 1
? const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace)
: EdgeInsets.zero,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius)),
child: Stack(
children: [
NetworkImgLayer(
type: floor == 1 ? 'emote' : null,
@ -120,8 +128,10 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
children: [
DefaultTextStyle.merge(
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Colors.white),
child: Row(
children: [
@ -144,6 +154,7 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
),
),
],
),
);
}),
const SizedBox(height: 6),

View File

@ -96,7 +96,7 @@ class VideoContent extends StatelessWidget {
),
const Spacer(),
Text(
[23, 1].contains(favFolderItem.attr) ? '私密' : '公开',
[22, 0].contains(favFolderItem.attr) ? '公开' : '私密',
textAlign: TextAlign.start,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,

View File

@ -21,6 +21,7 @@ class FavDetailController extends GetxController {
RxString loadingText = '加载中...'.obs;
RxInt mediaCount = 0.obs;
late String isOwner;
late bool hasMore = true;
@override
void onInit() {
@ -35,7 +36,7 @@ class FavDetailController extends GetxController {
}
Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async {
if (type == 'onLoad' && favList.length >= mediaCount.value) {
if (type == 'onLoad' && !hasMore) {
loadingText.value = '没有更多了';
return;
}
@ -47,17 +48,18 @@ class FavDetailController extends GetxController {
);
if (res['status']) {
favInfo.value = res['data'].info;
hasMore = res['data'].hasMore;
if (currentPage == 1 && type == 'init') {
favList.value = res['data'].medias;
mediaCount.value = res['data'].info['media_count'];
} else if (type == 'onLoad') {
favList.addAll(res['data'].medias);
}
if (favList.length >= mediaCount.value) {
if (!hasMore) {
loadingText.value = '没有更多了';
}
}
currentPage += 1;
}
isLoadingMore = false;
return res;
}
@ -126,7 +128,7 @@ class FavDetailController extends GetxController {
'title': item!.title,
'intro': item!.intro,
'cover': item!.cover,
'privacy': [23, 1].contains(item!.attr) ? 1 : 0,
'privacy': [22, 0].contains(item!.attr) ? 0 : 1,
},
);
title.value = res['title'];

View File

@ -193,7 +193,9 @@ class _FavDetailPageState extends State<FavDetailPage> {
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx(
() => Text(
'${_favDetailController.mediaCount}条视频',
_favDetailController.mediaCount > 0
? '${_favDetailController.mediaCount}条视频'
: '',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
@ -215,7 +217,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
List favList = _favDetailController.favList;
return Obx(
() => favList.isEmpty
? const SliverToBoxAdapter(child: SizedBox())
? const NoData()
: SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
@ -247,18 +249,20 @@ class _FavDetailPageState extends State<FavDetailPage> {
),
SliverToBoxAdapter(
child: Container(
height: MediaQuery.of(context).padding.bottom + 60,
height: MediaQuery.of(context).padding.bottom + 90,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom),
child: Center(
child: Obx(
() => Text(
_favDetailController.loadingText.value,
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: 13),
),
),
child: Obx(() {
final mediaCount = _favDetailController.mediaCount;
final loadingText = _favDetailController.loadingText.value;
final textColor = Theme.of(context).colorScheme.outline;
return Text(
mediaCount > 0 ? loadingText : '',
style: TextStyle(color: textColor, fontSize: 13),
);
}),
),
),
)

View File

@ -1,4 +1,3 @@
import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -48,24 +47,27 @@ class FollowItem extends StatelessWidget {
height: 34,
child: TextButton(
onPressed: () async {
await showFlexibleBottomSheet(
bottomSheetBorderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
minHeight: 1,
initHeight: 1,
maxHeight: 1,
context: Get.context!,
await showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
builder: (BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0,
maxChildSize: 1,
snap: true,
expand: false,
snapSizes: const [0.6],
builder: (BuildContext context,
ScrollController scrollController, double offset) {
ScrollController scrollController) {
return GroupPanel(
mid: item.mid!,
scrollController: scrollController,
);
},
anchors: [1],
isSafeArea: true,
);
},
);
},
style: TextButton.styleFrom(

View File

@ -129,7 +129,7 @@ class _LaterPageState extends State<LaterPage> {
),
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).padding.bottom + 10,
height: MediaQuery.of(context).padding.bottom + 80,
),
)
],

View File

@ -1,13 +1,11 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.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/http/search.dart';
import 'package:pilipala/models/msg/like.dart';
import 'package:pilipala/utils/utils.dart';
import '../utils/index.dart';
import 'controller.dart';
class MessageLikePage extends StatefulWidget {
@ -122,39 +120,13 @@ class LikeItem extends StatelessWidget {
final nickNameList = item.users!.map((e) => e.nickname).take(2).toList();
int usersLen = item.users!.length > 3 ? 3 : item.users!.length;
final Uri uri = Uri.parse(item.item!.uri!);
final String path = uri.path;
final String bvid = path.split('/').last;
/// bilibili://
final Uri nativeUri = Uri.parse(item.item!.nativeUri!);
final Map<String, String> queryParameters = nativeUri.queryParameters;
final String type = item.item!.type!;
// cid
final String? argCid = queryParameters['cid'];
// 页码
final String? page = queryParameters['page'];
// 根评论id
final String? commentRootId = queryParameters['comment_root_id'];
// 二级评论id
final String? commentSecondaryId = queryParameters['comment_secondary_id'];
return InkWell(
onTap: () async {
try {
final int cid = argCid != null
? int.parse(argCid)
: await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
} catch (e) {
SmartDialog.showToast('视频可能失效了$e');
}
MessageUtils.onClickMessage(context, uri, nativeUri, type);
},
child: Stack(
children: [
@ -243,6 +215,7 @@ class LikeItem extends StatelessWidget {
width: 60,
height: 60,
src: item.item!.image,
radius: 6,
),
],
),

View File

@ -4,10 +4,9 @@ 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/http/search.dart';
import 'package:pilipala/models/msg/reply.dart';
import 'package:pilipala/pages/message/utils/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MessageReplyPage extends StatefulWidget {
@ -112,28 +111,14 @@ class ReplyItem extends StatelessWidget {
Widget build(BuildContext context) {
Color outline = Theme.of(context).colorScheme.outline;
final String heroTag = Utils.makeHeroTag(item.user!.mid);
final String bvid = item.item!.uri!.split('/').last;
// 页码
final String page =
item.item!.nativeUri!.split('page=').last.split('&').first;
// 根评论id
final String commentRootId =
item.item!.nativeUri!.split('comment_root_id=').last.split('&').first;
// 二级评论id
final String commentSecondaryId =
item.item!.nativeUri!.split('comment_secondary_id=').last;
final Uri uri = Uri.parse(item.item!.uri!);
/// bilibili://
final Uri nativeUri = Uri.parse(item.item!.nativeUri!);
final String type = item.item!.type!;
return InkWell(
onTap: () async {
final int cid = await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
MessageUtils.onClickMessage(context, uri, nativeUri, type);
},
child: Padding(
padding: const EdgeInsets.all(14),
@ -217,6 +202,7 @@ class ReplyItem extends StatelessWidget {
width: 60,
height: 60,
src: item.item!.image,
radius: 6,
),
],
),

View File

@ -1,7 +1,10 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/models/msg/system.dart';
import 'package:pilipala/pages/message/utils/index.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'controller.dart';
class MessageSystemPage extends StatefulWidget {
@ -97,6 +100,13 @@ class SystemItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
// if (item.content is Map) {
// var res = MessageUtils().extractLinks(item.content['web']);
// print('res: $res');
// } else {
// var res = MessageUtils().extractLinks(item.content);
// print('res: $res');
// }
return Padding(
padding: const EdgeInsets.fromLTRB(14, 14, 14, 12),
child: Column(
@ -111,9 +121,73 @@ class SystemItem extends StatelessWidget {
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
const SizedBox(height: 6),
Text(item.content is String ? item.content : item.content!['web']),
Text.rich(
TextSpan(
children: [
buildContent(
context,
item.content is String ? item.content : item.content!['web']!,
),
],
),
),
// if (item.content is String)
// Text(item.content)
// else ...[
// Text(item.content!['web']!),
// ]
],
),
);
}
InlineSpan buildContent(
BuildContext context,
String content,
) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final List<InlineSpan> spanChilds = <InlineSpan>[];
Map<String, dynamic> contentMap = MessageUtils().extractLinks(content);
List<String> keys = contentMap.keys.toList();
keys.removeWhere((element) => element == 'message');
String patternStr = keys.join('|');
RegExp regExp = RegExp(patternStr, caseSensitive: false);
contentMap['message'].splitMapJoin(
regExp,
onMatch: (Match match) {
if (!match.group(0)!.startsWith('BV')) {
spanChilds.add(
WidgetSpan(
child: Icon(Icons.link, color: colorScheme.primary, size: 16),
),
);
}
spanChilds.add(
TextSpan(
text: match.group(0),
style: TextStyle(
color: colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
PiliSchame.routePush(Uri.parse(contentMap[match.group(0)]));
},
),
);
return '';
},
onNonMatch: (String text) {
spanChilds.add(
TextSpan(
text: text,
),
);
return '';
},
);
return TextSpan(
children: spanChilds,
);
}
}

View File

@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/pages/video/detail/reply_reply/index.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/utils.dart';
class MessageUtils {
// 回复我的、收到的赞点击
static void onClickMessage(
BuildContext context, Uri uri, Uri nativeUri, String type) async {
final String path = uri.path;
final String bvid = path.split('/').last;
final String nativePath = nativeUri.path;
final String oid = nativePath.split('/').last;
final Map<String, String> queryParameters = nativeUri.queryParameters;
final String? argCid = queryParameters['cid'];
// final String? page = queryParameters['page'];
final String? commentRootId = queryParameters['comment_root_id'];
// final String? commentSecondaryId = queryParameters['comment_secondary_id'];
switch (type) {
case 'video':
case 'danmu':
try {
final int cid = argCid != null
? int.parse(argCid)
: await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
} catch (e) {
SmartDialog.showToast('视频可能失效了$e');
}
break;
case 'reply':
debugPrint('commentRootId: $oid, $commentRootId');
navigateToComment(
context, oid, commentRootId!, ReplyType.video, nativeUri);
break;
default:
break;
}
}
// 跳转查看评论
static void navigateToComment(
BuildContext context,
String oid,
String rpid,
ReplyType replyType,
Uri nativeUri,
) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: const Text('评论详情'),
actions: [
IconButton(
tooltip: '查看原内容',
onPressed: () {
PiliSchame.routePush(nativeUri);
},
icon: const Icon(Icons.open_in_new_outlined),
),
const SizedBox(width: 10),
],
),
body: VideoReplyReplyPanel(
oid: int.tryParse(oid),
rpid: int.tryParse(rpid),
source: 'routePush',
replyType: replyType,
firstFloor: null,
showRoot: true,
),
),
),
);
}
// 匹配链接
Map<String, String> extractLinks(String text) {
Map<String, String> result = {};
String message = '';
// 是否匹配到bv
RegExp bvRegex = RegExp(r'bv1[\d\w]{9}', caseSensitive: false);
final Iterable<RegExpMatch> bvMatches = bvRegex.allMatches(text);
for (var match in bvMatches) {
result[match.group(0)!] =
'https://www.bilibili.com/video/${match.group(0)!}';
}
// 定义正则表达式
RegExp regex = RegExp(
r'(?:(?:(?:http:\/\/|https:\/\/)(?:[a-zA-Z0-9_.-]+\.)*(?:bilibili|biligame)\.com(?:\/[/.$*?~=#!%@&\-\w]*)?)|(?:(?:http:\/\/|https:\/\/)(?:[a-zA-Z0-9_.-]+\.)*(?:acg|b23)\.tv(?:\/[/.$*?~=#!%@&\-\w]*)?)|(?:(?:http:\/\/|https:\/\/)dl\.(?:hdslb)\.com(?:\/[/.$*?~=#!%@&\-\w]*)?))');
// 链接文字
RegExp linkTextRegex = RegExp(r"#\{(.*?)\}");
final Iterable<RegExpMatch> matches = regex.allMatches(text);
int lastMatchEnd = 0;
if (matches.isNotEmpty) {
for (var match in matches) {
final int start = match.start;
final int end = match.end;
String str = text.substring(lastMatchEnd, start);
final Iterable<RegExpMatch> linkTextMatches =
linkTextRegex.allMatches(str);
if (linkTextMatches.isNotEmpty) {
for (var linkTextMatch in linkTextMatches) {
if (linkTextMatch.group(1) != null) {
String linkText = linkTextMatch.group(1)!;
str = str
.replaceAll(linkTextMatch.group(0)!, linkText)
.replaceAll('{', '')
.replaceAll('}', '');
result[linkText] = match.group(0)!;
}
message += str;
}
} else {
message += '$str查看详情';
result['查看详情'] = match.group(0)!;
}
lastMatchEnd = end;
}
result['message'] = message;
} else {
result['message'] = text;
}
return result;
}
}

View File

@ -183,6 +183,14 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableAi,
defaultVal: true,
),
SetSwitchItem(
title: '视频简介默认展开',
setKey: SettingBoxKey.enableAutoExpand,
defaultVal: false,
callFn: (val) {
GlobalDataCache().enableAutoExpand = val;
},
),
const SetSwitchItem(
title: '相关视频推荐',
subTitle: '视频详情页推荐相关视频',

View File

@ -108,6 +108,12 @@ class _StyleSettingState extends State<StyleSetting> {
defaultVal: true,
needReboot: true,
),
const SetSwitchItem(
title: '动态页滑动切换up',
setKey: SettingBoxKey.enableDynamicSwitch,
defaultVal: true,
needReboot: true,
),
ListTile(
onTap: () async {
int? result = await showDialog(

View File

@ -1,6 +1,6 @@
import 'dart:async';
import 'package:bottom_sheet/bottom_sheet.dart';
// import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@ -62,6 +62,7 @@ class VideoIntroController extends GetxController {
late ModelResult modelResult;
PersistentBottomSheetController? bottomSheetController;
late bool enableRelatedVideo;
UgcSeason? ugcSeason;
@override
void onInit() {
@ -87,6 +88,7 @@ class VideoIntroController extends GetxController {
var result = await VideoHttp.videoIntro(bvid: bvid);
if (result['status']) {
videoDetail.value = result['data']!;
ugcSeason = result['data']!.ugcSeason;
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
lastPlayCid.value = videoDetail.value.pages!.first.cid!;
}
@ -531,25 +533,31 @@ class VideoIntroController extends GetxController {
}
// 设置关注分组
void setFollowGroup() {
showFlexibleBottomSheet(
bottomSheetBorderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
minHeight: 0.6,
initHeight: 0.6,
maxHeight: 1,
void setFollowGroup() async {
final mediaQueryData = MediaQuery.of(Get.context!);
final contentHeight = mediaQueryData.size.height - kToolbarHeight;
final double initialChildSize =
(contentHeight - Get.width * 9 / 16) / contentHeight;
await showModalBottomSheet(
context: Get.context!,
builder: (BuildContext context, ScrollController scrollController,
double offset) {
useSafeArea: true,
isScrollControlled: true,
builder: (BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: initialChildSize,
minChildSize: 0,
maxChildSize: 1,
snap: true,
expand: false,
snapSizes: [initialChildSize],
builder: (BuildContext context, ScrollController scrollController) {
return GroupPanel(
mid: videoDetail.value.owner!.mid!,
scrollController: scrollController,
);
},
anchors: [0.6, 1],
isSafeArea: true,
);
},
);
}
@ -602,9 +610,9 @@ class VideoIntroController extends GetxController {
episodes: episodes,
currentCid: lastPlayCid.value,
dataType: dataType,
context: Get.context!,
sheetHeight: Get.size.height,
isFullScreen: true,
ugcSeason: ugcSeason,
changeFucCall: (item, index) {
if (dataType == VideoEpidoesType.videoEpisode) {
changeSeasonOrbangu(
@ -615,7 +623,7 @@ class VideoIntroController extends GetxController {
}
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),
).buildShowContent(),
);
}

View File

@ -1,4 +1,4 @@
import 'package:bottom_sheet/bottom_sheet.dart';
// import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:expandable/expandable.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -169,7 +169,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
owner = widget.videoDetail!.owner;
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
_expandableCtr = ExpandableController(initialExpanded: false);
_expandableCtr = ExpandableController(
initialExpanded: GlobalDataCache().enableAutoExpand);
}
// 收藏
@ -198,25 +199,35 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
}
}
void _showFavPanel() {
showFlexibleBottomSheet(
bottomSheetBorderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
void _showFavPanel() async {
final mediaQueryData = MediaQuery.of(context);
final contentHeight = mediaQueryData.size.height - kToolbarHeight;
final double initialChildSize =
(contentHeight - Get.width * 9 / 16) / contentHeight;
await showModalBottomSheet(
context: Get.context!,
useSafeArea: true,
isScrollControlled: true,
transitionAnimationController: AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
),
minHeight: 0.6,
initHeight: 0.6,
maxHeight: 1,
context: context,
builder: (BuildContext context, ScrollController scrollController,
double offset) {
builder: (BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: initialChildSize,
minChildSize: 0,
maxChildSize: 1,
snap: true,
expand: false,
snapSizes: [initialChildSize],
builder: (BuildContext context, ScrollController scrollController) {
return FavPanel(
ctr: videoIntroController,
scrollController: scrollController,
);
},
anchors: [0.6, 1],
isSafeArea: true,
);
},
);
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@ -32,8 +31,14 @@ class _FavPanelState extends State<FavPanel> {
AppBar(
centerTitle: false,
elevation: 0,
automaticallyImplyLeading: false,
leadingWidth: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)),
title: Text(
'选择收藏夹',
style: Theme.of(context)
@ -61,16 +66,16 @@ class _FavPanelState extends State<FavPanel> {
onTap: () =>
widget.ctr!.onChoose(item.favState != 1, index),
dense: true,
leading: Icon([23, 1].contains(item.attr)
leading: Icon([22, 0].contains(item.attr)
? Icons.lock_outline
: Icons.folder_outlined),
minLeadingWidth: 0,
title: Text(item.title!),
subtitle: Text(
'${item.mediaCount}个内容 - ${[
23,
1
].contains(item.attr) ? '私密' : '公开'}',
22,
0
].contains(item.attr) ? '公开' : '私密'}',
),
trailing: Transform.scale(
scale: 0.9,
@ -92,7 +97,7 @@ class _FavPanelState extends State<FavPanel> {
}
} else {
// 骨架屏
return const Text('请求中');
return const Center(child: Text('请求中'));
}
},
),

View File

@ -59,10 +59,19 @@ class _GroupPanelState extends State<GroupPanel> {
AppBar(
centerTitle: false,
elevation: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)),
title: Text('设置关注分组', style: Theme.of(context).textTheme.titleMedium),
title: Text('设置关注分组',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold)),
),
Expanded(
child: Material(
@ -115,7 +124,7 @@ class _GroupPanelState extends State<GroupPanel> {
}
} else {
// 骨架屏
return const Text('请求中');
return const Center(child: Text('请求中'));
}
},
),

View File

@ -116,7 +116,6 @@ class _PagesPanelState extends State<PagesPanel> {
changeFucCall: changeFucCall,
sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.videoPart,
context: context,
).show(context);
},
child: Text(

View File

@ -124,7 +124,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
changeFucCall: changeFucCall,
sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.videoEpisode,
context: context,
ugcSeason: widget.ugcSeason,
).show(context);
},
child: Padding(

View File

@ -764,14 +764,14 @@ InlineSpan buildContent(
});
} else {
Uri uri = Uri.parse(matchStr.replaceAll('/?', '?'));
SchemeEntity scheme = SchemeEntity(
Uri scheme = Uri(
scheme: uri.scheme,
host: uri.host,
port: uri.port,
path: uri.path,
query: uri.queryParameters,
source: '',
dataString: matchStr,
// query: uri.queryParameters,
// source: '',
// dataString: matchStr,
);
PiliSchame.httpsScheme(scheme);
}

View File

@ -5,13 +5,15 @@ import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
class VideoReplyReplyController extends GetxController {
VideoReplyReplyController(this.aid, this.rpid, this.replyType);
VideoReplyReplyController(this.aid, this.rpid, this.replyType, this.showRoot);
final ScrollController scrollController = ScrollController();
// 视频aid 请求时使用的oid
int? aid;
// rpid 请求楼中楼回复
String? rpid;
ReplyType replyType = ReplyType.video;
bool showRoot = false;
ReplyItemModel? rootReply;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
// 当前页
int currentPage = 0;
@ -42,6 +44,7 @@ class VideoReplyReplyController extends GetxController {
);
if (res['status']) {
final List<ReplyItemModel> replies = res['data'].replies;
ReplyItemModel? root = res['data'].root;
if (replies.isNotEmpty) {
noMore.value = '加载中...';
if (replies.length == res['data'].page.count) {
@ -60,7 +63,9 @@ class VideoReplyReplyController extends GetxController {
return;
}
replyList.addAll(replies);
// res['data'].replies.addAll(replyList);
}
if (showRoot && root != null) {
rootReply = root;
}
}
if (replyList.isNotEmpty && currentReply != null) {

View File

@ -8,7 +8,6 @@ import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/utils/storage.dart';
import 'controller.dart';
class VideoReplyReplyPanel extends StatefulWidget {
@ -22,6 +21,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.sheetHeight,
this.currentReply,
this.loadMore = true,
this.showRoot = false,
super.key,
});
final int? oid;
@ -33,6 +33,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
final double? sheetHeight;
final dynamic currentReply;
final bool loadMore;
final bool showRoot;
@override
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
@ -49,7 +50,11 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
void initState() {
_videoReplyReplyController = Get.put(
VideoReplyReplyController(
widget.oid, widget.rpid.toString(), widget.replyType!),
widget.oid,
widget.rpid.toString(),
widget.replyType!,
widget.showRoot,
),
tag: widget.rpid.toString());
super.initState();
@ -80,15 +85,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.source == 'videoDetail' ? widget.sheetHeight : null,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (widget.source == 'videoDetail')
AppBar(
Widget _buildAppBar() {
return AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
centerTitle: false,
@ -101,13 +99,87 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
icon: const Icon(Icons.close, size: 20),
onPressed: () {
_videoReplyReplyController.currentPage = 0;
widget.closePanel?.call;
widget.closePanel?.call();
Navigator.pop(context);
},
),
const SizedBox(width: 14),
],
);
}
Widget _buildReplyItem(ReplyItemModel? replyItem, String replyLevel) {
return ReplyItem(
replyItem: replyItem,
replyLevel: replyLevel,
showReplyRow: false,
addReply: (replyItem) {
_videoReplyReplyController.replyList.add(replyItem);
},
replyType: widget.replyType,
replyReply: (replyItem) => replyReply(replyItem),
);
}
Widget _buildSliverList() {
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index == 0) {
return _videoReplyReplyController.rootReply != null
? Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color:
Theme.of(context).dividerColor.withOpacity(0.1),
width: 6,
),
),
),
child: _buildReplyItem(
_videoReplyReplyController.rootReply, '1'),
)
: const SizedBox();
}
int adjustedIndex = index - 1;
if (adjustedIndex == _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(
_videoReplyReplyController.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
),
),
);
} else {
return _buildReplyItem(
_videoReplyReplyController.replyList[adjustedIndex], '2');
}
},
childCount: _videoReplyReplyController.replyList.length + 2,
),
),
);
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.source == 'videoDetail' ? widget.sheetHeight : null,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (widget.source == 'videoDetail') _buildAppBar(),
Expanded(
child: RefreshIndicator(
onRefresh: () async {
@ -120,28 +192,22 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
child: CustomScrollView(
controller: _videoReplyReplyController.scrollController,
slivers: <Widget>[
if (widget.firstFloor != null) ...[
// const SliverToBoxAdapter(child: SizedBox(height: 10)),
if (widget.firstFloor != null)
SliverToBoxAdapter(
child: ReplyItem(
replyItem: widget.firstFloor,
replyLevel: '2',
showReplyRow: false,
addReply: (replyItem) {
_videoReplyReplyController.replyList.add(replyItem);
},
replyType: widget.replyType,
replyReply: (replyItem) => replyReply(replyItem),
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
width: 6,
),
),
SliverToBoxAdapter(
child: Divider(
height: 20,
color: Theme.of(context).dividerColor.withOpacity(0.1),
thickness: 6,
),
child: _buildReplyItem(widget.firstFloor, '2'),
),
),
],
widget.loadMore
? FutureBuilder(
future: _futureBuilderFuture,
@ -150,76 +216,21 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
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(
_videoReplyReplyController
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
);
return _buildSliverList();
} else {
return ReplyItem(
replyItem:
_videoReplyReplyController
.replyList[index],
replyLevel: '2',
showReplyRow: false,
addReply: (replyItem) {
_videoReplyReplyController
.replyList
.add(replyItem);
},
replyType: widget.replyType,
replyReply: (replyItem) =>
replyReply(replyItem),
);
}
},
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),
},
childCount: 8,
),
);
}
},
@ -237,7 +248,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
),
),
),
)
),
],
),
),

View File

@ -24,30 +24,30 @@ class PiliSchame {
appScheme.getInitScheme().then((SchemeEntity? value) {
if (value != null) {
_routePush(value);
routePush(value);
}
});
appScheme.getLatestScheme().then((SchemeEntity? value) {
if (value != null) {
_routePush(value);
routePush(value);
}
});
appScheme.registerSchemeListener().listen((SchemeEntity? event) {
if (event != null) {
_routePush(event);
routePush(event);
}
});
}
/// 路由跳转
static void _routePush(value) async {
static void routePush(value) async {
final String scheme = value.scheme;
if (scheme == 'bilibili') {
biliScheme(value);
}
if (scheme == 'https') {
if (['http', 'https'].contains(scheme)) {
httpsScheme(value);
}
}
@ -79,16 +79,16 @@ class PiliSchame {
}
}
static Future<void> httpsScheme(SchemeEntity value) async {
static Future<void> httpsScheme(Uri value) async {
// https://m.bilibili.com/bangumi/play/ss39708
// https | m.bilibili.com | /bangumi/play/ss39708
// final String scheme = value.scheme!;
final String host = value.host!;
final String? path = value.path;
Map<String, String>? query = value.query;
final String host = value.host;
final String path = value.path;
Map<String, String>? query = value.queryParameters;
RegExp regExp = RegExp(r'^((www\.)|(m\.))?bilibili\.com$');
if (regExp.hasMatch(host)) {
final String lastPathSegment = path!.split('/').last;
final String lastPathSegment = path.split('/').last;
if (path.startsWith('/video')) {
Map matchRes = IdUtils.matchAvorBv(input: path);
if (matchRes.containsKey('AV')) {
@ -113,13 +113,13 @@ class PiliSchame {
_videoPush(Utils.matchNum(path.split('?').first).first, null);
}
} else if (host.contains('live')) {
int roomId = int.parse(path!.split('/').last);
int roomId = int.parse(path.split('/').last);
Get.toNamed(
'/liveRoom?roomid=$roomId',
arguments: {'liveItem': null, 'heroTag': roomId.toString()},
);
} else if (host.contains('space')) {
var mid = path!.split('/').last;
var mid = path.split('/').last;
Get.toNamed('/member?mid=$mid', arguments: {'face': ''});
return;
} else if (host == 'b23.tv') {
@ -154,7 +154,7 @@ class PiliSchame {
parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''},
);
}
} else if (path != null) {
} else {
final String area = path.split('/').last;
switch (area) {
case 'bangumi':
@ -178,12 +178,12 @@ class PiliSchame {
break;
case 'read':
print('专栏');
String id = Utils.matchNum(query!['id']!).first.toString();
String id = Utils.matchNum(query['id']!).first.toString();
Get.toNamed('/read', parameters: {
'url': value.dataString!,
'url': value.toString(),
'title': '',
'id': id,
'articleType': 'read'
'articleType': 'read',
});
break;
case 'space':
@ -201,9 +201,9 @@ class PiliSchame {
Get.toNamed(
'/webview',
parameters: {
'url': value.dataString ?? "",
'url': value.toString(),
'type': 'url',
'pageTitle': ''
'pageTitle': '',
},
);
}

View File

@ -49,6 +49,10 @@ class GlobalDataCache {
late List historyCacheList;
//
late bool enableSearchSuggest = true;
// 简介默认展开
late bool enableAutoExpand = false;
//
late bool enableDynamicSwitch = true;
// 私有构造函数
GlobalDataCache._();
@ -112,5 +116,9 @@ class GlobalDataCache {
historyCacheList = localCache.get('cacheList', defaultValue: []);
enableSearchSuggest =
setting.get(SettingBoxKey.enableSearchSuggest, defaultValue: true);
enableAutoExpand =
setting.get(SettingBoxKey.enableAutoExpand, defaultValue: false);
enableDynamicSwitch =
setting.get(SettingBoxKey.enableDynamicSwitch, defaultValue: true);
}
}

View File

@ -113,6 +113,7 @@ class SettingBoxKey {
enableSearchWord = 'enableSearchWord',
enableSystemProxy = 'enableSystemProxy',
enableAi = 'enableAi',
enableAutoExpand = 'enableAutoExpand',
defaultHomePage = 'defaultHomePage',
enableRelatedVideo = 'enableRelatedVideo';
@ -130,6 +131,7 @@ class SettingBoxKey {
tabbarSort = 'tabbarSort', // 首页tabbar
dynamicBadgeMode = 'dynamicBadgeMode',
enableGradientBg = 'enableGradientBg',
enableDynamicSwitch = 'enableDynamicSwitch',
navBarSort = 'navBarSort',
actionTypeSort = 'actionTypeSort';
}

View File

@ -145,22 +145,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
bottom_inset_observer:
dependency: transitive
description:
name: bottom_inset_observer
sha256: cbfb01e0e07cc4922052701786d5e607765a6f54e1844f41061abf8744519a7d
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
bottom_sheet:
dependency: "direct main"
description:
name: bottom_sheet
sha256: efd28f52357d23e1c01eaeb45466b407f1e29318305bd6d10baf814fda18bd7e
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.4"
brotli:
dependency: "direct main"
description:

View File

@ -147,7 +147,6 @@ dependencies:
lottie: ^3.1.2
# 二维码
qr_flutter: ^4.1.0
bottom_sheet: ^4.0.4
web_socket_channel: ^2.4.5
brotli: ^0.6.0
# 文本语法高亮