Compare commits

...

13 Commits

Author SHA1 Message Date
3ad3ca9d48 v1.0.4 更新 2023-08-22 22:02:16 +08:00
8a8e99f30b feat: 字体大小调节 2023-08-22 20:53:50 +08:00
0fe6d6c8e2 mod: expansionPanelList 扩展 2023-08-22 16:51:43 +08:00
5a03bee410 feat: 主题颜色选择 2023-08-22 16:49:49 +08:00
b6023e35bc Update README.md 2023-08-22 14:26:23 +08:00
6a1c89f885 fix: 点赞异常、动态课程内容渲染 2023-08-22 14:17:51 +08:00
b7c0ef8341 mod: 取消热搜缓存、增加刷新功能 2023-08-22 12:27:02 +08:00
9e44995082 feat: 视频搜索时长筛选v2 2023-08-22 11:47:19 +08:00
8703d9f576 feat: 视频搜索条件筛选v1 2023-08-22 09:49:23 +08:00
5812b5cff1 fix: 动态渲染报错、转发评论加载 2023-08-21 21:16:03 +08:00
1884801ed2 fix: 视频解码默认格式 2023-08-21 18:35:50 +08:00
a19ab8d17f Merge branch 'fix' 2023-08-21 14:56:15 +08:00
a4078c0a8e fix: 搜索推荐词richText 2023-08-21 14:55:32 +08:00
42 changed files with 1294 additions and 284 deletions

View File

@ -74,12 +74,14 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
- [ ] 弹幕
- [ ] 字幕
- [x] 记忆播放
- [x] 视频比例:高度/宽度适应、填充、包含等
- [x] 搜索相关
- [x] 热搜
- [x] 搜索历史
- [x] 默认搜索词
- [x] 投稿、番剧、直播间、用户搜索
- [x] 视频搜索排序、按时长筛选
- [x] 视频详情页相关
- [x] 视频选集(分p)切换

21
change_log/1.0.4.0822.md Normal file
View File

@ -0,0 +1,21 @@
## 1.0.4
### 新功能
+ 热搜刷新
+ 视频搜索排序、筛选
+ app字体大小自定义
+ app主题色自定义
+ 「课堂」类动态渲染
### 修复
+ 搜索词联想richText渲染异常
+ 部分动态点赞异常
+ 默认视频解码格式
+ 搜索页面返回搜索词未清空
+ 动态详情评论加载异常
+ 动态页面下拉刷新数据异常
### 优化
+ 一些样式修改
+ 取消热搜词缓存

View File

