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/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.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/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 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../models/common/video_episode_type.dart'; import '../models/common/video_episode_type.dart';
import 'widgets/badge.dart';
import 'widgets/stat/danmu.dart';
import 'widgets/stat/view.dart';
class EpisodeBottomSheet { class EpisodeBottomSheet {
final List<dynamic> episodes; final List<dynamic> episodes;
final int currentCid; final int currentCid;
final dynamic dataType; final dynamic dataType;
final BuildContext context;
final Function changeFucCall; final Function changeFucCall;
final int? cid; final int? cid;
final double? sheetHeight; final double? sheetHeight;
bool isFullScreen = false; bool isFullScreen = false;
final UgcSeason? ugcSeason;
EpisodeBottomSheet({ EpisodeBottomSheet({
required this.episodes, required this.episodes,
required this.currentCid, required this.currentCid,
required this.dataType, required this.dataType,
required this.context,
required this.changeFucCall, required this.changeFucCall,
this.cid, this.cid,
this.sheetHeight, this.sheetHeight,
this.isFullScreen = false, this.isFullScreen = false,
this.ugcSeason,
}); });
Widget buildEpisodeListItem( Widget buildShowContent() {
dynamic episode, return PagesBottomSheet(
int index, episodes: episodes,
bool isCurrentIndex, 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 primary = Theme.of(context).colorScheme.primary;
Color onSurface = Theme.of(context).colorScheme.onSurface; Color onSurface = Theme.of(context).colorScheme.onSurface;
@ -45,9 +259,19 @@ class EpisodeBottomSheet {
title = '${episode.title}${episode.longTitle!}'; title = '${episode.title}${episode.longTitle!}';
break; break;
} }
return isFullScreen || episode?.cover == null || episode?.cover == '' 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: () { onTap: () {
if (isCurrentIndex) {
return;
}
SmartDialog.showToast('切换至「$title'); SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index); changeFucCall.call(episode, index);
}, },
@ -59,114 +283,284 @@ class EpisodeBottomSheet {
height: 12, height: 12,
) )
: null, : null,
title: Text(title, title: Text(
title,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: isCurrentIndex ? primary : onSurface, color: isCurrentIndex ? primary : onSurface,
))) ),
: InkWell( ),
);
}
Widget _buildInkWell(
BuildContext context, String title, Color primary, Color onSurface) {
return InkWell(
onTap: () { onTap: () {
if (isCurrentIndex) {
return;
}
SmartDialog.showToast('切换至「$title'); SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index); changeFucCall.call(episode, index);
}, },
child: Padding( child: Padding(
padding: padding: const EdgeInsets.fromLTRB(
const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8), 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( 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: [ children: [
NetworkImgLayer( NetworkImgLayer(
width: 130, height: 75, src: episode?.cover ?? ''), src: episode?.cover ?? '',
const SizedBox(width: 10), width: maxWidth,
height: maxHeight,
),
if (episode.duration != 0)
PBadge(
text: Utils.timeFormat(episode.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
],
);
},
),
),
Expanded( Expanded(
child: Text( child: Padding(
title, padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
episode.title as String,
textAlign: TextAlign.start,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w500,
color: isCurrentIndex ? primary : onSurface, 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(
);
}
Widget buildTitle() {
return AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
centerTitle: false,
title: Text(
'合集(${episodes.length}',
style: Theme.of(context).textTheme.titleMedium,
),
actions: !isFullScreen
? [
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 14),
]
: null,
);
}
Widget buildShowContent(BuildContext context) {
final ItemScrollController itemScrollController = ItemScrollController();
int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid);
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
itemScrollController.jumpTo(index: currentIndex);
});
return Container(
height: sheetHeight,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [ children: [
buildTitle(), StatView(view: episode.stat.view),
Expanded( const SizedBox(width: 8),
child: Material( StatDanMu(danmu: episode.stat.danmaku),
child: PageStorage( const Spacer(),
bucket: PageStorageBucket(), ],
child: ScrollablePositionedList.builder( ),
itemScrollController: itemScrollController, const SizedBox(height: 4),
itemCount: episodes.length + 1, ]
itemBuilder: (BuildContext context, int index) { ],
bool isLastItem = index == episodes.length; ),
bool isCurrentIndex = currentIndex == index; ),
return isLastItem
? SizedBox(
height:
MediaQuery.of(context).padding.bottom + 20,
) )
: buildEpisodeListItem(
episodes[index],
index,
isCurrentIndex,
);
},
),
),
),
),
], ],
), ),
); );
});
}
/// The [BuildContext] of the widget that calls the bottom sheet.
PersistentBottomSheetController show(BuildContext context) {
final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
context: context,
builder: (BuildContext context) {
return buildShowContent(context);
}, },
),
),
);
}
}
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)),
),
)
],
);
}
}
class UgcSeasonBuild extends StatelessWidget {
final UgcSeason ugcSeason;
const UgcSeasonBuild({
Key? key,
required this.ugcSeason,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
color: Theme.of(context).colorScheme.surface,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(
height: 1,
thickness: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
const SizedBox(height: 10),
Text(
'合集:${ugcSeason.title}',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
),
if (ugcSeason.intro != null && ugcSeason.intro != '') ...[
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Text(ugcSeason.intro ?? '',
style: TextStyle(
color: Theme.of(context).colorScheme.outline)),
),
// SizedBox(
// height: 32,
// child: FilledButton.tonal(
// onPressed: () {},
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// child: const Text('订阅'),
// ),
// ),
// const SizedBox(width: 6),
],
),
],
const SizedBox(height: 4),
Text.rich(
TextSpan(
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
children: [
TextSpan(text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'),
const TextSpan(text: ' · '),
TextSpan(text: '${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'),
],
),
),
const SizedBox(height: 14),
Divider(
height: 1,
thickness: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
],
),
); );
return btmSheetCtr;
} }
} }

