Merge branch 'design'
This commit is contained in:
@ -1,35 +1,249 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import '../models/common/video_episode_type.dart';
|
||||
import 'widgets/badge.dart';
|
||||
import 'widgets/stat/danmu.dart';
|
||||
import 'widgets/stat/view.dart';
|
||||
|
||||
class EpisodeBottomSheet {
|
||||
final List<dynamic> episodes;
|
||||
final int currentCid;
|
||||
final dynamic dataType;
|
||||
final BuildContext context;
|
||||
final Function changeFucCall;
|
||||
final int? cid;
|
||||
final double? sheetHeight;
|
||||
bool isFullScreen = false;
|
||||
final UgcSeason? ugcSeason;
|
||||
|
||||
EpisodeBottomSheet({
|
||||
required this.episodes,
|
||||
required this.currentCid,
|
||||
required this.dataType,
|
||||
required this.context,
|
||||
required this.changeFucCall,
|
||||
this.cid,
|
||||
this.sheetHeight,
|
||||
this.isFullScreen = false,
|
||||
this.ugcSeason,
|
||||
});
|
||||
|
||||
Widget buildEpisodeListItem(
|
||||
dynamic episode,
|
||||
int index,
|
||||
bool isCurrentIndex,
|
||||
) {
|
||||
Widget buildShowContent() {
|
||||
return PagesBottomSheet(
|
||||
episodes: episodes,
|
||||
currentCid: currentCid,
|
||||
dataType: dataType,
|
||||
changeFucCall: changeFucCall,
|
||||
cid: cid,
|
||||
sheetHeight: sheetHeight,
|
||||
isFullScreen: isFullScreen,
|
||||
ugcSeason: ugcSeason,
|
||||
);
|
||||
}
|
||||
|
||||
PersistentBottomSheetController show(BuildContext context) {
|
||||
final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return buildShowContent();
|
||||
},
|
||||
);
|
||||
return btmSheetCtr;
|
||||
}
|
||||
}
|
||||
|
||||
class PagesBottomSheet extends StatefulWidget {
|
||||
const PagesBottomSheet({
|
||||
super.key,
|
||||
required this.episodes,
|
||||
required this.currentCid,
|
||||
required this.dataType,
|
||||
required this.changeFucCall,
|
||||
this.cid,
|
||||
this.sheetHeight,
|
||||
this.isFullScreen = false,
|
||||
this.ugcSeason,
|
||||
});
|
||||
|
||||
final List<dynamic> episodes;
|
||||
final int currentCid;
|
||||
final dynamic dataType;
|
||||
final Function changeFucCall;
|
||||
final int? cid;
|
||||
final double? sheetHeight;
|
||||
final bool isFullScreen;
|
||||
final UgcSeason? ugcSeason;
|
||||
|
||||
@override
|
||||
State<PagesBottomSheet> createState() => _PagesBottomSheetState();
|
||||
}
|
||||
|
||||
class _PagesBottomSheetState extends State<PagesBottomSheet> {
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
late int currentIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentIndex =
|
||||
widget.episodes.indexWhere((dynamic e) => e.cid == widget.currentCid);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (widget.dataType == VideoEpidoesType.videoEpisode) {
|
||||
_itemScrollController.jumpTo(index: currentIndex);
|
||||
} else {
|
||||
double itemHeight = (widget.isFullScreen
|
||||
? 400
|
||||
: Get.size.width - 3 * StyleString.safeSpace) /
|
||||
5.2;
|
||||
double offset = ((currentIndex - 1) / 2).ceil() * itemHeight;
|
||||
_scrollController.jumpTo(offset);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String prefix() {
|
||||
switch (widget.dataType) {
|
||||
case VideoEpidoesType.videoEpisode:
|
||||
return '选集';
|
||||
case VideoEpidoesType.videoPart:
|
||||
return '分集';
|
||||
case VideoEpidoesType.bangumiEpisode:
|
||||
return '选集';
|
||||
}
|
||||
return '选集';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return SizedBox(
|
||||
height: widget.sheetHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
TitleBar(
|
||||
title: '${prefix()}(${widget.episodes.length})',
|
||||
isFullScreen: widget.isFullScreen,
|
||||
),
|
||||
if (widget.ugcSeason != null) ...[
|
||||
UgcSeasonBuild(ugcSeason: widget.ugcSeason!),
|
||||
],
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: widget.dataType == VideoEpidoesType.videoEpisode
|
||||
? ScrollablePositionedList.builder(
|
||||
itemScrollController: _itemScrollController,
|
||||
itemCount: widget.episodes.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
bool isLastItem = index == widget.episodes.length;
|
||||
bool isCurrentIndex = currentIndex == index;
|
||||
return isLastItem
|
||||
? SizedBox(
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
20,
|
||||
)
|
||||
: EpisodeListItem(
|
||||
episode: widget.episodes[index],
|
||||
index: index,
|
||||
isCurrentIndex: isCurrentIndex,
|
||||
dataType: widget.dataType,
|
||||
changeFucCall: widget.changeFucCall,
|
||||
isFullScreen: widget.isFullScreen,
|
||||
);
|
||||
},
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0), // 设置左右间距为12
|
||||
child: GridView.count(
|
||||
controller: _scrollController,
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: StyleString.safeSpace,
|
||||
childAspectRatio: 2.6,
|
||||
children: List.generate(
|
||||
widget.episodes.length,
|
||||
(index) {
|
||||
bool isCurrentIndex = currentIndex == index;
|
||||
return EpisodeGridItem(
|
||||
episode: widget.episodes[index],
|
||||
index: index,
|
||||
isCurrentIndex: isCurrentIndex,
|
||||
dataType: widget.dataType,
|
||||
changeFucCall: widget.changeFucCall,
|
||||
isFullScreen: widget.isFullScreen,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TitleBar extends StatelessWidget {
|
||||
final String title;
|
||||
final bool isFullScreen;
|
||||
|
||||
const TitleBar({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.isFullScreen,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
toolbarHeight: 45,
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: !isFullScreen
|
||||
? [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EpisodeListItem extends StatelessWidget {
|
||||
final dynamic episode;
|
||||
final int index;
|
||||
final bool isCurrentIndex;
|
||||
final dynamic dataType;
|
||||
final Function changeFucCall;
|
||||
final bool isFullScreen;
|
||||
|
||||
const EpisodeListItem({
|
||||
Key? key,
|
||||
required this.episode,
|
||||
required this.index,
|
||||
required this.isCurrentIndex,
|
||||
required this.dataType,
|
||||
required this.changeFucCall,
|
||||
required this.isFullScreen,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color primary = Theme.of(context).colorScheme.primary;
|
||||
Color onSurface = Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
@ -45,128 +259,308 @@ class EpisodeBottomSheet {
|
||||
title = '第${episode.title}话 ${episode.longTitle!}';
|
||||
break;
|
||||
}
|
||||
|
||||
return isFullScreen || episode?.cover == null || episode?.cover == ''
|
||||
? ListTile(
|
||||
onTap: () {
|
||||
SmartDialog.showToast('切换至「$title」');
|
||||
changeFucCall.call(episode, index);
|
||||
},
|
||||
dense: false,
|
||||
leading: isCurrentIndex
|
||||
? Image.asset(
|
||||
'assets/images/live.gif',
|
||||
color: primary,
|
||||
height: 12,
|
||||
? _buildListTile(context, title, primary, onSurface)
|
||||
: _buildInkWell(context, title, primary, onSurface);
|
||||
}
|
||||
|
||||
Widget _buildListTile(
|
||||
BuildContext context, String title, Color primary, Color onSurface) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
if (isCurrentIndex) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showToast('切换至「$title」');
|
||||
changeFucCall.call(episode, index);
|
||||
},
|
||||
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: () {
|
||||
SmartDialog.showToast('切换至「$title」');
|
||||
if (isCurrentIndex) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showToast('切换至「${episode.title}」');
|
||||
changeFucCall.call(episode, index);
|
||||
},
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8),
|
||||
child: Row(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 130, height: 75, src: episode?.cover ?? ''),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isCurrentIndex ? primary : onSurface,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 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);
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -144,6 +144,7 @@ class EpisodeItem {
|
||||
this.link,
|
||||
this.longTitle,
|
||||
this.pubTime,
|
||||
this.pubdate,
|
||||
this.pv,
|
||||
this.releaseDate,
|
||||
this.rights,
|
||||
@ -155,6 +156,7 @@ class EpisodeItem {
|
||||
this.subtitle,
|
||||
this.title,
|
||||
this.vid,
|
||||
this.stat,
|
||||
});
|
||||
|
||||
int? aid;
|
||||
@ -173,6 +175,7 @@ class EpisodeItem {
|
||||
String? link;
|
||||
String? longTitle;
|
||||
int? pubTime;
|
||||
int? pubdate;
|
||||
int? pv;
|
||||
String? releaseDate;
|
||||
Map? rights;
|
||||
@ -184,6 +187,7 @@ class EpisodeItem {
|
||||
String? subtitle;
|
||||
String? title;
|
||||
String? vid;
|
||||
String? stat;
|
||||
|
||||
EpisodeItem.fromJson(Map<String, dynamic> json) {
|
||||
aid = json['aid'];
|
||||
@ -202,6 +206,7 @@ class EpisodeItem {
|
||||
link = json['link'];
|
||||
longTitle = json['long_title'];
|
||||
pubTime = json['pub_time'];
|
||||
pubdate = json['pub_time'];
|
||||
pv = json['pv'];
|
||||
releaseDate = json['release_date'];
|
||||
rights = json['rights'];
|
||||
@ -211,7 +216,7 @@ class EpisodeItem {
|
||||
skip = json['skip'];
|
||||
status = json['status'];
|
||||
subtitle = json['subtitle'];
|
||||
title = json['title'];
|
||||
title = json['long_title'];
|
||||
vid = json['vid'];
|
||||
}
|
||||
}
|
||||
|
@ -101,6 +101,7 @@ class ReplyReplyData {
|
||||
this.page,
|
||||
this.config,
|
||||
this.replies,
|
||||
this.root,
|
||||
this.topReplies,
|
||||
this.upper,
|
||||
});
|
||||
@ -108,6 +109,7 @@ class ReplyReplyData {
|
||||
ReplyPage? page;
|
||||
ReplyConfig? config;
|
||||
late List<ReplyItemModel>? replies;
|
||||
ReplyItemModel? root;
|
||||
late List<ReplyItemModel>? topReplies;
|
||||
ReplyUpper? upper;
|
||||
|
||||
@ -120,6 +122,9 @@ class ReplyReplyData {
|
||||
(item) => ReplyItemModel.fromJson(item, json['upper']['mid']))
|
||||
.toList()
|
||||
: [];
|
||||
root = json['root'] != null
|
||||
? ReplyItemModel.fromJson(json['root'], false)
|
||||
: null;
|
||||
topReplies = json['top_replies'] != null
|
||||
? json['top_replies']
|
||||
.map<ReplyItemModel>((item) => ReplyItemModel.fromJson(
|
||||
|
@ -377,6 +377,7 @@ class Part {
|
||||
int? page;
|
||||
String? from;
|
||||
String? pagePart;
|
||||
String? title;
|
||||
int? duration;
|
||||
String? vid;
|
||||
String? weblink;
|
||||
@ -389,6 +390,7 @@ class Part {
|
||||
this.page,
|
||||
this.from,
|
||||
this.pagePart,
|
||||
this.title,
|
||||
this.duration,
|
||||
this.vid,
|
||||
this.weblink,
|
||||
@ -406,6 +408,7 @@ class Part {
|
||||
page = json["page"];
|
||||
from = json["from"];
|
||||
pagePart = json["part"];
|
||||
title = json["part"];
|
||||
duration = json["duration"];
|
||||
vid = json["vid"];
|
||||
weblink = json["weblink"];
|
||||
@ -649,6 +652,9 @@ class EpisodeItem {
|
||||
Part? page;
|
||||
String? bvid;
|
||||
String? cover;
|
||||
int? pubdate;
|
||||
int? duration;
|
||||
Stat? stat;
|
||||
|
||||
EpisodeItem.fromJson(Map<String, dynamic> json) {
|
||||
seasonId = json['season_id'];
|
||||
@ -661,6 +667,9 @@ class EpisodeItem {
|
||||
page = Part.fromJson(json['page']);
|
||||
bvid = json['bvid'];
|
||||
cover = json['arc']['pic'];
|
||||
pubdate = json['arc']['pubdate'];
|
||||
duration = json['arc']['duration'];
|
||||
stat = Stat.fromJson(json['arc']['stat']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,14 +302,13 @@ class BangumiIntroController extends GetxController {
|
||||
episodes: episodes,
|
||||
currentCid: videoDetailCtr.cid.value,
|
||||
dataType: dataType,
|
||||
context: Get.context!,
|
||||
sheetHeight: Get.size.height,
|
||||
isFullScreen: true,
|
||||
changeFucCall: (item, index) {
|
||||
changeSeasonOrbangu(item.bvid, item.cid, item.aid, item.cover);
|
||||
SmartDialog.dismiss();
|
||||
},
|
||||
).buildShowContent(Get.context!),
|
||||
).buildShowContent(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -146,17 +146,34 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
||||
}
|
||||
|
||||
// 收藏
|
||||
showFavBottomSheet() {
|
||||
showFavBottomSheet() async {
|
||||
if (bangumiIntroController.userInfo.mid == null) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
final mediaQueryData = MediaQuery.of(context);
|
||||
final contentHeight = mediaQueryData.size.height - kToolbarHeight;
|
||||
final double initialChildSize =
|
||||
(contentHeight - Get.width * 9 / 16) / contentHeight;
|
||||
await showModalBottomSheet(
|
||||
context: Get.context!,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return FavPanel(ctr: bangumiIntroController);
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: initialChildSize,
|
||||
minChildSize: 0,
|
||||
maxChildSize: 1,
|
||||
snap: true,
|
||||
expand: false,
|
||||
snapSizes: [initialChildSize],
|
||||
builder: (BuildContext context, ScrollController scrollController) {
|
||||
return FavPanel(
|
||||
ctr: bangumiIntroController,
|
||||
scrollController: scrollController,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -151,7 +151,6 @@ class _BangumiPanelState extends State<BangumiPanel> {
|
||||
changeFucCall: changeFucCall,
|
||||
sheetHeight: widget.sheetHeight,
|
||||
dataType: VideoEpidoesType.bangumiEpisode,
|
||||
context: context,
|
||||
).show(context);
|
||||
},
|
||||
child: Text(
|
||||
|
@ -11,6 +11,7 @@ import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/plugin/pl_popup/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/global_data_cache.dart';
|
||||
import 'package:pilipala/utils/main_stream.dart';
|
||||
import 'package:pilipala/utils/route_push.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
@ -90,32 +91,34 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Obx(() {
|
||||
if (_dynamicsController.mid.value != -1 &&
|
||||
_dynamicsController.upInfo.value.uname != null) {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
'${_dynamicsController.upInfo.value.uname!}的动态',
|
||||
key: ValueKey<String>(
|
||||
_dynamicsController.upInfo.value.uname!),
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.fontSize,
|
||||
)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final mid = _dynamicsController.mid.value;
|
||||
final uname = _dynamicsController.upInfo.value.uname;
|
||||
|
||||
if (mid == -1 || uname == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
'$uname的动态',
|
||||
key: ValueKey<String>(uname),
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge!
|
||||
.fontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(
|
||||
() => _dynamicsController.userLogin.value
|
||||
@ -207,14 +210,19 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
() => UpPanel(
|
||||
upData: _dynamicsController.upData.value,
|
||||
onClickUpCb: (data) {
|
||||
// _dynamicsController.onTapUp(data);
|
||||
Navigator.push(
|
||||
context,
|
||||
PlPopupRoute(
|
||||
child: OverlayPanel(
|
||||
ctr: _dynamicsController, upInfo: data),
|
||||
),
|
||||
);
|
||||
if (GlobalDataCache().enableDynamicSwitch) {
|
||||
Navigator.push(
|
||||
context,
|
||||
PlPopupRoute(
|
||||
child: OverlayPanel(
|
||||
ctr: _dynamicsController,
|
||||
upInfo: data,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_dynamicsController.onTapUp(data);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -66,76 +66,85 @@ Widget liveRcmdPanel(item, context, {floor = 1}) {
|
||||
},
|
||||
child: LayoutBuilder(builder: (context, box) {
|
||||
double width = box.maxWidth;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: liveRcmd.roomId.toString(),
|
||||
child: NetworkImgLayer(
|
||||
type: floor == 1 ? 'emote' : null,
|
||||
width: width,
|
||||
height: width / StyleString.aspectRatio,
|
||||
src: item.modules.moduleDynamic.major.liveRcmd.cover,
|
||||
),
|
||||
),
|
||||
PBadge(
|
||||
text: watchedShow['text_large'],
|
||||
top: 6,
|
||||
right: 56,
|
||||
bottom: null,
|
||||
left: null,
|
||||
type: 'gray',
|
||||
),
|
||||
PBadge(
|
||||
text: liveStatus == 1 ? '直播中' : '直播结束',
|
||||
top: 6,
|
||||
right: 6,
|
||||
bottom: null,
|
||||
left: null,
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Colors.transparent,
|
||||
Colors.black45,
|
||||
],
|
||||
),
|
||||
borderRadius: floor == 1
|
||||
? null
|
||||
: const BorderRadius.all(Radius.circular(6))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
DefaultTextStyle.merge(
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize,
|
||||
color: Colors.white),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(item.modules.moduleDynamic.major.liveRcmd
|
||||
.areaName ??
|
||||
''),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
return Container(
|
||||
margin: floor == 1
|
||||
? const EdgeInsets.only(
|
||||
left: StyleString.safeSpace, right: StyleString.safeSpace)
|
||||
: EdgeInsets.zero,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(StyleString.imgRadius)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: liveRcmd.roomId.toString(),
|
||||
child: NetworkImgLayer(
|
||||
type: floor == 1 ? 'emote' : null,
|
||||
width: width,
|
||||
height: width / StyleString.aspectRatio,
|
||||
src: item.modules.moduleDynamic.major.liveRcmd.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
PBadge(
|
||||
text: watchedShow['text_large'],
|
||||
top: 8.0,
|
||||
right: 62.0,
|
||||
bottom: null,
|
||||
left: null,
|
||||
type: 'gray',
|
||||
),
|
||||
PBadge(
|
||||
text: liveStatus == 1 ? '直播中' : '直播结束',
|
||||
top: 8.0,
|
||||
right: 10.0,
|
||||
bottom: null,
|
||||
left: null,
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Colors.transparent,
|
||||
Colors.black45,
|
||||
],
|
||||
),
|
||||
borderRadius: floor == 1
|
||||
? null
|
||||
: const BorderRadius.all(Radius.circular(6))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
DefaultTextStyle.merge(
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize,
|
||||
color: Colors.white),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(item.modules.moduleDynamic.major.liveRcmd
|
||||
.areaName ??
|
||||
''),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
@ -5,6 +5,7 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/dynamics/up.dart';
|
||||
import 'package:pilipala/models/live/item.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/global_data_cache.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class UpPanel extends StatefulWidget {
|
||||
@ -23,7 +24,7 @@ class UpPanel extends StatefulWidget {
|
||||
|
||||
class _UpPanelState extends State<UpPanel> {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
int currentMid = -1;
|
||||
RxInt currentMid = (-1).obs;
|
||||
late double contentWidth = 56;
|
||||
List<UpItem> upList = [];
|
||||
List<LiveUserItem> liveList = [];
|
||||
@ -37,26 +38,36 @@ class _UpPanelState extends State<UpPanel> {
|
||||
}
|
||||
|
||||
void onClickUp(data, i) {
|
||||
currentMid = data.mid;
|
||||
currentMid.value = data.mid;
|
||||
widget.onClickUpCb?.call(data);
|
||||
// int liveLen = liveList.length;
|
||||
// int upLen = upList.length;
|
||||
// double itemWidth = contentWidth + itemPadding.horizontal;
|
||||
// double screenWidth = MediaQuery.sizeOf(context).width;
|
||||
// double moveDistance = 0.0;
|
||||
// if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
|
||||
// } else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
|
||||
// moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
|
||||
// } else {
|
||||
// moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
|
||||
// }
|
||||
// data.hasUpdate = false;
|
||||
// scrollController.animateTo(
|
||||
// moveDistance,
|
||||
// duration: const Duration(milliseconds: 200),
|
||||
// curve: Curves.linear,
|
||||
// );
|
||||
// setState(() {});
|
||||
}
|
||||
|
||||
void onClickUpAni(data, i) {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
final itemWidth = contentWidth + itemPadding.horizontal;
|
||||
final liveLen = liveList.length;
|
||||
final upLen = upList.length;
|
||||
|
||||
currentMid.value = data.mid;
|
||||
widget.onClickUpCb?.call(data);
|
||||
|
||||
double moveDistance = 0.0;
|
||||
final totalItemsWidth = itemWidth * (upLen + liveLen);
|
||||
|
||||
if (totalItemsWidth > screenWidth) {
|
||||
if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
|
||||
moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
|
||||
} else {
|
||||
moveDistance = totalItemsWidth + 46 - screenWidth;
|
||||
}
|
||||
}
|
||||
|
||||
data.hasUpdate = false;
|
||||
scrollController.animateTo(
|
||||
moveDistance,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -144,14 +155,17 @@ class _UpPanelState extends State<UpPanel> {
|
||||
}
|
||||
|
||||
Widget upItemBuild(data, i) {
|
||||
bool isCurrent = currentMid == data.mid || currentMid == -1;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
feedBack();
|
||||
if (data.type == 'up') {
|
||||
EasyThrottle.throttle('follow', const Duration(milliseconds: 300),
|
||||
() {
|
||||
onClickUp(data, i);
|
||||
if (GlobalDataCache().enableDynamicSwitch) {
|
||||
onClickUp(data, i);
|
||||
} else {
|
||||
onClickUpAni(data, i);
|
||||
}
|
||||
});
|
||||
} else if (data.type == 'live') {
|
||||
LiveItemModel liveItem = LiveItemModel.fromJson({
|
||||
@ -177,60 +191,66 @@ class _UpPanelState extends State<UpPanel> {
|
||||
},
|
||||
child: Padding(
|
||||
padding: itemPadding,
|
||||
child: AnimatedOpacity(
|
||||
opacity: isCurrent ? 1 : 0.3,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Badge(
|
||||
smallSize: 8,
|
||||
label: data.type == 'live' ? const Text('Live') : null,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
alignment: data.type == 'live'
|
||||
? AlignmentDirectional.topCenter
|
||||
: AlignmentDirectional.topEnd,
|
||||
padding: const EdgeInsets.only(left: 6, right: 6),
|
||||
isLabelVisible: data.type == 'live' ||
|
||||
(data.type == 'up' && (data.hasUpdate ?? false)),
|
||||
backgroundColor: data.type == 'live'
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
child: data.face != ''
|
||||
? NetworkImgLayer(
|
||||
width: 50,
|
||||
height: 50,
|
||||
src: data.face,
|
||||
type: 'avatar',
|
||||
)
|
||||
: const CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundImage: AssetImage(
|
||||
'assets/images/noface.jpeg',
|
||||
child: Obx(
|
||||
() => AnimatedOpacity(
|
||||
opacity: currentMid.value == data.mid || currentMid.value == -1
|
||||
? 1
|
||||
: 0.3,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Badge(
|
||||
smallSize: 8,
|
||||
label: data.type == 'live' ? const Text('Live') : null,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
alignment: data.type == 'live'
|
||||
? AlignmentDirectional.topCenter
|
||||
: AlignmentDirectional.topEnd,
|
||||
padding: const EdgeInsets.only(left: 6, right: 6),
|
||||
isLabelVisible: data.type == 'live' ||
|
||||
(data.type == 'up' && (data.hasUpdate ?? false)),
|
||||
backgroundColor: data.type == 'live'
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
child: data.face != ''
|
||||
? NetworkImgLayer(
|
||||
width: 50,
|
||||
height: 50,
|
||||
src: data.face,
|
||||
type: 'avatar',
|
||||
)
|
||||
: const CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundImage: AssetImage(
|
||||
'assets/images/noface.jpeg',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: SizedBox(
|
||||
width: contentWidth,
|
||||
child: Text(
|
||||
data.uname,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: currentMid == data.mid
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.outline,
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: SizedBox(
|
||||
width: contentWidth,
|
||||
child: Text(
|
||||
data.uname,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: currentMid.value == data.mid
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.outline,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -78,72 +78,83 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
|
||||
],
|
||||
LayoutBuilder(builder: (context, box) {
|
||||
double width = box.maxWidth;
|
||||
return Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
type: floor == 1 ? 'emote' : null,
|
||||
width: width,
|
||||
height: width / StyleString.aspectRatio,
|
||||
src: content.cover,
|
||||
),
|
||||
if (content.badge != null && content.badge['text'] != null)
|
||||
PBadge(
|
||||
text: content.badge['text'],
|
||||
top: 8.0,
|
||||
right: 10.0,
|
||||
bottom: null,
|
||||
left: null,
|
||||
return Container(
|
||||
margin: floor == 1
|
||||
? const EdgeInsets.only(
|
||||
left: StyleString.safeSpace, right: StyleString.safeSpace)
|
||||
: EdgeInsets.zero,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(StyleString.imgRadius)),
|
||||
child: Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
type: floor == 1 ? 'emote' : null,
|
||||
width: width,
|
||||
height: width / StyleString.aspectRatio,
|
||||
src: content.cover,
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Colors.transparent,
|
||||
Colors.black54,
|
||||
],
|
||||
),
|
||||
borderRadius: floor == 1
|
||||
? null
|
||||
: const BorderRadius.all(Radius.circular(6))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
DefaultTextStyle.merge(
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Colors.white),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(content.durationText ?? ''),
|
||||
if (content.durationText != null)
|
||||
const SizedBox(width: 10),
|
||||
Text(content.stat.play + '次围观'),
|
||||
const SizedBox(width: 10),
|
||||
Text(content.stat.danmaku + '条弹幕')
|
||||
if (content.badge != null && content.badge['text'] != null)
|
||||
PBadge(
|
||||
text: content.badge['text'],
|
||||
top: 8.0,
|
||||
right: 10.0,
|
||||
bottom: null,
|
||||
left: null,
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Colors.transparent,
|
||||
Colors.black54,
|
||||
],
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
'assets/images/play.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
],
|
||||
borderRadius: floor == 1
|
||||
? null
|
||||
: const BorderRadius.all(Radius.circular(6))),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
DefaultTextStyle.merge(
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize,
|
||||
color: Colors.white),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(content.durationText ?? ''),
|
||||
if (content.durationText != null)
|
||||
const SizedBox(width: 10),
|
||||
Text(content.stat.play + '次围观'),
|
||||
const SizedBox(width: 10),
|
||||
Text(content.stat.danmaku + '条弹幕')
|
||||
],
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
'assets/images/play.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 6),
|
||||
|
@ -96,7 +96,7 @@ class VideoContent extends StatelessWidget {
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
[23, 1].contains(favFolderItem.attr) ? '私密' : '公开',
|
||||
[22, 0].contains(favFolderItem.attr) ? '公开' : '私密',
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
|
@ -21,6 +21,7 @@ class FavDetailController extends GetxController {
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
RxInt mediaCount = 0.obs;
|
||||
late String isOwner;
|
||||
late bool hasMore = true;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -35,7 +36,7 @@ class FavDetailController extends GetxController {
|
||||
}
|
||||
|
||||
Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async {
|
||||
if (type == 'onLoad' && favList.length >= mediaCount.value) {
|
||||
if (type == 'onLoad' && !hasMore) {
|
||||
loadingText.value = '没有更多了';
|
||||
return;
|
||||
}
|
||||
@ -47,17 +48,18 @@ class FavDetailController extends GetxController {
|
||||
);
|
||||
if (res['status']) {
|
||||
favInfo.value = res['data'].info;
|
||||
hasMore = res['data'].hasMore;
|
||||
if (currentPage == 1 && type == 'init') {
|
||||
favList.value = res['data'].medias;
|
||||
mediaCount.value = res['data'].info['media_count'];
|
||||
} else if (type == 'onLoad') {
|
||||
favList.addAll(res['data'].medias);
|
||||
}
|
||||
if (favList.length >= mediaCount.value) {
|
||||
if (!hasMore) {
|
||||
loadingText.value = '没有更多了';
|
||||
}
|
||||
currentPage += 1;
|
||||
}
|
||||
currentPage += 1;
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
}
|
||||
@ -126,7 +128,7 @@ class FavDetailController extends GetxController {
|
||||
'title': item!.title,
|
||||
'intro': item!.intro,
|
||||
'cover': item!.cover,
|
||||
'privacy': [23, 1].contains(item!.attr) ? 1 : 0,
|
||||
'privacy': [22, 0].contains(item!.attr) ? 0 : 1,
|
||||
},
|
||||
);
|
||||
title.value = res['title'];
|
||||
|
@ -193,7 +193,9 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
|
||||
child: Obx(
|
||||
() => Text(
|
||||
'共${_favDetailController.mediaCount}条视频',
|
||||
_favDetailController.mediaCount > 0
|
||||
? '共${_favDetailController.mediaCount}条视频'
|
||||
: '',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
@ -215,7 +217,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
List favList = _favDetailController.favList;
|
||||
return Obx(
|
||||
() => favList.isEmpty
|
||||
? const SliverToBoxAdapter(child: SizedBox())
|
||||
? const NoData()
|
||||
: SliverList(
|
||||
delegate:
|
||||
SliverChildBuilderDelegate((context, index) {
|
||||
@ -247,18 +249,20 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).padding.bottom + 60,
|
||||
height: MediaQuery.of(context).padding.bottom + 90,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_favDetailController.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
child: Obx(() {
|
||||
final mediaCount = _favDetailController.mediaCount;
|
||||
final loadingText = _favDetailController.loadingText.value;
|
||||
final textColor = Theme.of(context).colorScheme.outline;
|
||||
|
||||
return Text(
|
||||
mediaCount > 0 ? loadingText : '',
|
||||
style: TextStyle(color: textColor, fontSize: 13),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
@ -48,24 +47,27 @@ class FollowItem extends StatelessWidget {
|
||||
height: 34,
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
await showFlexibleBottomSheet(
|
||||
bottomSheetBorderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
minHeight: 1,
|
||||
initHeight: 1,
|
||||
maxHeight: 1,
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context,
|
||||
ScrollController scrollController, double offset) {
|
||||
return GroupPanel(
|
||||
mid: item.mid!,
|
||||
scrollController: scrollController,
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.6,
|
||||
minChildSize: 0,
|
||||
maxChildSize: 1,
|
||||
snap: true,
|
||||
expand: false,
|
||||
snapSizes: const [0.6],
|
||||
builder: (BuildContext context,
|
||||
ScrollController scrollController) {
|
||||
return GroupPanel(
|
||||
mid: item.mid!,
|
||||
scrollController: scrollController,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
anchors: [1],
|
||||
isSafeArea: true,
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
|
@ -129,7 +129,7 @@ class _LaterPageState extends State<LaterPage> {
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).padding.bottom + 10,
|
||||
height: MediaQuery.of(context).padding.bottom + 80,
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -1,13 +1,11 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/http/search.dart';
|
||||
import 'package:pilipala/models/msg/like.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
import '../utils/index.dart';
|
||||
import 'controller.dart';
|
||||
|
||||
class MessageLikePage extends StatefulWidget {
|
||||
@ -122,39 +120,13 @@ class LikeItem extends StatelessWidget {
|
||||
final nickNameList = item.users!.map((e) => e.nickname).take(2).toList();
|
||||
int usersLen = item.users!.length > 3 ? 3 : item.users!.length;
|
||||
final Uri uri = Uri.parse(item.item!.uri!);
|
||||
final String path = uri.path;
|
||||
final String bvid = path.split('/').last;
|
||||
|
||||
/// bilibili://
|
||||
final Uri nativeUri = Uri.parse(item.item!.nativeUri!);
|
||||
final Map<String, String> queryParameters = nativeUri.queryParameters;
|
||||
final String type = item.item!.type!;
|
||||
// cid
|
||||
final String? argCid = queryParameters['cid'];
|
||||
// 页码
|
||||
final String? page = queryParameters['page'];
|
||||
// 根评论id
|
||||
final String? commentRootId = queryParameters['comment_root_id'];
|
||||
// 二级评论id
|
||||
final String? commentSecondaryId = queryParameters['comment_secondary_id'];
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
try {
|
||||
final int cid = argCid != null
|
||||
? int.parse(argCid)
|
||||
: await SearchHttp.ab2c(bvid: bvid);
|
||||
final String heroTag = Utils.makeHeroTag(bvid);
|
||||
Get.toNamed<dynamic>(
|
||||
'/video?bvid=$bvid&cid=$cid',
|
||||
arguments: <String, String?>{
|
||||
'pic': '',
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('视频可能失效了$e');
|
||||
}
|
||||
MessageUtils.onClickMessage(context, uri, nativeUri, type);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
@ -243,6 +215,7 @@ class LikeItem extends StatelessWidget {
|
||||
width: 60,
|
||||
height: 60,
|
||||
src: item.item!.image,
|
||||
radius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -4,10 +4,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/http/search.dart';
|
||||
import 'package:pilipala/models/msg/reply.dart';
|
||||
import 'package:pilipala/pages/message/utils/index.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class MessageReplyPage extends StatefulWidget {
|
||||
@ -112,28 +111,14 @@ class ReplyItem extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
Color outline = Theme.of(context).colorScheme.outline;
|
||||
final String heroTag = Utils.makeHeroTag(item.user!.mid);
|
||||
final String bvid = item.item!.uri!.split('/').last;
|
||||
// 页码
|
||||
final String page =
|
||||
item.item!.nativeUri!.split('page=').last.split('&').first;
|
||||
// 根评论id
|
||||
final String commentRootId =
|
||||
item.item!.nativeUri!.split('comment_root_id=').last.split('&').first;
|
||||
// 二级评论id
|
||||
final String commentSecondaryId =
|
||||
item.item!.nativeUri!.split('comment_secondary_id=').last;
|
||||
final Uri uri = Uri.parse(item.item!.uri!);
|
||||
|
||||
/// bilibili://
|
||||
final Uri nativeUri = Uri.parse(item.item!.nativeUri!);
|
||||
final String type = item.item!.type!;
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
final int cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
final String heroTag = Utils.makeHeroTag(bvid);
|
||||
Get.toNamed<dynamic>(
|
||||
'/video?bvid=$bvid&cid=$cid',
|
||||
arguments: <String, String?>{
|
||||
'pic': '',
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
MessageUtils.onClickMessage(context, uri, nativeUri, type);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
@ -217,6 +202,7 @@ class ReplyItem extends StatelessWidget {
|
||||
width: 60,
|
||||
height: 60,
|
||||
src: item.item!.image,
|
||||
radius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,7 +1,10 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/models/msg/system.dart';
|
||||
import 'package:pilipala/pages/message/utils/index.dart';
|
||||
import 'package:pilipala/utils/app_scheme.dart';
|
||||
import 'controller.dart';
|
||||
|
||||
class MessageSystemPage extends StatefulWidget {
|
||||
@ -97,6 +100,13 @@ class SystemItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// if (item.content is Map) {
|
||||
// var res = MessageUtils().extractLinks(item.content['web']);
|
||||
// print('res: $res');
|
||||
// } else {
|
||||
// var res = MessageUtils().extractLinks(item.content);
|
||||
// print('res: $res');
|
||||
// }
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 12),
|
||||
child: Column(
|
||||
@ -111,9 +121,73 @@ class SystemItem extends StatelessWidget {
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(item.content is String ? item.content : item.content!['web']),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
buildContent(
|
||||
context,
|
||||
item.content is String ? item.content : item.content!['web']!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// if (item.content is String)
|
||||
// Text(item.content)
|
||||
// else ...[
|
||||
// Text(item.content!['web']!),
|
||||
// ]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InlineSpan buildContent(
|
||||
BuildContext context,
|
||||
String content,
|
||||
) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
final List<InlineSpan> spanChilds = <InlineSpan>[];
|
||||
Map<String, dynamic> contentMap = MessageUtils().extractLinks(content);
|
||||
List<String> keys = contentMap.keys.toList();
|
||||
keys.removeWhere((element) => element == 'message');
|
||||
String patternStr = keys.join('|');
|
||||
RegExp regExp = RegExp(patternStr, caseSensitive: false);
|
||||
|
||||
contentMap['message'].splitMapJoin(
|
||||
regExp,
|
||||
onMatch: (Match match) {
|
||||
if (!match.group(0)!.startsWith('BV')) {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: Icon(Icons.link, color: colorScheme.primary, size: 16),
|
||||
),
|
||||
);
|
||||
}
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: match.group(0),
|
||||
style: TextStyle(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
PiliSchame.routePush(Uri.parse(contentMap[match.group(0)]));
|
||||
},
|
||||
),
|
||||
);
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String text) {
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
return TextSpan(
|
||||
children: spanChilds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
141
lib/pages/message/utils/index.dart
Normal file
141
lib/pages/message/utils/index.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -183,6 +183,14 @@ class _ExtraSettingState extends State<ExtraSetting> {
|
||||
setKey: SettingBoxKey.enableAi,
|
||||
defaultVal: true,
|
||||
),
|
||||
SetSwitchItem(
|
||||
title: '视频简介默认展开',
|
||||
setKey: SettingBoxKey.enableAutoExpand,
|
||||
defaultVal: false,
|
||||
callFn: (val) {
|
||||
GlobalDataCache().enableAutoExpand = val;
|
||||
},
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '相关视频推荐',
|
||||
subTitle: '视频详情页推荐相关视频',
|
||||
|
@ -108,6 +108,12 @@ class _StyleSettingState extends State<StyleSetting> {
|
||||
defaultVal: true,
|
||||
needReboot: true,
|
||||
),
|
||||
const SetSwitchItem(
|
||||
title: '动态页滑动切换up',
|
||||
setKey: SettingBoxKey.enableDynamicSwitch,
|
||||
defaultVal: true,
|
||||
needReboot: true,
|
||||
),
|
||||
ListTile(
|
||||
onTap: () async {
|
||||
int? result = await showDialog(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bottom_sheet/bottom_sheet.dart';
|
||||
// import 'package:bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -62,6 +62,7 @@ class VideoIntroController extends GetxController {
|
||||
late ModelResult modelResult;
|
||||
PersistentBottomSheetController? bottomSheetController;
|
||||
late bool enableRelatedVideo;
|
||||
UgcSeason? ugcSeason;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -87,6 +88,7 @@ class VideoIntroController extends GetxController {
|
||||
var result = await VideoHttp.videoIntro(bvid: bvid);
|
||||
if (result['status']) {
|
||||
videoDetail.value = result['data']!;
|
||||
ugcSeason = result['data']!.ugcSeason;
|
||||
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
|
||||
lastPlayCid.value = videoDetail.value.pages!.first.cid!;
|
||||
}
|
||||
@ -531,25 +533,31 @@ class VideoIntroController extends GetxController {
|
||||
}
|
||||
|
||||
// 设置关注分组
|
||||
void setFollowGroup() {
|
||||
showFlexibleBottomSheet(
|
||||
bottomSheetBorderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
minHeight: 0.6,
|
||||
initHeight: 0.6,
|
||||
maxHeight: 1,
|
||||
void setFollowGroup() async {
|
||||
final mediaQueryData = MediaQuery.of(Get.context!);
|
||||
final contentHeight = mediaQueryData.size.height - kToolbarHeight;
|
||||
final double initialChildSize =
|
||||
(contentHeight - Get.width * 9 / 16) / contentHeight;
|
||||
await showModalBottomSheet(
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context, ScrollController scrollController,
|
||||
double offset) {
|
||||
return GroupPanel(
|
||||
mid: videoDetail.value.owner!.mid!,
|
||||
scrollController: scrollController,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: initialChildSize,
|
||||
minChildSize: 0,
|
||||
maxChildSize: 1,
|
||||
snap: true,
|
||||
expand: false,
|
||||
snapSizes: [initialChildSize],
|
||||
builder: (BuildContext context, ScrollController scrollController) {
|
||||
return GroupPanel(
|
||||
mid: videoDetail.value.owner!.mid!,
|
||||
scrollController: scrollController,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
anchors: [0.6, 1],
|
||||
isSafeArea: true,
|
||||
);
|
||||
}
|
||||
|
||||
@ -602,9 +610,9 @@ class VideoIntroController extends GetxController {
|
||||
episodes: episodes,
|
||||
currentCid: lastPlayCid.value,
|
||||
dataType: dataType,
|
||||
context: Get.context!,
|
||||
sheetHeight: Get.size.height,
|
||||
isFullScreen: true,
|
||||
ugcSeason: ugcSeason,
|
||||
changeFucCall: (item, index) {
|
||||
if (dataType == VideoEpidoesType.videoEpisode) {
|
||||
changeSeasonOrbangu(
|
||||
@ -615,7 +623,7 @@ class VideoIntroController extends GetxController {
|
||||
}
|
||||
SmartDialog.dismiss();
|
||||
},
|
||||
).buildShowContent(Get.context!),
|
||||
).buildShowContent(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:bottom_sheet/bottom_sheet.dart';
|
||||
// import 'package:bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@ -169,7 +169,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
|
||||
owner = widget.videoDetail!.owner;
|
||||
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
|
||||
_expandableCtr = ExpandableController(initialExpanded: false);
|
||||
_expandableCtr = ExpandableController(
|
||||
initialExpanded: GlobalDataCache().enableAutoExpand);
|
||||
}
|
||||
|
||||
// 收藏
|
||||
@ -198,25 +199,35 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
}
|
||||
}
|
||||
|
||||
void _showFavPanel() {
|
||||
showFlexibleBottomSheet(
|
||||
bottomSheetBorderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
void _showFavPanel() async {
|
||||
final mediaQueryData = MediaQuery.of(context);
|
||||
final contentHeight = mediaQueryData.size.height - kToolbarHeight;
|
||||
final double initialChildSize =
|
||||
(contentHeight - Get.width * 9 / 16) / contentHeight;
|
||||
await showModalBottomSheet(
|
||||
context: Get.context!,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
transitionAnimationController: AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
),
|
||||
minHeight: 0.6,
|
||||
initHeight: 0.6,
|
||||
maxHeight: 1,
|
||||
context: context,
|
||||
builder: (BuildContext context, ScrollController scrollController,
|
||||
double offset) {
|
||||
return FavPanel(
|
||||
ctr: videoIntroController,
|
||||
scrollController: scrollController,
|
||||
builder: (BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: initialChildSize,
|
||||
minChildSize: 0,
|
||||
maxChildSize: 1,
|
||||
snap: true,
|
||||
expand: false,
|
||||
snapSizes: [initialChildSize],
|
||||
builder: (BuildContext context, ScrollController scrollController) {
|
||||
return FavPanel(
|
||||
ctr: videoIntroController,
|
||||
scrollController: scrollController,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
anchors: [0.6, 1],
|
||||
isSafeArea: true,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
@ -32,8 +31,14 @@ class _FavPanelState extends State<FavPanel> {
|
||||
AppBar(
|
||||
centerTitle: false,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
leadingWidth: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close_outlined)),
|
||||
title: Text(
|
||||
'选择收藏夹',
|
||||
style: Theme.of(context)
|
||||
@ -61,16 +66,16 @@ class _FavPanelState extends State<FavPanel> {
|
||||
onTap: () =>
|
||||
widget.ctr!.onChoose(item.favState != 1, index),
|
||||
dense: true,
|
||||
leading: Icon([23, 1].contains(item.attr)
|
||||
leading: Icon([22, 0].contains(item.attr)
|
||||
? Icons.lock_outline
|
||||
: Icons.folder_outlined),
|
||||
minLeadingWidth: 0,
|
||||
title: Text(item.title!),
|
||||
subtitle: Text(
|
||||
'${item.mediaCount}个内容 - ${[
|
||||
23,
|
||||
1
|
||||
].contains(item.attr) ? '私密' : '公开'}',
|
||||
22,
|
||||
0
|
||||
].contains(item.attr) ? '公开' : '私密'}',
|
||||
),
|
||||
trailing: Transform.scale(
|
||||
scale: 0.9,
|
||||
@ -92,7 +97,7 @@ class _FavPanelState extends State<FavPanel> {
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const Text('请求中');
|
||||
return const Center(child: Text('请求中'));
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -59,10 +59,19 @@ class _GroupPanelState extends State<GroupPanel> {
|
||||
AppBar(
|
||||
centerTitle: false,
|
||||
elevation: 0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close_outlined)),
|
||||
title: Text('设置关注分组', style: Theme.of(context).textTheme.titleMedium),
|
||||
title: Text('设置关注分组',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
@ -115,7 +124,7 @@ class _GroupPanelState extends State<GroupPanel> {
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const Text('请求中');
|
||||
return const Center(child: Text('请求中'));
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -116,7 +116,6 @@ class _PagesPanelState extends State<PagesPanel> {
|
||||
changeFucCall: changeFucCall,
|
||||
sheetHeight: widget.sheetHeight,
|
||||
dataType: VideoEpidoesType.videoPart,
|
||||
context: context,
|
||||
).show(context);
|
||||
},
|
||||
child: Text(
|
||||
|
@ -124,7 +124,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
changeFucCall: changeFucCall,
|
||||
sheetHeight: widget.sheetHeight,
|
||||
dataType: VideoEpidoesType.videoEpisode,
|
||||
context: context,
|
||||
ugcSeason: widget.ugcSeason,
|
||||
).show(context);
|
||||
},
|
||||
child: Padding(
|
||||
|
@ -764,14 +764,14 @@ InlineSpan buildContent(
|
||||
});
|
||||
} else {
|
||||
Uri uri = Uri.parse(matchStr.replaceAll('/?', '?'));
|
||||
SchemeEntity scheme = SchemeEntity(
|
||||
Uri scheme = Uri(
|
||||
scheme: uri.scheme,
|
||||
host: uri.host,
|
||||
port: uri.port,
|
||||
path: uri.path,
|
||||
query: uri.queryParameters,
|
||||
source: '',
|
||||
dataString: matchStr,
|
||||
// query: uri.queryParameters,
|
||||
// source: '',
|
||||
// dataString: matchStr,
|
||||
);
|
||||
PiliSchame.httpsScheme(scheme);
|
||||
}
|
||||
|
@ -5,13 +5,15 @@ import 'package:pilipala/models/common/reply_type.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
|
||||
class VideoReplyReplyController extends GetxController {
|
||||
VideoReplyReplyController(this.aid, this.rpid, this.replyType);
|
||||
VideoReplyReplyController(this.aid, this.rpid, this.replyType, this.showRoot);
|
||||
final ScrollController scrollController = ScrollController();
|
||||
// 视频aid 请求时使用的oid
|
||||
int? aid;
|
||||
// rpid 请求楼中楼回复
|
||||
String? rpid;
|
||||
ReplyType replyType = ReplyType.video;
|
||||
bool showRoot = false;
|
||||
ReplyItemModel? rootReply;
|
||||
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
|
||||
// 当前页
|
||||
int currentPage = 0;
|
||||
@ -42,6 +44,7 @@ class VideoReplyReplyController extends GetxController {
|
||||
);
|
||||
if (res['status']) {
|
||||
final List<ReplyItemModel> replies = res['data'].replies;
|
||||
ReplyItemModel? root = res['data'].root;
|
||||
if (replies.isNotEmpty) {
|
||||
noMore.value = '加载中...';
|
||||
if (replies.length == res['data'].page.count) {
|
||||
@ -60,7 +63,9 @@ class VideoReplyReplyController extends GetxController {
|
||||
return;
|
||||
}
|
||||
replyList.addAll(replies);
|
||||
// res['data'].replies.addAll(replyList);
|
||||
}
|
||||
if (showRoot && root != null) {
|
||||
rootReply = root;
|
||||
}
|
||||
}
|
||||
if (replyList.isNotEmpty && currentReply != null) {
|
||||
|
@ -8,7 +8,6 @@ import 'package:pilipala/models/common/reply_type.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class VideoReplyReplyPanel extends StatefulWidget {
|
||||
@ -22,6 +21,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
|
||||
this.sheetHeight,
|
||||
this.currentReply,
|
||||
this.loadMore = true,
|
||||
this.showRoot = false,
|
||||
super.key,
|
||||
});
|
||||
final int? oid;
|
||||
@ -33,6 +33,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
|
||||
final double? sheetHeight;
|
||||
final dynamic currentReply;
|
||||
final bool loadMore;
|
||||
final bool showRoot;
|
||||
|
||||
@override
|
||||
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
|
||||
@ -49,7 +50,11 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
void initState() {
|
||||
_videoReplyReplyController = Get.put(
|
||||
VideoReplyReplyController(
|
||||
widget.oid, widget.rpid.toString(), widget.replyType!),
|
||||
widget.oid,
|
||||
widget.rpid.toString(),
|
||||
widget.replyType!,
|
||||
widget.showRoot,
|
||||
),
|
||||
tag: widget.rpid.toString());
|
||||
super.initState();
|
||||
|
||||
@ -80,6 +85,93 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildAppBar() {
|
||||
return AppBar(
|
||||
toolbarHeight: 45,
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
'评论详情',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: () {
|
||||
_videoReplyReplyController.currentPage = 0;
|
||||
widget.closePanel?.call();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReplyItem(ReplyItemModel? replyItem, String replyLevel) {
|
||||
return ReplyItem(
|
||||
replyItem: replyItem,
|
||||
replyLevel: replyLevel,
|
||||
showReplyRow: false,
|
||||
addReply: (replyItem) {
|
||||
_videoReplyReplyController.replyList.add(replyItem);
|
||||
},
|
||||
replyType: widget.replyType,
|
||||
replyReply: (replyItem) => replyReply(replyItem),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSliverList() {
|
||||
return Obx(
|
||||
() => SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return _videoReplyReplyController.rootReply != null
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color:
|
||||
Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
width: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildReplyItem(
|
||||
_videoReplyReplyController.rootReply, '1'),
|
||||
)
|
||||
: const SizedBox();
|
||||
}
|
||||
int adjustedIndex = index - 1;
|
||||
if (adjustedIndex == _videoReplyReplyController.replyList.length) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom),
|
||||
height: MediaQuery.of(context).padding.bottom + 100,
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_videoReplyReplyController.noMore.value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return _buildReplyItem(
|
||||
_videoReplyReplyController.replyList[adjustedIndex], '2');
|
||||
}
|
||||
},
|
||||
childCount: _videoReplyReplyController.replyList.length + 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@ -87,27 +179,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.source == 'videoDetail')
|
||||
AppBar(
|
||||
toolbarHeight: 45,
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
'评论详情',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: () {
|
||||
_videoReplyReplyController.currentPage = 0;
|
||||
widget.closePanel?.call;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
],
|
||||
),
|
||||
if (widget.source == 'videoDetail') _buildAppBar(),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
@ -120,28 +192,22 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
child: CustomScrollView(
|
||||
controller: _videoReplyReplyController.scrollController,
|
||||
slivers: <Widget>[
|
||||
if (widget.firstFloor != null) ...[
|
||||
// const SliverToBoxAdapter(child: SizedBox(height: 10)),
|
||||
if (widget.firstFloor != null)
|
||||
SliverToBoxAdapter(
|
||||
child: ReplyItem(
|
||||
replyItem: widget.firstFloor,
|
||||
replyLevel: '2',
|
||||
showReplyRow: false,
|
||||
addReply: (replyItem) {
|
||||
_videoReplyReplyController.replyList.add(replyItem);
|
||||
},
|
||||
replyType: widget.replyType,
|
||||
replyReply: (replyItem) => replyReply(replyItem),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.1),
|
||||
width: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildReplyItem(widget.firstFloor, '2'),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Divider(
|
||||
height: 20,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
thickness: 6,
|
||||
),
|
||||
),
|
||||
],
|
||||
widget.loadMore
|
||||
? FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
@ -150,76 +216,21 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
ConnectionState.done) {
|
||||
Map? data = snapshot.data;
|
||||
if (data != null && data['status']) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
() => SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
if (index ==
|
||||
_videoReplyReplyController
|
||||
.replyList.length) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
100,
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_videoReplyReplyController
|
||||
.noMore.value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
return _buildSliverList();
|
||||
} else {
|
||||
// 请求错误
|
||||
return HttpError(
|
||||
errMsg: data?['msg'] ?? '请求错误',
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 8),
|
||||
(BuildContext context, int index) {
|
||||
return const VideoReplySkeleton();
|
||||
},
|
||||
childCount: 8,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -237,7 +248,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -24,30 +24,30 @@ class PiliSchame {
|
||||
|
||||
appScheme.getInitScheme().then((SchemeEntity? value) {
|
||||
if (value != null) {
|
||||
_routePush(value);
|
||||
routePush(value);
|
||||
}
|
||||
});
|
||||
|
||||
appScheme.getLatestScheme().then((SchemeEntity? value) {
|
||||
if (value != null) {
|
||||
_routePush(value);
|
||||
routePush(value);
|
||||
}
|
||||
});
|
||||
|
||||
appScheme.registerSchemeListener().listen((SchemeEntity? event) {
|
||||
if (event != null) {
|
||||
_routePush(event);
|
||||
routePush(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 路由跳转
|
||||
static void _routePush(value) async {
|
||||
static void routePush(value) async {
|
||||
final String scheme = value.scheme;
|
||||
if (scheme == 'bilibili') {
|
||||
biliScheme(value);
|
||||
}
|
||||
if (scheme == 'https') {
|
||||
if (['http', 'https'].contains(scheme)) {
|
||||
httpsScheme(value);
|
||||
}
|
||||
}
|
||||
@ -79,16 +79,16 @@ class PiliSchame {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> httpsScheme(SchemeEntity value) async {
|
||||
static Future<void> httpsScheme(Uri value) async {
|
||||
// https://m.bilibili.com/bangumi/play/ss39708
|
||||
// https | m.bilibili.com | /bangumi/play/ss39708
|
||||
// final String scheme = value.scheme!;
|
||||
final String host = value.host!;
|
||||
final String? path = value.path;
|
||||
Map<String, String>? query = value.query;
|
||||
final String host = value.host;
|
||||
final String path = value.path;
|
||||
Map<String, String>? query = value.queryParameters;
|
||||
RegExp regExp = RegExp(r'^((www\.)|(m\.))?bilibili\.com$');
|
||||
if (regExp.hasMatch(host)) {
|
||||
final String lastPathSegment = path!.split('/').last;
|
||||
final String lastPathSegment = path.split('/').last;
|
||||
if (path.startsWith('/video')) {
|
||||
Map matchRes = IdUtils.matchAvorBv(input: path);
|
||||
if (matchRes.containsKey('AV')) {
|
||||
@ -113,13 +113,13 @@ class PiliSchame {
|
||||
_videoPush(Utils.matchNum(path.split('?').first).first, null);
|
||||
}
|
||||
} else if (host.contains('live')) {
|
||||
int roomId = int.parse(path!.split('/').last);
|
||||
int roomId = int.parse(path.split('/').last);
|
||||
Get.toNamed(
|
||||
'/liveRoom?roomid=$roomId',
|
||||
arguments: {'liveItem': null, 'heroTag': roomId.toString()},
|
||||
);
|
||||
} else if (host.contains('space')) {
|
||||
var mid = path!.split('/').last;
|
||||
var mid = path.split('/').last;
|
||||
Get.toNamed('/member?mid=$mid', arguments: {'face': ''});
|
||||
return;
|
||||
} else if (host == 'b23.tv') {
|
||||
@ -154,7 +154,7 @@ class PiliSchame {
|
||||
parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''},
|
||||
);
|
||||
}
|
||||
} else if (path != null) {
|
||||
} else {
|
||||
final String area = path.split('/').last;
|
||||
switch (area) {
|
||||
case 'bangumi':
|
||||
@ -178,12 +178,12 @@ class PiliSchame {
|
||||
break;
|
||||
case 'read':
|
||||
print('专栏');
|
||||
String id = Utils.matchNum(query!['id']!).first.toString();
|
||||
String id = Utils.matchNum(query['id']!).first.toString();
|
||||
Get.toNamed('/read', parameters: {
|
||||
'url': value.dataString!,
|
||||
'url': value.toString(),
|
||||
'title': '',
|
||||
'id': id,
|
||||
'articleType': 'read'
|
||||
'articleType': 'read',
|
||||
});
|
||||
break;
|
||||
case 'space':
|
||||
@ -201,9 +201,9 @@ class PiliSchame {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': value.dataString ?? "",
|
||||
'url': value.toString(),
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
'pageTitle': '',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -49,6 +49,10 @@ class GlobalDataCache {
|
||||
late List historyCacheList;
|
||||
//
|
||||
late bool enableSearchSuggest = true;
|
||||
// 简介默认展开
|
||||
late bool enableAutoExpand = false;
|
||||
//
|
||||
late bool enableDynamicSwitch = true;
|
||||
|
||||
// 私有构造函数
|
||||
GlobalDataCache._();
|
||||
@ -112,5 +116,9 @@ class GlobalDataCache {
|
||||
historyCacheList = localCache.get('cacheList', defaultValue: []);
|
||||
enableSearchSuggest =
|
||||
setting.get(SettingBoxKey.enableSearchSuggest, defaultValue: true);
|
||||
enableAutoExpand =
|
||||
setting.get(SettingBoxKey.enableAutoExpand, defaultValue: false);
|
||||
enableDynamicSwitch =
|
||||
setting.get(SettingBoxKey.enableDynamicSwitch, defaultValue: true);
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,7 @@ class SettingBoxKey {
|
||||
enableSearchWord = 'enableSearchWord',
|
||||
enableSystemProxy = 'enableSystemProxy',
|
||||
enableAi = 'enableAi',
|
||||
enableAutoExpand = 'enableAutoExpand',
|
||||
defaultHomePage = 'defaultHomePage',
|
||||
enableRelatedVideo = 'enableRelatedVideo';
|
||||
|
||||
@ -130,6 +131,7 @@ class SettingBoxKey {
|
||||
tabbarSort = 'tabbarSort', // 首页tabbar
|
||||
dynamicBadgeMode = 'dynamicBadgeMode',
|
||||
enableGradientBg = 'enableGradientBg',
|
||||
enableDynamicSwitch = 'enableDynamicSwitch',
|
||||
navBarSort = 'navBarSort',
|
||||
actionTypeSort = 'actionTypeSort';
|
||||
}
|
||||
|
16
pubspec.lock
16
pubspec.lock
@ -145,22 +145,6 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
bottom_inset_observer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bottom_inset_observer
|
||||
sha256: cbfb01e0e07cc4922052701786d5e607765a6f54e1844f41061abf8744519a7d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
bottom_sheet:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bottom_sheet
|
||||
sha256: efd28f52357d23e1c01eaeb45466b407f1e29318305bd6d10baf814fda18bd7e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
brotli:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -147,7 +147,6 @@ dependencies:
|
||||
lottie: ^3.1.2
|
||||
# 二维码
|
||||
qr_flutter: ^4.1.0
|
||||
bottom_sheet: ^4.0.4
|
||||
web_socket_channel: ^2.4.5
|
||||
brotli: ^0.6.0
|
||||
# 文本语法高亮
|
||||
|
Reference in New Issue
Block a user