@ -0,0 +1,351 @@
import 'package:flutter/material.dart';
const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;
class _SaltedKey<S, V> extends LocalKey {
const _SaltedKey(this.salt, this.value);
final S salt;
final V value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is _SaltedKey<S, V> &&
other.salt == salt &&
other.value == value;
}
@override
int get hashCode => Object.hash(runtimeType, salt, value);
@override
String toString() {
final String saltString = S == String ? "<'$salt'>" : '<$salt>';
final String valueString = V == String ? "<'$value'>" : '<$value>';
return '[$saltString $valueString]';
}
}
class AppExpansionPanelList extends StatefulWidget {
/// Creates an expansion panel list widget. The [expansionCallback] is
/// triggered when an expansion panel expand/collapse button is pushed.
///
/// The [children] and [animationDuration] arguments must not be null.
const AppExpansionPanelList({
super.key,
required this.children,
this.expansionCallback,
this.animationDuration = kThemeAnimationDuration,
this.expandedHeaderPadding = EdgeInsets.zero,
this.dividerColor,
this.elevation = 2,
}) : _allowOnlyOnePanelOpen = false,
initialOpenPanelValue = null;
/// The children of the expansion panel list. They are laid out in a similar
/// fashion to [ListBody].
final List<AppExpansionPanel> children;
/// The callback that gets called whenever one of the expand/collapse buttons
/// is pressed. The arguments passed to the callback are the index of the
/// pressed panel and whether the panel is currently expanded or not.
///
/// If AppExpansionPanelList.radio is used, the callback may be called a
/// second time if a different panel was previously open. The arguments
/// passed to the second callback are the index of the panel that will close
/// and false, marking that it will be closed.
///
/// For AppExpansionPanelList, the callback needs to setState when it's notified
/// about the closing/opening panel. On the other hand, the callback for
/// AppExpansionPanelList.radio is simply meant to inform the parent widget of
/// changes, as the radio panels' open/close states are managed internally.
///
/// This callback is useful in order to keep track of the expanded/collapsed
/// panels in a parent widget that may need to react to these changes.
final ExpansionPanelCallback? expansionCallback;
/// The duration of the expansion animation.
final Duration animationDuration;
// Whether multiple panels can be open simultaneously
final bool _allowOnlyOnePanelOpen;
/// The value of the panel that initially begins open. (This value is
/// only used when initializing with the [AppExpansionPanelList.radio]
/// constructor.)
final Object? initialOpenPanelValue;
/// The padding that surrounds the panel header when expanded.
///
/// By default, 16px of space is added to the header vertically (above and below)
/// during expansion.
final EdgeInsets expandedHeaderPadding;
/// Defines color for the divider when [AppExpansionPanel.isExpanded] is false.
///
/// If `dividerColor` is null, then [DividerThemeData.color] is used. If that
/// is null, then [ThemeData.dividerColor] is used.
final Color? dividerColor;
/// Defines elevation for the [AppExpansionPanel] while it's expanded.
///
/// By default, the value of elevation is 2.
final double elevation;
@override
State<AppExpansionPanelList> createState() => _AppExpansionPanelListState();
}
class _AppExpansionPanelListState extends State<AppExpansionPanelList> {
ExpansionPanelRadio? _currentOpenPanel;
@override
void initState() {
super.initState();
if (widget._allowOnlyOnePanelOpen) {
assert(_allIdentifiersUnique(),
'All ExpansionPanelRadio identifier values must be unique.');
if (widget.initialOpenPanelValue != null) {
_currentOpenPanel = searchPanelByValue(
widget.children.cast<ExpansionPanelRadio>(),
widget.initialOpenPanelValue);
}
}
}
@override
void didUpdateWidget(AppExpansionPanelList oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget._allowOnlyOnePanelOpen) {
assert(_allIdentifiersUnique(),
'All ExpansionPanelRadio identifier values must be unique.');
// If the previous widget was non-radio AppExpansionPanelList, initialize the
// open panel to widget.initialOpenPanelValue
if (!oldWidget._allowOnlyOnePanelOpen) {
_currentOpenPanel = searchPanelByValue(
widget.children.cast<ExpansionPanelRadio>(),
widget.initialOpenPanelValue);
}
} else {
_currentOpenPanel = null;
}
}
bool _allIdentifiersUnique() {
final Map<Object, bool> identifierMap = <Object, bool>{};
for (final ExpansionPanelRadio child
in widget.children.cast<ExpansionPanelRadio>()) {
identifierMap[child.value] = true;
}
return identifierMap.length == widget.children.length;
}
bool _isChildExpanded(int index) {
if (widget._allowOnlyOnePanelOpen) {
final ExpansionPanelRadio radioWidget =
widget.children[index] as ExpansionPanelRadio;
return _currentOpenPanel?.value == radioWidget.value;
}
return widget.children[index].isExpanded;
}
void _handlePressed(bool isExpanded, int index) {
widget.expansionCallback?.call(index, isExpanded);
if (widget._allowOnlyOnePanelOpen) {
final ExpansionPanelRadio pressedChild =
widget.children[index] as ExpansionPanelRadio;
// If another ExpansionPanelRadio was already open, apply its
// expansionCallback (if any) to false, because it's closing.
for (int childIndex = 0;
childIndex < widget.children.length;
childIndex += 1) {
final ExpansionPanelRadio child =
widget.children[childIndex] as ExpansionPanelRadio;
if (widget.expansionCallback != null &&
childIndex != index &&
child.value == _currentOpenPanel?.value) {
widget.expansionCallback!(childIndex, false);
}
}
setState(() {
_currentOpenPanel = isExpanded ? null : pressedChild;
});
}
}
ExpansionPanelRadio? searchPanelByValue(
List<ExpansionPanelRadio> panels, Object? value) {
for (final ExpansionPanelRadio panel in panels) {
if (panel.value == value) return panel;
}
return null;
}
@override
Widget build(BuildContext context) {
assert(
kElevationToShadow.containsKey(widget.elevation),
'Invalid value for elevation. See the kElevationToShadow constant for'
' possible elevation values.',
);
final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];
for (int index = 0; index < widget.children.length; index += 1) {
//todo: Uncomment to add gap between selected panels
/*if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));*/
final AppExpansionPanel child = widget.children[index];
final Widget headerWidget = child.headerBuilder(
context,
_isChildExpanded(index),
);
Widget? expandIconContainer = ExpandIcon(
isExpanded: _isChildExpanded(index),
onPressed: !child.canTapOnHeader
? (bool isExpanded) => _handlePressed(isExpanded, index)
: null,
);
if (!child.canTapOnHeader) {
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
expandIconContainer = Semantics(
label: _isChildExpanded(index)
? localizations.expandedIconTapHint
: localizations.collapsedIconTapHint,
container: true,
child: expandIconContainer,
);
}
final iconContainer = child.iconBuilder;
if (iconContainer != null) {
expandIconContainer = iconContainer(
expandIconContainer,
_isChildExpanded(index),
);
}
Widget header = Row(
children: <Widget>[
Expanded(
child: AnimatedContainer(
duration: widget.animationDuration,
curve: Curves.fastOutSlowIn,
margin: _isChildExpanded(index)
? widget.expandedHeaderPadding
: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: _kPanelHeaderCollapsedHeight),
child: headerWidget,
),
),
),
if (expandIconContainer != null) expandIconContainer,
],
);
if (child.canTapOnHeader) {
header = MergeSemantics(
child: InkWell(
onTap: () => _handlePressed(_isChildExpanded(index), index),
child: header,
),
);
}
items.add(
MaterialSlice(
key: _SaltedKey<BuildContext, int>(context, index * 2),
color: child.backgroundColor,
child: Column(
children: <Widget>[
header,
AnimatedCrossFade(
firstChild: Container(height: 0.0),
secondChild: child.body,
firstCurve:
const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
secondCurve:
const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
sizeCurve: Curves.fastOutSlowIn,
crossFadeState: _isChildExpanded(index)
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: widget.animationDuration,
),
],
),
),
);
if (_isChildExpanded(index) && index != widget.children.length - 1) {
items.add(MaterialGap(
key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
}
}
return MergeableMaterial(
hasDividers: true,
dividerColor: widget.dividerColor,
elevation: widget.elevation,
children: items,
);
}
}
typedef ExpansionPanelIconBuilder = Widget? Function(
Widget child,
bool isExpanded,
);
class AppExpansionPanel {
/// Creates an expansion panel to be used as a child for [ExpansionPanelList].
/// See [ExpansionPanelList] for an example on how to use this widget.
///
/// The [headerBuilder], [body], and [isExpanded] arguments must not be null.
AppExpansionPanel({
required this.headerBuilder,
required this.body,
this.iconBuilder,
this.isExpanded = false,
this.canTapOnHeader = false,
this.backgroundColor,
});
/// The widget builder that builds the expansion panels' header.
final ExpansionPanelHeaderBuilder headerBuilder;
/// The widget builder that builds the expansion panels' icon.
///
/// If not pass any function, then default icon will be displayed.
///
/// If builder function return null, then icon will not displayed.
final ExpansionPanelIconBuilder? iconBuilder;
/// The body of the expansion panel that's displayed below the header.
///
/// This widget is visible only when the panel is expanded.
final Widget body;
/// Whether the panel is expanded.
///
/// Defaults to false.
final bool isExpanded;
/// Whether tapping on the panel's header will expand/collapse it.
///
/// Defaults to false.
final bool canTapOnHeader;
/// Defines the background color of the panel.
///
/// Defaults to [ThemeData.cardColor].
final Color? backgroundColor;
}

View File

