opt: pagesList style
This commit is contained in:
@ -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,128 +259,308 @@ 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)
|
||||||
onTap: () {
|
: _buildInkWell(context, title, primary, onSurface);
|
||||||
SmartDialog.showToast('切换至「$title」');
|
}
|
||||||
changeFucCall.call(episode, index);
|
|
||||||
},
|
Widget _buildListTile(
|
||||||
dense: false,
|
BuildContext context, String title, Color primary, Color onSurface) {
|
||||||
leading: isCurrentIndex
|
return ListTile(
|
||||||
? Image.asset(
|
onTap: () {
|
||||||
'assets/images/live.gif',
|
if (isCurrentIndex) {
|
||||||
color: primary,
|
return;
|
||||||
height: 12,
|
}
|
||||||
|
SmartDialog.showToast('切换至「$title」');
|
||||||
|
changeFucCall.call(episode, index);
|
||||||
|
},
|
||||||
|
dense: false,
|
||||||
|
leading: isCurrentIndex
|
||||||
|
? Image.asset(
|
||||||
|
'assets/images/live.gif',
|
||||||
|
color: primary,
|
||||||
|
height: 12,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: isCurrentIndex ? primary : onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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(
|
||||||
|
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: 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(
|
||||||
|
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),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
],
|
||||||
title: Text(title,
|
),
|
||||||
style: TextStyle(
|
);
|
||||||
fontSize: 14,
|
},
|
||||||
color: isCurrentIndex ? primary : onSurface,
|
),
|
||||||
)))
|
),
|
||||||
: InkWell(
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: () {
|
onTap: () {
|
||||||
SmartDialog.showToast('切换至「$title」');
|
if (isCurrentIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SmartDialog.showToast('切换至「${episode.title}」');
|
||||||
changeFucCall.call(episode, index);
|
changeFucCall.call(episode, index);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8),
|
child: Column(
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
NetworkImgLayer(
|
Text(
|
||||||
width: 130, height: 75, src: episode?.cover ?? ''),
|
dataType == VideoEpidoesType.bangumiEpisode
|
||||||
const SizedBox(width: 10),
|
? '第${index + 1}话'
|
||||||
Expanded(
|
: '第${index + 1}p',
|
||||||
child: Text(
|
style: textStyle),
|
||||||
title,
|
const SizedBox(height: 1),
|
||||||
maxLines: 2,
|
Text(
|
||||||
style: TextStyle(
|
episode.title,
|
||||||
fontSize: 14,
|
maxLines: 1,
|
||||||
color: isCurrentIndex ? primary : onSurface,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
style: textStyle,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
|
||||||
|
|
||||||
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: [
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
if (dataType == VideoEpidoesType.bangumiEpisode &&
|
||||||
});
|
episode.badge != '' &&
|
||||||
}
|
episode.badge != null)
|
||||||
|
Positioned(
|
||||||
/// The [BuildContext] of the widget that calls the bottom sheet.
|
right: 8,
|
||||||
PersistentBottomSheetController show(BuildContext context) {
|
top: 18,
|
||||||
final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
|
child: Text(
|
||||||
context: context,
|
episode.badge,
|
||||||
builder: (BuildContext context) {
|
style: const TextStyle(fontSize: 11, color: Color(0xFFFF6699)),
|
||||||
return buildShowContent(context);
|
),
|
||||||
},
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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!;
|
||||||
}
|
}
|
||||||
@ -608,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(
|
||||||
@ -621,7 +623,7 @@ class VideoIntroController extends GetxController {
|
|||||||
}
|
}
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
},
|
},
|
||||||
).buildShowContent(Get.context!),
|
).buildShowContent(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user