Compare commits
13 Commits
v1.0.3.082
...
v1.0.4.082
Author | SHA1 | Date | |
---|---|---|---|
3ad3ca9d48 | |||
8a8e99f30b | |||
0fe6d6c8e2 | |||
5a03bee410 | |||
b6023e35bc | |||
6a1c89f885 | |||
b7c0ef8341 | |||
9e44995082 | |||
8703d9f576 | |||
5812b5cff1 | |||
1884801ed2 | |||
a19ab8d17f | |||
a4078c0a8e |
@ -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
21
change_log/1.0.4.0822.md
Normal file
@ -0,0 +1,21 @@
|
||||
## 1.0.4
|
||||
|
||||
### 新功能
|
||||
+ 热搜刷新
|
||||
+ 视频搜索排序、筛选
|
||||
+ app字体大小自定义
|
||||
+ app主题色自定义
|
||||
+ 「课堂」类动态渲染
|
||||
|
||||
|
||||
### 修复
|
||||
+ 搜索词联想richText渲染异常
|
||||
+ 部分动态点赞异常
|
||||
+ 默认视频解码格式
|
||||
+ 搜索页面返回搜索词未清空
|
||||
+ 动态详情评论加载异常
|
||||
+ 动态页面下拉刷新数据异常
|
||||
|
||||
### 优化
|
||||
+ 一些样式修改
|
||||
+ 取消热搜词缓存
|
351
lib/common/widgets/app_expansion_panel_list.dart
Normal file
351
lib/common/widgets/app_expansion_panel_list.dart
Normal 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;
|
||||
}
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
23
lib/models/common/color_type.dart
Normal file
23
lib/models/common/color_type.dart
Normal 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': '灰色'},
|
||||
];
|
@ -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];
|
||||
}
|
||||
|
@ -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'] ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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),
|
||||
|
@ -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('🙏 暂未支持的类型,请联系开发者反馈 '),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,6 @@ class VideoContent extends StatelessWidget {
|
||||
favFolderItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
|
@ -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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -159,7 +159,6 @@ class VideoContent extends StatelessWidget {
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
|
@ -205,7 +205,6 @@ class VideoContent extends StatelessWidget {
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
// 返回顶部并刷新
|
||||
|
@ -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:
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
162
lib/pages/setting/pages/color_select.dart
Normal file
162
lib/pages/setting/pages/color_select.dart
Normal 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();
|
||||
}
|
||||
}
|
101
lib/pages/setting/pages/font_size_select.dart
Normal file
101
lib/pages/setting/pages/font_size_select.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -100,7 +100,7 @@ class IntroDetail extends StatelessWidget {
|
||||
Text.rich(
|
||||
style: const TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 13,
|
||||
// fontSize: 13,
|
||||
),
|
||||
TextSpan(
|
||||
children: [
|
||||
|
@ -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()),
|
||||
];
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user