@ -58,8 +58,11 @@ class VideoCardH extends StatelessWidget {
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.of(context).textScaleFactor) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
@ -123,7 +126,7 @@ class VideoContent extends StatelessWidget {
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
padding: const EdgeInsets.fromLTRB(10, 0, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -132,7 +135,6 @@ class VideoContent extends StatelessWidget {
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
maxLines: 2,
@ -147,7 +149,6 @@ class VideoContent extends StatelessWidget {
TextSpan(
text: i['text'],
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
color: i['type'] == 'em'
@ -177,7 +178,7 @@ class VideoContent extends StatelessWidget {
// color: Theme.of(context).colorScheme.surfaceTint),
// ),
// ),
const SizedBox(height: 4),
// const SizedBox(height: 4),
Row(
children: [
Text(

View File

@ -77,11 +77,8 @@ class VideoCardV extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id);
return Card(
elevation: 0,
elevation: 1,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: GestureDetector(
onLongPress: () {
@ -129,14 +126,13 @@ class VideoContent extends StatelessWidget {
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(4, 8, 0, 3),
padding: const EdgeInsets.fromLTRB(9, 8, 9, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
videoItem.title,
style: const TextStyle(fontSize: 13),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),

View File

@ -46,14 +46,19 @@ class SearchHttp {
required SearchType searchType,
required String keyword,
required page,
String? order,
int? duration,
}) async {
var res = await Request().get(Api.searchByType, data: {
var reqData = {
'search_type': searchType.type,
'keyword': keyword,
// 'order_sort': 0,
// 'user_type': 0,
'page': page
});
'page': page,
if (order != null) 'order': order,
if (duration != null) 'duration': duration,
};
var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
Object data;
switch (searchType) {

View File

@ -7,6 +7,7 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/custom_toast.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/color_type.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
@ -35,15 +36,27 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color brandColor = const Color.fromARGB(255, 92, 182, 123);
Box setting = GStrorage.setting;
// 主题色
Color defaultColor =
colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)]
['color'];
Color brandColor = defaultColor;
// 主题模式
ThemeType currentThemeValue = ThemeType.values[setting
.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)];
// 是否动态取色
bool isDynamicColor =
setting.get(SettingBoxKey.dynamicColor, defaultValue: true);
// 字体缩放大小
double textScale =
setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
return DynamicColorBuilder(
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme? lightColorScheme;
ColorScheme? darkColorScheme;
if (lightDynamic != null && darkDynamic != null) {
if (lightDynamic != null && darkDynamic != null && isDynamicColor) {
// dynamic取色成功
lightColorScheme = lightDynamic.harmonized();
darkColorScheme = darkDynamic.harmonized();
@ -93,9 +106,17 @@ class MyApp extends StatelessWidget {
fallbackLocale: const Locale("zh", "CN"),
getPages: Routes.getPages,
home: const MainApp(),
builder: FlutterSmartDialog.init(
toastBuilder: (String msg) => CustomToast(msg: msg),
),
builder: (BuildContext context, Widget? child) {
return FlutterSmartDialog(
toastBuilder: (String msg) => CustomToast(msg: msg),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor:
MediaQuery.of(context).textScaleFactor * textScale),
child: child!,
),
);
},
navigatorObservers: [
VideoDetailPage.routeObserver,
SearchPage.routeObserver,

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
final List<Map<String, dynamic>> colorThemeTypes = [
{'color': const Color.fromARGB(255, 92, 182, 123), 'label': '默认绿'},
{'color': Colors.pink, 'label': '粉红色'},
{'color': Colors.red, 'label': '红色'},
{'color': Colors.orange, 'label': '橙色'},
{'color': Colors.amber, 'label': '琥珀色'},
{'color': Colors.yellow, 'label': '黄色'},
{'color': Colors.lime, 'label': '酸橙色'},
{'color': Colors.lightGreen, 'label': '浅绿色'},
{'color': Colors.green, 'label': '绿色'},
{'color': Colors.teal, 'label': '青色'},
{'color': Colors.cyan, 'label': '蓝绿色'},
{'color': Colors.lightBlue, 'label': '浅蓝色'},
{'color': Colors.blue, 'label': '蓝色'},
{'color': Colors.indigo, 'label': '靛蓝色'},
{'color': Colors.purple, 'label': '紫色'},
{'color': Colors.deepPurple, 'label': '深紫色'},
{'color': Colors.blueGrey, 'label': '蓝灰色'},
{'color': Colors.brown, 'label': '棕色'},
{'color': Colors.grey, 'label': '灰色'},
];

View File

@ -27,3 +27,20 @@ extension SearchTypeExtension on SearchType {
['video', 'media_bangumi', 'live_room', 'bili_user'][index];
String get label => ['视频', '番剧', '直播间', '用户'][index];
}
// 搜索类型为视频、专栏及相簿时
enum ArchiveFilterType {
totalrank,
click,
pubdate,
dm,
stow,
scores,
// 专栏
// attention,
}
extension ArchiveFilterTypeExtension on ArchiveFilterType {
String get description =>
['默认排序', '播放多', '新发布', '弹幕多', '收藏多', '评论多', '最多喜欢'][index];
}

View File

@ -408,6 +408,7 @@ class DynamicMajorModel {
this.live,
this.none,
this.type,
this.courses,
});
DynamicArchiveModel? archive;
@ -422,6 +423,7 @@ class DynamicMajorModel {
// MAJOR_TYPE_ARCHIVE 视频
// MAJOR_TYPE_OPUS 图文/文章
String? type;
Map? courses;
DynamicMajorModel.fromJson(Map<String, dynamic> json) {
archive = json['archive'] != null
@ -444,6 +446,7 @@ class DynamicMajorModel {
none =
json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;
type = json['type'];
courses = json['courses'] ?? {};
}
}

View File

@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class SearchSuggestModel {
SearchSuggestModel({
this.tag,
@ -19,32 +22,74 @@ class SearchSuggestItem {
SearchSuggestItem({
this.value,
this.term,
this.name,
this.spid,
this.textRich,
});
String? value;
String? term;
List? name;
int? spid;
Widget? textRich;
SearchSuggestItem.fromJson(Map<String, dynamic> json, String inputTerm) {
value = json['value'];
term = json['term'];
String reg = '<em class="suggest_high_light">$inputTerm</em>';
try {
if (json['name'].indexOf(inputTerm) != -1) {
String str = json['name'].replaceAll(reg, '^');
List arr = str.split('^');
arr.insert(arr.length - 1, inputTerm);
name = arr;
} else {
name = ['', '', json['term']];
}
} catch (err) {
name = ['', '', json['term']];
}
spid = json['spid'];
textRich = highlightText(json['name']);
}
}
Widget highlightText(String str) {
// 创建正则表达式,匹配 <em class="suggest_high_light">...</em> 格式的文本
RegExp regex = RegExp(r'<em class="suggest_high_light">(.*?)<\/em>');
// 用于存储每个匹配项的列表
List<InlineSpan> children = [];
// 获取所有匹配项
Iterable<Match> matches = regex.allMatches(str);
// 当前索引位置
int currentIndex = 0;
// 遍历每个匹配项
for (var match in matches) {
// 获取当前匹配项之前的普通文本部分
String normalText = str.substring(currentIndex, match.start);
// 获取需要高亮显示的文本部分
String highlightedText = match.group(1)!;
// 如果普通文本部分不为空,则将其添加到 children 列表中
if (normalText.isNotEmpty) {
children.add(TextSpan(
text: normalText,
style: DefaultTextStyle.of(Get.context!).style,
));
}
// 将需要高亮显示的文本部分添加到 children 列表中,并设置相应样式
children.add(TextSpan(
text: highlightedText,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(Get.context!).colorScheme.primary),
));
// 更新当前索引位置
currentIndex = match.end;
}
// 如果当前索引位置小于文本长度,表示还有剩余的普通文本部分
if (currentIndex < str.length) {
String remainingText = str.substring(currentIndex);
// 将剩余的普通文本部分添加到 children 列表中
children.add(TextSpan(
text: remainingText,
style: DefaultTextStyle.of(Get.context!).style,
));
}
// 使用 Text.rich 创建包含高亮显示的富文本小部件,并返回
return Text.rich(TextSpan(children: children));
}

View File

@ -213,7 +213,8 @@ class _BangumiPageState extends State<BangumiPage>
crossAxisSpacing: StyleString.cardSpace,
// 列数
crossAxisCount: 3,
mainAxisExtent: Get.size.width / 3 / 0.65 + 30,
mainAxisExtent: Get.size.width / 3 / 0.65 +
32 * MediaQuery.of(context).textScaleFactor,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {

View File

@ -29,9 +29,6 @@ class BangumiCardV extends StatelessWidget {
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: GestureDetector(
// onLongPress: () {
@ -149,7 +146,6 @@ class BangumiContent extends StatelessWidget {
bangumiItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
@ -158,6 +154,7 @@ class BangumiContent extends StatelessWidget {
)),
],
),
const SizedBox(height: 1),
if (bangumiItem.indexShow != null)
Text(
bangumiItem.indexShow,

View File

@ -70,6 +70,10 @@ class DynamicsController extends GetxController {
if (type == 'init') {
dynamicsList.clear();
}
// 下拉刷新数据渲染时会触发onLoad
if (type == 'onLoad' && page == 1) {
return;
}
isLoadingDynamic.value = true;
var res = await DynamicsHttp.followDynamic(
page: type == 'init' ? 1 : page,
@ -79,6 +83,10 @@ class DynamicsController extends GetxController {
);
isLoadingDynamic.value = false;
if (res['status']) {
if (type == 'onLoad' && res['data'].items.isEmpty) {
SmartDialog.showToast('没有更多了');
return;
}
if (type == 'init') {
dynamicsList.value = res['data'].items;
} else {
@ -219,6 +227,7 @@ class DynamicsController extends GetxController {
onRefresh() async {
page = 1;
print('onRefresh');
await queryFollowUp();
await queryFollowDynamic();
}

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_reply.dart';
@ -40,7 +41,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
} else {
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
}
type = Get.arguments['item'].basic!['comment_type'];
int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11;
type = (commentType == 0) ? 11 : commentType;
action =
Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
_dynamicDetailController = Get.put(DynamicDetailController(oid, type));
@ -56,10 +59,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
void _listen() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
if (!_dynamicDetailController!.isLoadingMore) {
_dynamicDetailController!.isLoadingMore = true;
await _dynamicDetailController!.queryReplyList(reqType: 'onLoad');
}
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_dynamicDetailController!.queryReplyList(reqType: 'onLoad');
});
}
if (scrollController.offset > 55 && !_visibleTitle) {
@ -242,6 +244,11 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
replyReply: (replyItem) =>
replyReply(replyItem),
replyType: ReplyType.values[type],
addReply: (replyItem) {
_dynamicDetailController!
.replyList[index].replies!
.add(replyItem);
},
);
}
},

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
@ -30,7 +31,6 @@ class _DynamicsPageState extends State<DynamicsPage>
final DynamicsController _dynamicsController = Get.put(DynamicsController());
late Future _futureBuilderFuture;
late Future _futureBuilderFutureUp;
bool _isLoadingMore = false;
Box userInfoCache = GStrorage.userInfo;
EventBus eventBus = EventBus();
late ScrollController scrollController;
@ -50,11 +50,10 @@ class _DynamicsPageState extends State<DynamicsPage>
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_isLoadingMore) {
_isLoadingMore = true;
await _dynamicsController.queryFollowDynamic(type: 'onLoad');
_isLoadingMore = false;
}
EasyThrottle.throttle(
'queryFollowDynamic', const Duration(seconds: 1), () {
_dynamicsController.queryFollowDynamic(type: 'onLoad');
});
}
final ScrollDirection direction =
@ -277,6 +276,7 @@ class _DynamicsPageState extends State<DynamicsPage>
}
},
),
const SliverToBoxAdapter(child: SizedBox(height: 40))
],
),
),

View File

@ -37,7 +37,7 @@ class _ActionPanelState extends State<ActionPanel> {
String dynamicId = item.idStr!;
// 1 已点赞 2 不喜欢 0 未操作
Like like = item.modules.moduleStat.like;
int count = int.parse(like.count!);
int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0');
bool status = like.status!;
int up = status ? 2 : 1;
var res = await DynamicsHttp.likeDynamic(dynamicId: dynamicId, up: up);
@ -47,7 +47,11 @@ class _ActionPanelState extends State<ActionPanel> {
item.modules.moduleStat.like.count = (count + 1).toString();
item.modules.moduleStat.like.status = true;
} else {
item.modules.moduleStat.like.count = (count - 1).toString();
if (count == 1) {
item.modules.moduleStat.like.count = '点赞';
} else {
item.modules.moduleStat.like.count = (count - 1).toString();
}
item.modules.moduleStat.like.status = false;
}
setState(() {});
@ -63,54 +67,63 @@ class _ActionPanelState extends State<ActionPanel> {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton.icon(
onPressed: () {},
icon: const Icon(
FontAwesomeIcons.shareFromSquare,
size: 16,
Expanded(
flex: 1,
child: TextButton.icon(
onPressed: () {},
icon: const Icon(
FontAwesomeIcons.shareFromSquare,
size: 16,
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: Text(stat.forward!.count ?? '转发'),
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: Text(stat.forward!.count ?? '转发'),
),
TextButton.icon(
onPressed: () =>
_dynamicsController.pushDetail(widget.item, 1, action: 'comment'),
icon: const Icon(
FontAwesomeIcons.comment,
size: 16,
Expanded(
flex: 1,
child: TextButton.icon(
onPressed: () => _dynamicsController.pushDetail(widget.item, 1,
action: 'comment'),
icon: const Icon(
FontAwesomeIcons.comment,
size: 16,
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: Text(stat.comment!.count ?? '评论'),
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: Text(stat.comment!.count ?? '评论'),
),
TextButton.icon(
onPressed: () => onLikeDynamic(),
icon: Icon(
stat.like!.status!
? FontAwesomeIcons.solidThumbsUp
: FontAwesomeIcons.thumbsUp,
size: 16,
color: stat.like!.status! ? primary : color,
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Text(
stat.like!.count ?? '点赞',
key: ValueKey<String>(stat.like!.count ?? '点赞'),
style: TextStyle(
color: stat.like!.status! ? primary : color,
Expanded(
flex: 1,
child: TextButton.icon(
onPressed: () => onLikeDynamic(),
icon: Icon(
stat.like!.status!
? FontAwesomeIcons.solidThumbsUp
: FontAwesomeIcons.thumbsUp,
size: 16,
color: stat.like!.status! ? primary : color,
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Text(
stat.like!.count ?? '点赞',
key: ValueKey<String>(stat.like!.count ?? '点赞'),
style: TextStyle(
color: stat.like!.status! ? primary : color,
),
),
),
),

View File

@ -60,43 +60,47 @@ Widget addWidget(item, context, type, {floor = 1}) {
),
);
case 'ADDITIONAL_TYPE_RESERVE':
return Padding(
padding: const EdgeInsets.only(top: 8),
child: InkWell(
onTap: () {},
child: Container(
width: double.infinity,
padding:
const EdgeInsets.only(left: 12, top: 10, right: 12, bottom: 10),
color: bgColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dynamicProperty[type].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 1),
Text.rich(
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize),
return dynamicProperty[type].state != -1
? Padding(
padding: const EdgeInsets.only(top: 8),
child: InkWell(
onTap: () {},
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(
left: 12, top: 10, right: 12, bottom: 10),
color: bgColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextSpan(text: dynamicProperty[type].desc1['text']),
const TextSpan(text: ' '),
TextSpan(text: dynamicProperty[type].desc2['text']),
Text(
dynamicProperty[type].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 1),
Text.rich(
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
children: [
TextSpan(text: dynamicProperty[type].desc1['text']),
const TextSpan(text: ' '),
TextSpan(text: dynamicProperty[type].desc2['text']),
],
),
)
],
),
)
],
),
// TextButton(onPressed: () {}, child: Text('123'))
),
),
);
// TextButton(onPressed: () {}, child: Text('123'))
),
),
)
: const SizedBox();
case 'ADDITIONAL_TYPE_GOODS':
return Padding(
padding: const EdgeInsets.only(top: 6),

View File

@ -100,6 +100,7 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
// 直播
case 'DYNAMIC_TYPE_LIVE_RCMD':
return liveRcmdPanel(item, context, floor: floor);
// 直播
case 'DYNAMIC_TYPE_LIVE':
return livePanel(item, context, floor: floor);
// 合集
@ -147,6 +148,7 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
return videoSeasonWidget(item, context, 'pgc', floor: floor);
case 'DYNAMIC_TYPE_PGC_UNION':
return videoSeasonWidget(item, context, 'pgc', floor: floor);
// 直播结束
case 'DYNAMIC_TYPE_NONE':
return Row(
children: [
@ -158,7 +160,23 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
Text(item.modules.moduleDynamic.major.none.tips)
],
);
// 课堂
case 'DYNAMIC_TYPE_COURSES_SEASON':
return Row(
children: [
Expanded(
child: Text(
"课堂💪:${item.modules.moduleDynamic.major.courses['title']}",
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
],
);
default:
return const SizedBox(height: 0);
return const SizedBox(
width: double.infinity,
child: Text('🙏 暂未支持的类型,请联系开发者反馈 '),
);
}
}

View File

@ -77,7 +77,6 @@ class VideoContent extends StatelessWidget {
favFolderItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),

View File

@ -127,32 +127,34 @@ class _FavDetailPageState extends State<FavDetailPage> {
),
),
const SizedBox(width: 14),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
_favDetailController.item!.title!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleMedium!
.fontSize,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
_favDetailController.item!.upper!.name!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
)
],
)
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
_favDetailController.item!.title!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleMedium!
.fontSize,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
_favDetailController.item!.upper!.name!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
)
],
),
),
],
),
),

View File

@ -159,7 +159,6 @@ class VideoContent extends StatelessWidget {
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),

View File

@ -205,7 +205,6 @@ class VideoContent extends StatelessWidget {
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),

View File

@ -146,7 +146,8 @@ class _LivePageState extends State<LivePage> {
// 列数
crossAxisCount: crossAxisCount,
mainAxisExtent:
Get.size.width / crossAxisCount / StyleString.aspectRatio + 66,
Get.size.width / crossAxisCount / StyleString.aspectRatio +
68 * MediaQuery.of(context).textScaleFactor,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {

View File

@ -23,11 +23,8 @@ class LiveCardV extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomId);
return Card(
elevation: 0,
elevation: 1,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: GestureDetector(
onLongPress: () {
@ -103,7 +100,7 @@ class LiveContent extends StatelessWidget {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 8, 0, 6),
padding: const EdgeInsets.fromLTRB(9, 9, 9, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -112,7 +109,6 @@ class LiveContent extends StatelessWidget {
liveItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),

View File

@ -138,7 +138,7 @@ class _MediaPageState extends State<MediaPage>
// const SizedBox(height: 10),
SizedBox(
width: double.infinity,
height: 170,
height: 170 * MediaQuery.of(context).textScaleFactor,
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {

View File

@ -159,7 +159,8 @@ class _RcmdPageState extends State<RcmdPage>
// 列数
crossAxisCount: crossAxisCount,
mainAxisExtent:
Get.size.width / crossAxisCount / StyleString.aspectRatio + 66,
(Get.size.width / crossAxisCount / StyleString.aspectRatio) +
68 * MediaQuery.of(context).textScaleFactor,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {

View File

@ -12,8 +12,7 @@ class SSearchController extends GetxController {
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
Rx<TextEditingController> controller = TextEditingController().obs;
List<HotSearchItem> hotSearchList = [];
Box hotKeyword = GStrorage.hotKeyword;
RxList<HotSearchItem> hotSearchList = [HotSearchItem()].obs;
Box histiryWord = GStrorage.historyword;
List historyCacheList = [];
RxList historyList = [].obs;
@ -27,14 +26,6 @@ class SSearchController extends GetxController {
void onInit() {
super.onInit();
searchDefault();
if (hotKeyword.get('cacheList') != null &&
hotKeyword.get('cacheList').isNotEmpty) {
List<HotSearchItem> list = [];
for (var i in hotKeyword.get('cacheList')) {
list.add(i);
}
hotSearchList = list;
}
// 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) {
if (Get.parameters['keyword'] != null) {
@ -89,8 +80,7 @@ class SSearchController extends GetxController {
// 获取热搜关键词
Future queryHotSearchList() async {
var result = await SearchHttp.hotSearchList();
hotSearchList = result['data'].list;
hotKeyword.put('cacheList', result['data'].list);
hotSearchList.value = result['data'].list;
return result;
}

View File

@ -45,6 +45,11 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
return OpenContainer(
closedElevation: 0,
openElevation: 0,
onClosed: (_) async {
// 在 openBuilder 关闭时触发的回调函数
await Future.delayed(const Duration(milliseconds: 500));
_searchController.onClear();
},
openColor: Theme.of(context).colorScheme.background,
middleColor: Theme.of(context).colorScheme.background,
closedColor: Theme.of(context).colorScheme.background,
@ -145,7 +150,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
// 搜索建议
_searchSuggest(),
// 热搜
hotSearch(),
hotSearch(_searchController),
// 搜索历史
_history()
],
@ -176,25 +181,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
// child: Text(
// _searchController.searchSuggestList[index].term!,
// ),
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: _searchController
.searchSuggestList[index].name![0]),
TextSpan(
text: _searchController
.searchSuggestList[index].name![1],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold),
),
TextSpan(
text: _searchController
.searchSuggestList[index].name![2]),
],
),
),
child: _searchController.searchSuggestList[index].textRich,
),
);
},
@ -203,20 +190,37 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
);
}
Widget hotSearch() {
Widget hotSearch(ctr) {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 14, 4, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 0, 6),
child: Text(
'大家都在搜',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'大家都在搜',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(
height: 34,
child: TextButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all(const EdgeInsets.only(
left: 10, top: 6, bottom: 6, right: 10)),
),
onPressed: () => ctr.queryHotSearchList(),
icon: const Icon(Icons.refresh_outlined, size: 18),
label: const Text('刷新'),
),
),
],
),
),
LayoutBuilder(
@ -228,15 +232,17 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList,
onClick: (keyword) async {
_searchController.searchFocusNode.unfocus();
await Future.delayed(
const Duration(milliseconds: 150));
_searchController.onClickKeyword(keyword);
},
return Obx(
() => HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList.value,
onClick: (keyword) async {
_searchController.searchFocusNode.unfocus();
await Future.delayed(
const Duration(milliseconds: 150));
_searchController.onClickKeyword(keyword);
},
),
);
} else {
return HttpError(

View File

@ -12,17 +12,25 @@ class SearchPanelController extends GetxController {
SearchType? searchType;
RxInt page = 1.obs;
RxList resultList = [].obs;
// 结果排序方式 搜索类型为视频、专栏及相簿时
RxString order = ''.obs;
// 视频时长筛选 仅用于搜索视频
RxInt duration = 0.obs;
Future onSearch({type = 'init'}) async {
var result = await SearchHttp.searchByType(
searchType: searchType!, keyword: keyword!, page: page.value);
searchType: searchType!,
keyword: keyword!,
page: page.value,
order: searchType!.type != 'video' ? null : order.value,
duration: searchType!.type != 'video' ? null : duration.value);
if (result['status']) {
if (type == 'init' || type == 'onLoad') {
page.value++;
resultList.addAll(result['data'].list);
} else if (type == 'onRefresh') {
if (type == 'onRefresh') {
resultList.value = result['data'].list;
} else {
resultList.addAll(result['data'].list);
}
page.value++;
onPushDetail(keyword, resultList);
}
return result;
@ -30,7 +38,7 @@ class SearchPanelController extends GetxController {
Future onRefresh() async {
page.value = 1;
onSearch(type: 'onRefresh');
await onSearch(type: 'onRefresh');
}
// 返回顶部并刷新

View File

@ -28,7 +28,6 @@ class _SearchPanelState extends State<SearchPanel>
with AutomaticKeepAliveClientMixin {
late SearchPanelController _searchPanelController;
bool _isLoadingMore = false;
late Future _futureBuilderFuture;
late ScrollController scrollController;
@ -76,12 +75,15 @@ class _SearchPanelState extends State<SearchPanel>
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
var ctr = _searchPanelController;
List list = ctr.resultList;
RxList list = ctr.resultList;
if (data['status']) {
return Obx(() {
switch (widget.searchType) {
case SearchType.video:
return searchVideoPanel(context, ctr, list);
return SearchVideoPanel(
ctr: _searchPanelController,
list: list.value,
);
case SearchType.media_bangumi:
return searchMbangumiPanel(context, ctr, list);
case SearchType.bili_user:

View File

@ -12,13 +12,12 @@ Widget searchLivePanel(BuildContext context, ctr, list) {
primary: false,
controller: ctr!.scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: StyleString.cardSpace + 2,
mainAxisSpacing: StyleString.cardSpace + 3,
mainAxisExtent:
MediaQuery.of(context).size.width / 2 / StyleString.aspectRatio +
60,
),
crossAxisCount: 2,
crossAxisSpacing: StyleString.cardSpace + 2,
mainAxisSpacing: StyleString.cardSpace + 3,
mainAxisExtent:
MediaQuery.of(context).size.width / 2 / StyleString.aspectRatio +
66 * MediaQuery.of(context).textScaleFactor),
itemCount: list.length,
itemBuilder: (context, index) {
return LiveItem(liveItem: list![index]);
@ -35,11 +34,8 @@ class LiveItem extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomid);
return Card(
elevation: 0,
elevation: 1,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: InkWell(
onTap: () async {
@ -104,7 +100,7 @@ class LiveContent extends StatelessWidget {
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(4, 5, 6, 6),
padding: const EdgeInsets.fromLTRB(9, 8, 9, 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -116,7 +112,6 @@ class LiveContent extends StatelessWidget {
TextSpan(
text: i['text'],
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
color: i['type'] == 'em'

View File

@ -68,9 +68,10 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
text: i['text'],
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleSmall!
.fontSize,
.textTheme
.titleSmall!
.fontSize! *
MediaQuery.of(context).textScaleFactor,
fontWeight: FontWeight.bold,
color: i['type'] == 'em'
? Theme.of(context).colorScheme.primary

View File

@ -1,15 +1,217 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/searchPanel/index.dart';
Widget searchVideoPanel(BuildContext context, ctr, list) {
return ListView.builder(
controller: ctr!.scrollController,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemCount: list!.length,
itemBuilder: (context, index) {
var i = list![index];
return VideoCardH(videoItem: i);
},
);
class SearchVideoPanel extends StatelessWidget {
SearchVideoPanel({
this.ctr,
this.list,
Key? key,
}) : super(key: key);
final SearchPanelController? ctr;
final List? list;
final VideoPanelController controller = Get.put(VideoPanelController());
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.topCenter,
children: [
Padding(
padding: const EdgeInsets.only(top: 36),
child: ListView.builder(
controller: ctr!.scrollController,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemCount: list!.length,
itemBuilder: (context, index) {
var i = list![index];
return Padding(
padding: index == 0
? const EdgeInsets.only(top: 2)
: EdgeInsets.zero,
child: VideoCardH(videoItem: i),
);
},
),
),
// 分类筛选
Container(
width: double.infinity,
height: 36,
padding: const EdgeInsets.only(left: 8, top: 0, right: 12),
// decoration: BoxDecoration(
// border: Border(
// bottom: BorderSide(
// color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
// ),
// ),
// ),
child: Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Obx(
() => Wrap(
// spacing: ,
children: [
for (var i in controller.filterList) ...[
CustomFilterChip(
label: i['label'],
type: i['type'],
selectedType: controller.selectedType.value,
callFn: (bool selected) async {
controller.selectedType.value = i['type'];
ctr!.order.value =
i['type'].toString().split('.').last;
SmartDialog.showLoading(msg: 'loooad');
await ctr!.onRefresh();
SmartDialog.dismiss();
},
),
]
],
),
),
),
),
const VerticalDivider(indent: 7, endIndent: 8),
const SizedBox(width: 3),
SizedBox(
width: 32,
height: 32,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => controller.onShowFilterDialog(),
icon: Icon(
Icons.filter_list_outlined,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
), // 放置在ListView.builder()上方的组件
],
);
}
}
class CustomFilterChip extends StatelessWidget {
const CustomFilterChip({
this.label,
this.type,
this.selectedType,
this.callFn,
Key? key,
}) : super(key: key);
final String? label;
final ArchiveFilterType? type;
final ArchiveFilterType? selectedType;
final Function? callFn;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 34,
child: FilterChip(
padding: const EdgeInsets.only(left: 11, right: 11),
labelPadding: EdgeInsets.zero,
label: Text(
label!,
style: const TextStyle(fontSize: 13),
),
labelStyle: TextStyle(
color: type == selectedType
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline),
selected: type == selectedType,
showCheckmark: false,
shape: ContinuousRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
selectedColor: Colors.transparent,
// backgroundColor:
// Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
backgroundColor: Colors.transparent,
side: BorderSide.none,
onSelected: (bool selected) => callFn!(selected),
),
);
}
}
class VideoPanelController extends GetxController {
RxList<Map> filterList = [{}].obs;
Rx<ArchiveFilterType> selectedType = ArchiveFilterType.values.first.obs;
List<Map<String, dynamic>> timeFiltersList = [
{'label': '全部时长', 'value': 0},
{'label': '0-10分钟', 'value': 1},
{'label': '10-30分钟', 'value': 2},
{'label': '30-60分钟', 'value': 3},
{'label': '60分钟+', 'value': 4},
];
RxInt currentTimeFilterval = 0.obs;
@override
void onInit() {
List<Map<String, dynamic>> list = ArchiveFilterType.values
.map((type) => {
'label': type.description,
'type': type,
})
.toList();
filterList.value = list;
super.onInit();
}
onShowFilterDialog() {
SmartDialog.show(
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
TextStyle textStyle = Theme.of(context).textTheme.titleMedium!;
return AlertDialog(
title: const Text('时长筛选'),
contentPadding: const EdgeInsets.fromLTRB(0, 15, 0, 20),
content: StatefulBuilder(builder: (context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (var i in timeFiltersList) ...[
RadioListTile(
value: i['value'],
autofocus: true,
title: Text(i['label'], style: textStyle),
groupValue: currentTimeFilterval.value,
onChanged: (value) async {
currentTimeFilterval.value = value!;
setState(() {});
SmartDialog.dismiss();
SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr =
Get.find<SearchPanelController>(tag: 'video');
ctr.duration.value = i['value'];
SmartDialog.showLoading(msg: 'loooad');
await ctr.onRefresh();
SmartDialog.dismiss();
},
),
],
],
);
}),
);
},
);
}
}

View File

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/color_type.dart';
import 'package:pilipala/utils/storage.dart';
class ColorSelectPage extends StatefulWidget {
const ColorSelectPage({super.key});
@override
State<ColorSelectPage> createState() => _ColorSelectPageState();
}
class Item {
Item({
required this.expandedValue,
required this.headerValue,
this.isExpanded = false,
});
String expandedValue;
String headerValue;
bool isExpanded;
}
List<Item> generateItems(int count) {
return List<Item>.generate(count, (int index) {
return Item(
headerValue: 'Panel $index',
expandedValue: 'This is item number $index',
);
});
}
class _ColorSelectPageState extends State<ColorSelectPage> {
final ColorSelectController ctr = Get.put(ColorSelectController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('选择应用主题'),
),
body: ListView(
children: [
Obx(
() => RadioListTile(
value: 0,
title: const Text('动态取色'),
groupValue: ctr.type.value,
onChanged: (dynamic val) async {
ctr.type.value = 0;
ctr.setting.put(SettingBoxKey.dynamicColor, true);
},
),
),
Obx(
() => RadioListTile(
value: 1,
title: const Text('指定颜色'),
groupValue: ctr.type.value,
onChanged: (dynamic val) async {
ctr.type.value = 1;
ctr.setting.put(SettingBoxKey.dynamicColor, false);
},
),
),
Obx(
() {
int type = ctr.type.value;
return AnimatedOpacity(
opacity: type == 1 ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: Padding(
padding: const EdgeInsets.only(top: 12, left: 12, right: 12),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 22,
runSpacing: 18,
children: [
...ctr.colorThemes.map(
(e) {
final index = ctr.colorThemes.indexOf(e);
return GestureDetector(
onTap: () {
ctr.currentColor.value = index;
ctr.setting.put(SettingBoxKey.customColor, index);
Get.forceAppUpdate();
},
child: Column(
children: [
Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: e['color'].withOpacity(0.8),
borderRadius: BorderRadius.circular(50),
border: Border.all(
width: 2,
color: ctr.currentColor.value == index
? Colors.black
: e['color'].withOpacity(0.8),
),
),
child: AnimatedOpacity(
opacity:
ctr.currentColor.value == index ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: const Icon(
Icons.done,
color: Colors.black,
size: 20,
),
),
),
const SizedBox(height: 3),
Text(
e['label'],
style: TextStyle(
fontSize: 12,
color: ctr.currentColor.value != index
? Theme.of(context).colorScheme.outline
: null,
),
),
],
),
);
},
)
],
),
),
);
},
),
],
),
);
}
}
class ColorSelectController extends GetxController {
Box setting = GStrorage.setting;
RxBool dynamicColor = true.obs;
RxInt type = 0.obs;
late final List<Map<String, dynamic>> colorThemes;
RxInt currentColor = 0.obs;
@override
void onInit() {
colorThemes = colorThemeTypes;
// 默认使用动态取色
dynamicColor.value =
setting.get(SettingBoxKey.dynamicColor, defaultValue: true);
type.value = dynamicColor.value ? 0 : 1;
currentColor.value =
setting.get(SettingBoxKey.customColor, defaultValue: 0);
super.onInit();
}
}

View File

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
class FontSizeSelectPage extends StatefulWidget {
const FontSizeSelectPage({super.key});
@override
State<FontSizeSelectPage> createState() => _FontSizeSelectPageState();
}
class _FontSizeSelectPageState extends State<FontSizeSelectPage> {
Box setting = GStrorage.setting;
List<double> list = [0.9, 0.95, 1.0, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3];
late double minsize;
late double maxSize;
late double currentSize;
@override
void initState() {
super.initState();
minsize = list.first;
maxSize = list.last;
currentSize =
setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
}
setFontSize() {
setting.put(SettingBoxKey.defaultTextScale, currentSize);
Get.forceAppUpdate();
Get.back();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(onPressed: () => setFontSize(), child: const Text('确定')),
const SizedBox(width: 12)
],
),
body: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Center(
child: Text(
'当前字体大小:${currentSize == 1.0 ? '默认' : currentSize}',
style: TextStyle(fontSize: 14 * currentSize),
),
),
),
),
Container(
width: double.infinity,
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 20,
bottom: MediaQuery.of(context).padding.bottom + 20,
),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3))),
color: Theme.of(context).colorScheme.background,
),
child: Row(
children: [
const Text(''),
Expanded(
child: Slider(
min: minsize,
value: currentSize,
max: maxSize,
divisions: list.length - 1,
secondaryTrackValue: 1,
onChanged: (double val) {
currentSize = double.parse(val.toStringAsFixed(2));
setState(() {});
},
),
),
const SizedBox(width: 5),
const Text(
'',
style: TextStyle(fontSize: 20),
),
],
),
)
],
),
);
}
}