View File

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

View File

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

View File

@ -377,6 +377,7 @@ class Part {
int? page; int? page;
String? from; String? from;
String? pagePart; String? pagePart;
String? title;
int? duration; int? duration;
String? vid; String? vid;
String? weblink; String? weblink;
@ -389,6 +390,7 @@ class Part {
this.page, this.page,
this.from, this.from,
this.pagePart, this.pagePart,
this.title,
this.duration, this.duration,
this.vid, this.vid,
this.weblink, this.weblink,
@ -406,6 +408,7 @@ class Part {
page = json["page"]; page = json["page"];
from = json["from"]; from = json["from"];
pagePart = json["part"]; pagePart = json["part"];
title = json["part"];
duration = json["duration"]; duration = json["duration"];
vid = json["vid"]; vid = json["vid"];
weblink = json["weblink"]; weblink = json["weblink"];
@ -649,6 +652,9 @@ class EpisodeItem {
Part? page; Part? page;
String? bvid; String? bvid;
String? cover; String? cover;
int? pubdate;
int? duration;
Stat? stat;
EpisodeItem.fromJson(Map<String, dynamic> json) { EpisodeItem.fromJson(Map<String, dynamic> json) {
seasonId = json['season_id']; seasonId = json['season_id'];
@ -661,6 +667,9 @@ class EpisodeItem {
page = Part.fromJson(json['page']); page = Part.fromJson(json['page']);
bvid = json['bvid']; bvid = json['bvid'];
cover = json['arc']['pic']; 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, episodes: episodes,
currentCid: videoDetailCtr.cid.value, currentCid: videoDetailCtr.cid.value,
dataType: dataType, dataType: dataType,
context: Get.context!,
sheetHeight: Get.size.height, sheetHeight: Get.size.height,
isFullScreen: true, isFullScreen: true,
changeFucCall: (item, index) { changeFucCall: (item, index) {
changeSeasonOrbangu(item.bvid, item.cid, item.aid, item.cover); changeSeasonOrbangu(item.bvid, item.cid, item.aid, item.cover);
SmartDialog.dismiss(); 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) { if (bangumiIntroController.userInfo.mid == null) {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
showModalBottomSheet( final mediaQueryData = MediaQuery.of(context);
context: context, final contentHeight = mediaQueryData.size.height - kToolbarHeight;
useRootNavigator: true, final double initialChildSize =
(contentHeight - Get.width * 9 / 16) / contentHeight;
await showModalBottomSheet(
context: Get.context!,
useSafeArea: true,
isScrollControlled: true, isScrollControlled: true,
builder: (BuildContext context) { 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, changeFucCall: changeFucCall,
sheetHeight: widget.sheetHeight, sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.bangumiEpisode, dataType: VideoEpidoesType.bangumiEpisode,
context: context,
).show(context); ).show(context);
}, },
child: Text( 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/models/dynamics/result.dart';
import 'package:pilipala/plugin/pl_popup/index.dart'; import 'package:pilipala/plugin/pl_popup/index.dart';
import 'package:pilipala/utils/feed_back.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/main_stream.dart';
import 'package:pilipala/utils/route_push.dart'; import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -90,8 +91,13 @@ class _DynamicsPageState extends State<DynamicsPage>
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Obx(() { Obx(() {
if (_dynamicsController.mid.value != -1 && final mid = _dynamicsController.mid.value;
_dynamicsController.upInfo.value.uname != null) { final uname = _dynamicsController.upInfo.value.uname;
if (mid == -1 || uname == null) {
return const SizedBox();
}
return SizedBox( return SizedBox(
height: 36, height: 36,
child: AnimatedSwitcher( child: AnimatedSwitcher(
@ -102,20 +108,17 @@ class _DynamicsPageState extends State<DynamicsPage>
scale: animation, child: child); scale: animation, child: child);
}, },
child: Text( child: Text(
'${_dynamicsController.upInfo.value.uname!}的动态', '$uname的动态',
key: ValueKey<String>( key: ValueKey<String>(uname),
_dynamicsController.upInfo.value.uname!),
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context) fontSize: Theme.of(context)
.textTheme .textTheme
.labelLarge! .labelLarge!
.fontSize, .fontSize,
)), ),
),
), ),
); );
} else {
return const SizedBox();
}
}), }),
Obx( Obx(
() => _dynamicsController.userLogin.value () => _dynamicsController.userLogin.value
@ -207,14 +210,19 @@ class _DynamicsPageState extends State<DynamicsPage>
() => UpPanel( () => UpPanel(
upData: _dynamicsController.upData.value, upData: _dynamicsController.upData.value,
onClickUpCb: (data) { onClickUpCb: (data) {
// _dynamicsController.onTapUp(data); if (GlobalDataCache().enableDynamicSwitch) {
Navigator.push( Navigator.push(
context, context,
PlPopupRoute( PlPopupRoute(
child: OverlayPanel( 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) { child: LayoutBuilder(builder: (context, box) {
double width = box.maxWidth; 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: [ children: [
Hero( Hero(
tag: liveRcmd.roomId.toString(), tag: liveRcmd.roomId.toString(),
@ -79,16 +87,16 @@ Widget liveRcmdPanel(item, context, {floor = 1}) {
), ),
PBadge( PBadge(
text: watchedShow['text_large'], text: watchedShow['text_large'],
top: 6, top: 8.0,
right: 56, right: 62.0,
bottom: null, bottom: null,
left: null, left: null,
type: 'gray', type: 'gray',
), ),
PBadge( PBadge(
text: liveStatus == 1 ? '直播中' : '直播结束', text: liveStatus == 1 ? '直播中' : '直播结束',
top: 6, top: 8.0,
right: 6, right: 10.0,
bottom: null, bottom: null,
left: 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/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class UpPanel extends StatefulWidget { class UpPanel extends StatefulWidget {
@ -23,7 +24,7 @@ class UpPanel extends StatefulWidget {
class _UpPanelState extends State<UpPanel> { class _UpPanelState extends State<UpPanel> {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
int currentMid = -1; RxInt currentMid = (-1).obs;
late double contentWidth = 56; late double contentWidth = 56;
List<UpItem> upList = []; List<UpItem> upList = [];
List<LiveUserItem> liveList = []; List<LiveUserItem> liveList = [];
@ -37,26 +38,36 @@ class _UpPanelState extends State<UpPanel> {
} }
void onClickUp(data, i) { void onClickUp(data, i) {
currentMid = data.mid; currentMid.value = data.mid;
widget.onClickUpCb?.call(data); widget.onClickUpCb?.call(data);
// int liveLen = liveList.length; }
// int upLen = upList.length;
// double itemWidth = contentWidth + itemPadding.horizontal; void onClickUpAni(data, i) {
// double screenWidth = MediaQuery.sizeOf(context).width; final screenWidth = MediaQuery.sizeOf(context).width;
// double moveDistance = 0.0; final itemWidth = contentWidth + itemPadding.horizontal;
// if (itemWidth * (upList.length + liveList.length) <= screenWidth) { final liveLen = liveList.length;
// } else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) { final upLen = upList.length;
// moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
// } else { currentMid.value = data.mid;
// moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth; widget.onClickUpCb?.call(data);
// }
// data.hasUpdate = false; double moveDistance = 0.0;
// scrollController.animateTo( final totalItemsWidth = itemWidth * (upLen + liveLen);
// moveDistance,
// duration: const Duration(milliseconds: 200), if (totalItemsWidth > screenWidth) {
// curve: Curves.linear, if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
// ); moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
// setState(() {}); } else {
moveDistance = totalItemsWidth + 46 - screenWidth;
}
}
data.hasUpdate = false;
scrollController.animateTo(
moveDistance,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
} }
@override @override
@ -144,14 +155,17 @@ class _UpPanelState extends State<UpPanel> {
} }
Widget upItemBuild(data, i) { Widget upItemBuild(data, i) {
bool isCurrent = currentMid == data.mid || currentMid == -1;
return InkWell( return InkWell(
onTap: () { onTap: () {
feedBack(); feedBack();
if (data.type == 'up') { if (data.type == 'up') {
EasyThrottle.throttle('follow', const Duration(milliseconds: 300), EasyThrottle.throttle('follow', const Duration(milliseconds: 300),
() { () {
if (GlobalDataCache().enableDynamicSwitch) {
onClickUp(data, i); onClickUp(data, i);
} else {
onClickUpAni(data, i);
}
}); });
} else if (data.type == 'live') { } else if (data.type == 'live') {
LiveItemModel liveItem = LiveItemModel.fromJson({ LiveItemModel liveItem = LiveItemModel.fromJson({
@ -177,8 +191,11 @@ class _UpPanelState extends State<UpPanel> {
}, },
child: Padding( child: Padding(
padding: itemPadding, padding: itemPadding,
child: AnimatedOpacity( child: Obx(
opacity: isCurrent ? 1 : 0.3, () => AnimatedOpacity(
opacity: currentMid.value == data.mid || currentMid.value == -1
? 1
: 0.3,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: Column( child: Column(
@ -222,11 +239,13 @@ class _UpPanelState extends State<UpPanel> {
softWrap: false, softWrap: false,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: currentMid == data.mid color: currentMid.value == data.mid
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline, : Theme.of(context).colorScheme.outline,
fontSize: fontSize: Theme.of(context)
Theme.of(context).textTheme.labelMedium!.fontSize), .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) { LayoutBuilder(builder: (context, box) {
double width = box.maxWidth; 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: [ children: [
NetworkImgLayer( NetworkImgLayer(
type: floor == 1 ? 'emote' : null, type: floor == 1 ? 'emote' : null,
@ -120,8 +128,10 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
children: [ children: [
DefaultTextStyle.merge( DefaultTextStyle.merge(
style: TextStyle( style: TextStyle(
fontSize: fontSize: Theme.of(context)
Theme.of(context).textTheme.labelMedium!.fontSize, .textTheme
.labelMedium!
.fontSize,
color: Colors.white), color: Colors.white),
child: Row( child: Row(
children: [ children: [
@ -144,6 +154,7 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
), ),
), ),
], ],
),
); );
}), }),
const SizedBox(height: 6), const SizedBox(height: 6),

View File

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

View File

@ -21,6 +21,7 @@ class FavDetailController extends GetxController {
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
RxInt mediaCount = 0.obs; RxInt mediaCount = 0.obs;
late String isOwner; late String isOwner;
late bool hasMore = true;
@override @override
void onInit() { void onInit() {
@ -35,7 +36,7 @@ class FavDetailController extends GetxController {
} }
Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async { Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async {
if (type == 'onLoad' && favList.length >= mediaCount.value) { if (type == 'onLoad' && !hasMore) {
loadingText.value = '没有更多了'; loadingText.value = '没有更多了';
return; return;
} }
@ -47,17 +48,18 @@ class FavDetailController extends GetxController {
); );
if (res['status']) { if (res['status']) {
favInfo.value = res['data'].info; favInfo.value = res['data'].info;
hasMore = res['data'].hasMore;
if (currentPage == 1 && type == 'init') { if (currentPage == 1 && type == 'init') {
favList.value = res['data'].medias; favList.value = res['data'].medias;
mediaCount.value = res['data'].info['media_count']; mediaCount.value = res['data'].info['media_count'];
} else if (type == 'onLoad') { } else if (type == 'onLoad') {
favList.addAll(res['data'].medias); favList.addAll(res['data'].medias);
} }
if (favList.length >= mediaCount.value) { if (!hasMore) {
loadingText.value = '没有更多了'; loadingText.value = '没有更多了';
} }
}
currentPage += 1; currentPage += 1;
}
isLoadingMore = false; isLoadingMore = false;
return res; return res;
} }
@ -126,7 +128,7 @@ class FavDetailController extends GetxController {
'title': item!.title, 'title': item!.title,
'intro': item!.intro, 'intro': item!.intro,
'cover': item!.cover, 'cover': item!.cover,
'privacy': [23, 1].contains(item!.attr) ? 1 : 0, 'privacy': [22, 0].contains(item!.attr) ? 0 : 1,
}, },
); );
title.value = res['title']; 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), padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx( child: Obx(
() => Text( () => Text(
'${_favDetailController.mediaCount}条视频', _favDetailController.mediaCount > 0
? '${_favDetailController.mediaCount}条视频'
: '',
style: TextStyle( style: TextStyle(
fontSize: fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize, Theme.of(context).textTheme.labelMedium!.fontSize,
@ -215,7 +217,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
List favList = _favDetailController.favList; List favList = _favDetailController.favList;
return Obx( return Obx(
() => favList.isEmpty () => favList.isEmpty
? const SliverToBoxAdapter(child: SizedBox()) ? const NoData()
: SliverList( : SliverList(
delegate: delegate:
SliverChildBuilderDelegate((context, index) { SliverChildBuilderDelegate((context, index) {
@ -247,18 +249,20 @@ class _FavDetailPageState extends State<FavDetailPage> {
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container( child: Container(
height: MediaQuery.of(context).padding.bottom + 60, height: MediaQuery.of(context).padding.bottom + 90,
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom), bottom: MediaQuery.of(context).padding.bottom),
child: Center( child: Center(
child: Obx( child: Obx(() {
() => Text( final mediaCount = _favDetailController.mediaCount;
_favDetailController.loadingText.value, final loadingText = _favDetailController.loadingText.value;
style: TextStyle( final textColor = Theme.of(context).colorScheme.outline;
color: Theme.of(context).colorScheme.outline,
fontSize: 13), 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:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -48,24 +47,27 @@ class FollowItem extends StatelessWidget {
height: 34, height: 34,
child: TextButton( child: TextButton(
onPressed: () async { onPressed: () async {
await showFlexibleBottomSheet( await showModalBottomSheet(
bottomSheetBorderRadius: const BorderRadius.only( context: context,
topLeft: Radius.circular(16), useSafeArea: true,
topRight: Radius.circular(16), isScrollControlled: true,
), builder: (BuildContext context) {
minHeight: 1, return DraggableScrollableSheet(
initHeight: 1, initialChildSize: 0.6,
maxHeight: 1, minChildSize: 0,
context: Get.context!, maxChildSize: 1,
snap: true,
expand: false,
snapSizes: const [0.6],
builder: (BuildContext context, builder: (BuildContext context,
ScrollController scrollController, double offset) { ScrollController scrollController) {
return GroupPanel( return GroupPanel(
mid: item.mid!, mid: item.mid!,
scrollController: scrollController, scrollController: scrollController,
); );
}, },
anchors: [1], );
isSafeArea: true, },
); );
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(

View File

@ -129,7 +129,7 @@ class _LaterPageState extends State<LaterPage> {
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: SizedBox( 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:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.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/models/msg/like.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../utils/index.dart';
import 'controller.dart'; import 'controller.dart';
class MessageLikePage extends StatefulWidget { class MessageLikePage extends StatefulWidget {
@ -122,39 +120,13 @@ class LikeItem extends StatelessWidget {
final nickNameList = item.users!.map((e) => e.nickname).take(2).toList(); final nickNameList = item.users!.map((e) => e.nickname).take(2).toList();
int usersLen = item.users!.length > 3 ? 3 : item.users!.length; int usersLen = item.users!.length > 3 ? 3 : item.users!.length;
final Uri uri = Uri.parse(item.item!.uri!); final Uri uri = Uri.parse(item.item!.uri!);
final String path = uri.path;
final String bvid = path.split('/').last;
/// bilibili:// /// bilibili://
final Uri nativeUri = Uri.parse(item.item!.nativeUri!); final Uri nativeUri = Uri.parse(item.item!.nativeUri!);
final Map<String, String> queryParameters = nativeUri.queryParameters;
final String type = item.item!.type!; 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( return InkWell(
onTap: () async { onTap: () async {
try { MessageUtils.onClickMessage(context, uri, nativeUri, type);
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');
}
}, },
child: Stack( child: Stack(
children: [ children: [
@ -243,6 +215,7 @@ class LikeItem extends StatelessWidget {
width: 60, width: 60,
height: 60, height: 60,
src: item.item!.image, src: item.item!.image,
radius: 6,
), ),
], ],
), ),

View File

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

View File

@ -1,7 +1,10 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/models/msg/system.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'; import 'controller.dart';
class MessageSystemPage extends StatefulWidget { class MessageSystemPage extends StatefulWidget {
@ -97,6 +100,13 @@ class SystemItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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( return Padding(
padding: const EdgeInsets.fromLTRB(14, 14, 14, 12), padding: const EdgeInsets.fromLTRB(14, 14, 14, 12),
child: Column( child: Column(
@ -111,9 +121,73 @@ class SystemItem extends StatelessWidget {
style: TextStyle(color: Theme.of(context).colorScheme.outline), style: TextStyle(color: Theme.of(context).colorScheme.outline),
), ),
const SizedBox(height: 6), 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, setKey: SettingBoxKey.enableAi,
defaultVal: true, defaultVal: true,
), ),
SetSwitchItem(
title: '视频简介默认展开',
setKey: SettingBoxKey.enableAutoExpand,
defaultVal: false,
callFn: (val) {
GlobalDataCache().enableAutoExpand = val;
},
),
const SetSwitchItem( const SetSwitchItem(
title: '相关视频推荐', title: '相关视频推荐',
subTitle: '视频详情页推荐相关视频', subTitle: '视频详情页推荐相关视频',

View File

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

View File

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

View File

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

View File

@ -59,10 +59,19 @@ class _GroupPanelState extends State<GroupPanel> {
AppBar( AppBar(
centerTitle: false, centerTitle: false,
elevation: 0, elevation: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
leading: IconButton( leading: IconButton(
onPressed: () => Get.back(), onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)), 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( Expanded(
child: Material( child: Material(
@ -115,7 +124,7 @@ class _GroupPanelState extends State<GroupPanel> {
} }
} else { } else {
// 骨架屏 // 骨架屏
return const Text('请求中'); return const Center(child: Text('请求中'));
} }
}, },
), ),

View File

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

View File

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

View File

@ -764,14 +764,14 @@ InlineSpan buildContent(
}); });
} else { } else {
Uri uri = Uri.parse(matchStr.replaceAll('/?', '?')); Uri uri = Uri.parse(matchStr.replaceAll('/?', '?'));
SchemeEntity scheme = SchemeEntity( Uri scheme = Uri(
scheme: uri.scheme, scheme: uri.scheme,
host: uri.host, host: uri.host,
port: uri.port, port: uri.port,
path: uri.path, path: uri.path,
query: uri.queryParameters, // query: uri.queryParameters,
source: '', // source: '',
dataString: matchStr, // dataString: matchStr,
); );
PiliSchame.httpsScheme(scheme); 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'; import 'package:pilipala/models/video/reply/item.dart';
class VideoReplyReplyController extends GetxController { class VideoReplyReplyController extends GetxController {
VideoReplyReplyController(this.aid, this.rpid, this.replyType); VideoReplyReplyController(this.aid, this.rpid, this.replyType, this.showRoot);
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
// 视频aid 请求时使用的oid // 视频aid 请求时使用的oid
int? aid; int? aid;
// rpid 请求楼中楼回复 // rpid 请求楼中楼回复
String? rpid; String? rpid;
ReplyType replyType = ReplyType.video; ReplyType replyType = ReplyType.video;
bool showRoot = false;
ReplyItemModel? rootReply;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs; RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
// 当前页 // 当前页
int currentPage = 0; int currentPage = 0;
@ -42,6 +44,7 @@ class VideoReplyReplyController extends GetxController {
); );
if (res['status']) { if (res['status']) {
final List<ReplyItemModel> replies = res['data'].replies; final List<ReplyItemModel> replies = res['data'].replies;
ReplyItemModel? root = res['data'].root;
if (replies.isNotEmpty) { if (replies.isNotEmpty) {
noMore.value = '加载中...'; noMore.value = '加载中...';
if (replies.length == res['data'].page.count) { if (replies.length == res['data'].page.count) {
@ -60,7 +63,9 @@ class VideoReplyReplyController extends GetxController {
return; return;
} }
replyList.addAll(replies); replyList.addAll(replies);
// res['data'].replies.addAll(replyList); }
if (showRoot && root != null) {
rootReply = root;
} }
} }
if (replyList.isNotEmpty && currentReply != null) { 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/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart'; import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'controller.dart'; import 'controller.dart';
class VideoReplyReplyPanel extends StatefulWidget { class VideoReplyReplyPanel extends StatefulWidget {
@ -22,6 +21,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.sheetHeight, this.sheetHeight,
this.currentReply, this.currentReply,
this.loadMore = true, this.loadMore = true,
this.showRoot = false,
super.key, super.key,
}); });
final int? oid; final int? oid;
@ -33,6 +33,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
final double? sheetHeight; final double? sheetHeight;
final dynamic currentReply; final dynamic currentReply;
final bool loadMore; final bool loadMore;
final bool showRoot;
@override @override
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState(); State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
@ -49,7 +50,11 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
void initState() { void initState() {
_videoReplyReplyController = Get.put( _videoReplyReplyController = Get.put(
VideoReplyReplyController( VideoReplyReplyController(
widget.oid, widget.rpid.toString(), widget.replyType!), widget.oid,
widget.rpid.toString(),
widget.replyType!,
widget.showRoot,
),
tag: widget.rpid.toString()); tag: widget.rpid.toString());
super.initState(); super.initState();
@ -80,15 +85,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
super.dispose(); super.dispose();
} }
@override Widget _buildAppBar() {
Widget build(BuildContext context) { return AppBar(
return Container(
height: widget.source == 'videoDetail' ? widget.sheetHeight : null,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (widget.source == 'videoDetail')
AppBar(
toolbarHeight: 45, toolbarHeight: 45,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
centerTitle: false, centerTitle: false,
@ -101,13 +99,87 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
icon: const Icon(Icons.close, size: 20), icon: const Icon(Icons.close, size: 20),
onPressed: () { onPressed: () {
_videoReplyReplyController.currentPage = 0; _videoReplyReplyController.currentPage = 0;
widget.closePanel?.call; widget.closePanel?.call();
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
const SizedBox(width: 14), 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( Expanded(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
@ -120,28 +192,22 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
child: CustomScrollView( child: CustomScrollView(
controller: _videoReplyReplyController.scrollController, controller: _videoReplyReplyController.scrollController,
slivers: <Widget>[ slivers: <Widget>[
if (widget.firstFloor != null) ...[ if (widget.firstFloor != null)
// const SliverToBoxAdapter(child: SizedBox(height: 10)),
SliverToBoxAdapter( SliverToBoxAdapter(
child: ReplyItem( child: Container(
replyItem: widget.firstFloor, decoration: BoxDecoration(
replyLevel: '2', border: Border(
showReplyRow: false, bottom: BorderSide(
addReply: (replyItem) { color: Theme.of(context)
_videoReplyReplyController.replyList.add(replyItem); .dividerColor
}, .withOpacity(0.1),
replyType: widget.replyType, width: 6,
replyReply: (replyItem) => replyReply(replyItem),
), ),
), ),
SliverToBoxAdapter( ),
child: Divider( child: _buildReplyItem(widget.firstFloor, '2'),
height: 20,
color: Theme.of(context).dividerColor.withOpacity(0.1),
thickness: 6,
), ),
), ),
],
widget.loadMore widget.loadMore
? FutureBuilder( ? FutureBuilder(
future: _futureBuilderFuture, future: _futureBuilderFuture,
@ -150,76 +216,21 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
ConnectionState.done) { ConnectionState.done) {
Map? data = snapshot.data; Map? data = snapshot.data;
if (data != null && data['status']) { if (data != null && data['status']) {
// 请求成功 return _buildSliverList();
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,
),
),
),
),
);
} else { } 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( return HttpError(
errMsg: data?['msg'] ?? '请求错误', errMsg: data?['msg'] ?? '请求错误',
fn: () => setState(() {}), fn: () => setState(() {}),
); );
} }
} else { } else {
// 骨架屏
return SliverList( return SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
return const VideoReplySkeleton(); 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) { appScheme.getInitScheme().then((SchemeEntity? value) {
if (value != null) { if (value != null) {
_routePush(value); routePush(value);
} }
}); });
appScheme.getLatestScheme().then((SchemeEntity? value) { appScheme.getLatestScheme().then((SchemeEntity? value) {
if (value != null) { if (value != null) {
_routePush(value); routePush(value);
} }
}); });
appScheme.registerSchemeListener().listen((SchemeEntity? event) { appScheme.registerSchemeListener().listen((SchemeEntity? event) {
if (event != null) { if (event != null) {
_routePush(event); routePush(event);
} }
}); });
} }
/// 路由跳转 /// 路由跳转
static void _routePush(value) async { static void routePush(value) async {
final String scheme = value.scheme; final String scheme = value.scheme;
if (scheme == 'bilibili') { if (scheme == 'bilibili') {
biliScheme(value); biliScheme(value);
} }
if (scheme == 'https') { if (['http', 'https'].contains(scheme)) {
httpsScheme(value); 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
// https | m.bilibili.com | /bangumi/play/ss39708 // https | m.bilibili.com | /bangumi/play/ss39708
// final String scheme = value.scheme!; // final String scheme = value.scheme!;
final String host = value.host!; final String host = value.host;
final String? path = value.path; final String path = value.path;
Map<String, String>? query = value.query; Map<String, String>? query = value.queryParameters;
RegExp regExp = RegExp(r'^((www\.)|(m\.))?bilibili\.com$'); RegExp regExp = RegExp(r'^((www\.)|(m\.))?bilibili\.com$');
if (regExp.hasMatch(host)) { if (regExp.hasMatch(host)) {
final String lastPathSegment = path!.split('/').last; final String lastPathSegment = path.split('/').last;
if (path.startsWith('/video')) { if (path.startsWith('/video')) {
Map matchRes = IdUtils.matchAvorBv(input: path); Map matchRes = IdUtils.matchAvorBv(input: path);
if (matchRes.containsKey('AV')) { if (matchRes.containsKey('AV')) {
@ -113,13 +113,13 @@ class PiliSchame {
_videoPush(Utils.matchNum(path.split('?').first).first, null); _videoPush(Utils.matchNum(path.split('?').first).first, null);
} }
} else if (host.contains('live')) { } else if (host.contains('live')) {
int roomId = int.parse(path!.split('/').last); int roomId = int.parse(path.split('/').last);
Get.toNamed( Get.toNamed(
'/liveRoom?roomid=$roomId', '/liveRoom?roomid=$roomId',
arguments: {'liveItem': null, 'heroTag': roomId.toString()}, arguments: {'liveItem': null, 'heroTag': roomId.toString()},
); );
} else if (host.contains('space')) { } else if (host.contains('space')) {
var mid = path!.split('/').last; var mid = path.split('/').last;
Get.toNamed('/member?mid=$mid', arguments: {'face': ''}); Get.toNamed('/member?mid=$mid', arguments: {'face': ''});
return; return;
} else if (host == 'b23.tv') { } else if (host == 'b23.tv') {
@ -154,7 +154,7 @@ class PiliSchame {
parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''}, parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''},
); );
} }
} else if (path != null) { } else {
final String area = path.split('/').last; final String area = path.split('/').last;
switch (area) { switch (area) {
case 'bangumi': case 'bangumi':
@ -178,12 +178,12 @@ class PiliSchame {
break; break;
case 'read': case 'read':
print('专栏'); print('专栏');
String id = Utils.matchNum(query!['id']!).first.toString(); String id = Utils.matchNum(query['id']!).first.toString();
Get.toNamed('/read', parameters: { Get.toNamed('/read', parameters: {
'url': value.dataString!, 'url': value.toString(),
'title': '', 'title': '',
'id': id, 'id': id,
'articleType': 'read' 'articleType': 'read',
}); });
break; break;
case 'space': case 'space':
@ -201,9 +201,9 @@ class PiliSchame {
Get.toNamed( Get.toNamed(
'/webview', '/webview',
parameters: { parameters: {
'url': value.dataString ?? "", 'url': value.toString(),
'type': 'url', 'type': 'url',
'pageTitle': '' 'pageTitle': '',
}, },
); );
} }

View File

@ -49,6 +49,10 @@ class GlobalDataCache {
late List historyCacheList; late List historyCacheList;
// //
late bool enableSearchSuggest = true; late bool enableSearchSuggest = true;
// 简介默认展开
late bool enableAutoExpand = false;
//
late bool enableDynamicSwitch = true;
// 私有构造函数 // 私有构造函数
GlobalDataCache._(); GlobalDataCache._();
@ -112,5 +116,9 @@ class GlobalDataCache {
historyCacheList = localCache.get('cacheList', defaultValue: []); historyCacheList = localCache.get('cacheList', defaultValue: []);
enableSearchSuggest = enableSearchSuggest =
setting.get(SettingBoxKey.enableSearchSuggest, defaultValue: true); 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', enableSearchWord = 'enableSearchWord',
enableSystemProxy = 'enableSystemProxy', enableSystemProxy = 'enableSystemProxy',
enableAi = 'enableAi', enableAi = 'enableAi',
enableAutoExpand = 'enableAutoExpand',
defaultHomePage = 'defaultHomePage', defaultHomePage = 'defaultHomePage',
enableRelatedVideo = 'enableRelatedVideo'; enableRelatedVideo = 'enableRelatedVideo';
@ -130,6 +131,7 @@ class SettingBoxKey {
tabbarSort = 'tabbarSort', // 首页tabbar tabbarSort = 'tabbarSort', // 首页tabbar
dynamicBadgeMode = 'dynamicBadgeMode', dynamicBadgeMode = 'dynamicBadgeMode',
enableGradientBg = 'enableGradientBg', enableGradientBg = 'enableGradientBg',
enableDynamicSwitch = 'enableDynamicSwitch',
navBarSort = 'navBarSort', navBarSort = 'navBarSort',
actionTypeSort = 'actionTypeSort'; actionTypeSort = 'actionTypeSort';
} }

View File

@ -145,22 +145,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.1" 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: brotli:
dependency: "direct main" dependency: "direct main"
description: description:

View File

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