View File

@ -184,6 +184,16 @@ class _StyleSettingState extends State<StyleSetting> {
style: subTitleStyle)),
trailing: const Icon(Icons.arrow_right_alt_outlined),
),
ListTile(
dense: false,
onTap: () => Get.toNamed('/colorSetting'),
title: Text('应用主题', style: titleStyle),
),
ListTile(
dense: false,
onTap: () => Get.toNamed('/fontSizeSetting'),
title: Text('字体大小', style: titleStyle),
)
],
),
);

View File

@ -228,11 +228,16 @@ class VideoDetailController extends GetxController
// 根据画质选编码格式
List supportDecodeFormats =
supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;
// 默认从设置中取AVC
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code))!;
try {
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: supportDecodeFormats.first))!;
// 当前视频没有对应格式返回第一个
currentDecodeFormats =
supportDecodeFormats.contains(supportDecodeFormats)
? supportDecodeFormats
: supportDecodeFormats.first;
} catch (_) {}
/// 取出符合当前解码格式的videoItem

View File

@ -100,7 +100,7 @@ class IntroDetail extends StatelessWidget {
Text.rich(
style: const TextStyle(
height: 1.4,
fontSize: 13,
// fontSize: 13,
),
TextSpan(
children: [

View File

@ -17,6 +17,8 @@ import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/searchResult/index.dart';
import 'package:pilipala/pages/setting/extra_setting.dart';
import 'package:pilipala/pages/setting/pages/color_select.dart';
import 'package:pilipala/pages/setting/pages/font_size_select.dart';
import 'package:pilipala/pages/setting/play_setting.dart';
import 'package:pilipala/pages/setting/privacy_setting.dart';
import 'package:pilipala/pages/setting/style_setting.dart';
@ -85,6 +87,8 @@ class Routes {
GetPage(name: '/extraSetting', page: () => const ExtraSetting()),
//
GetPage(name: '/blackListPage', page: () => const BlackListPage()),
GetPage(name: '/colorSetting', page: () => const ColorSelectPage()),
GetPage(name: '/fontSizeSetting', page: () => const FontSizeSelectPage()),
// 关于
GetPage(name: '/about', page: () => const AboutPage()),
];

View File

@ -9,7 +9,6 @@ import 'package:pilipala/models/user/info.dart';
class GStrorage {
static late final Box recVideo;
static late final Box userInfo;
static late final Box hotKeyword;
static late final Box historyword;
static late final Box localCache;
static late final Box setting;
@ -38,13 +37,6 @@ class GStrorage {
localCache = await Hive.openBox('localCache');
// 设置
setting = await Hive.openBox('setting');
// 热搜关键词
hotKeyword = await Hive.openBox(
'hotKeyword',
compactionStrategy: (entries, deletedEntries) {
return deletedEntries > 10;
},
);
// 搜索历史
historyword = await Hive.openBox(
'historyWord',
@ -78,8 +70,6 @@ class GStrorage {
recVideo.close();
userInfo.compact();
userInfo.close();
hotKeyword.compact();
hotKeyword.close();
historyword.compact();
historyword.close();
localCache.compact();
@ -92,24 +82,27 @@ class GStrorage {
}
class SettingBoxKey {
static const String themeMode = 'themeMode';
static const String feedBackEnable = 'feedBackEnable';
static const String defaultFontSize = 'fontSize';
static const String defaultVideoQa = 'defaultVideoQa';
static const String defaultAudioQa = 'defaultAudioQa';
static const String defaultDecode = 'defaultDecode';
static const String btmProgressBehavior = 'btmProgressBehavior';
static const String defaultVideoSpeed = 'defaultVideoSpeed';
static const String autoUpgradeEnable = 'autoUpgradeEnable';
static const String feedBackEnable = 'feedBackEnable';
static const String defaultVideoQa = 'defaultVideoQa';
static const String defaultAudioQa = 'defaultAudioQa';
static const String autoPlayEnable = 'autoPlayEnable';
static const String enableHA = 'enableHA';
static const String defaultPicQa = 'defaultPicQa';
static const String danmakuEnable = 'danmakuEnable';
static const String fullScreenMode = 'fullScreenMode';
static const String defaultDecode = 'defaultDecode';
static const String danmakuEnable = 'danmakuEnable';
static const String defaultPicQa = 'defaultPicQa';
static const String enableHA = 'enableHA';
static const String blackMidsList = 'blackMidsList';
static const String autoUpdate = 'autoUpdate';
static const String btmProgressBehavior = 'btmProgressBehavior';
static const String themeMode = 'themeMode';
static const String defaultTextScale = 'textScale';
static const String dynamicColor = 'dynamicColor'; // bool
static const String customColor = 'customColor'; // 自定义主题色
}
class LocalCacheKey {

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.3
version: 1.0.4
environment:
sdk: ">=2.19.6 <3.0.0"