Merge branch 'main' into feature-danmaku

This commit is contained in:
guozhigq
2023-08-26 15:59:04 +08:00
61 changed files with 1578 additions and 323 deletions

View File

@ -11,10 +11,11 @@
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/174shots_so.png" width="32%" alt="home" /> <img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/174shots_so.png" width="32%" alt="home" />
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/850shots_so.png" width="32%" alt="home" /> <img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/850shots_so.png" width="32%" alt="home" />
<br/> <br/>
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/main_screen.png" width="96%" alt="home" />
<br/> <br/>
</div> </div>
### 开发环境 ## 开发环境
Xcode 13.4 不支持**auto_orientation**,请注释相关代码 Xcode 13.4 不支持**auto_orientation**,请注释相关代码
```bash ```bash
@ -30,7 +31,7 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
<br/> <br/>
### 功能 ## 功能
目前着重移动端(Android、iOS)暂时没有适配桌面端、Pad端、手表端等 目前着重移动端(Android、iOS)暂时没有适配桌面端、Pad端、手表端等
@ -98,17 +99,18 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
- [x] 图片质量设定 - [x] 图片质量设定
- [x] 主题模式:亮色/暗色/跟随系统 - [x] 主题模式:亮色/暗色/跟随系统
- [x] 震动反馈(可选) - [x] 震动反馈(可选)
- [x] 高帧率
- [ ] 等等 - [ ] 等等
<br/> <br/>
### 下载 ## 下载
可以通过右侧release进行下载或拉取代码到本地进行编译 可以通过右侧release进行下载或拉取代码到本地进行编译
<br/> <br/>
### 声明 ## 声明
此项目PiliPala是个人为了兴趣而开发, 仅用于学习和测试。 此项目PiliPala是个人为了兴趣而开发, 仅用于学习和测试。
所用API皆从官方网站收集, 不提供任何破解内容。 所用API皆从官方网站收集, 不提供任何破解内容。
@ -117,7 +119,13 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
<br/> <br/>
### 致谢 ## 技术交流
Telegram https://t.me/+lm_oOVmF0RJiODk1
<br/>
## 致谢
- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect) - [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
- [flutter_meedu_videoplayer](https://github.com/zezo357/flutter_meedu_videoplayer) - [flutter_meedu_videoplayer](https://github.com/zezo357/flutter_meedu_videoplayer)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

21
change_log/1.0.4.0822.md Normal file
View File

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

30
change_log/1.0.5.0826.md Normal file
View File

@ -0,0 +1,30 @@
## 1.0.5
主要是bug修复跟一部分小功能弹幕功能需要下一版。
问题反馈请前往QQ频道或提交issues。
感谢🙏酷友「无力感*」「斤斤计较呀」「Pseudopamine」
### 新功能
+ 高帧率支持
+ 默认评论排序设置
+ 默认动态类别设置
+ 动态合集查看
+ 同时观看人数
+ iOS路由切换效果
### 修复
+ 收藏夹翻页
+ 首页搜索框频繁点击消失
+ 评论排序切换空白
+ 快速返回首页
+ 重复进入个人中心页面数据未刷新
+ 动态goods数据异常
+ 大会员切换番剧
+ 高画质codes匹配
### 优化
+ 倍速选择
+ 播放器亮度记忆
+ 下载对应版本apk

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

View File

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

View File

@ -288,4 +288,8 @@ class Api {
// github 获取最新版 // github 获取最新版
static const String latestApp = static const String latestApp =
'https://api.github.com/repos/guozhigq/pilipala/releases/latest'; 'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
// 多少人在看
// https://api.bilibili.com/x/player/online/total?aid=913663681&cid=1203559746&bvid=BV1MM4y1s7NZ&ts=56427838
static const String onlineTotal = '/x/player/online/total';
} }

View File

@ -22,10 +22,18 @@ class DynamicsHttp {
} }
var res = await Request().get(Api.followDynamic, data: data); var res = await Request().get(Api.followDynamic, data: data);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { try {
'status': true, return {
'data': DynamicsDataModel.fromJson(res.data['data']), 'status': true,
}; 'data': DynamicsDataModel.fromJson(res.data['data']),
};
} catch (err) {
return {
'status': false,
'data': [],
'msg': err.toString(),
};
}
} else { } else {
return { return {
'status': false, 'status': false,

View File

@ -51,10 +51,17 @@ class UserHttp {
'up_mid': mid, 'up_mid': mid,
}); });
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
FavFolderData data = FavFolderData.fromJson(res.data['data']); late FavFolderData data;
return {'status': true, 'data': data}; if (res.data['data'] != null) {
data = FavFolderData.fromJson(res.data['data']);
return {'status': true, 'data': data};
}
} else { } else {
return {'status': false, 'data': [], 'msg': '账号未登录'}; return {
'status': false,
'data': [],
'msg': res.data['message'] ?? '账号未登录'
};
} }
} }

View File

@ -399,4 +399,16 @@ class VideoHttp {
return {'status': false, 'msg': res.data['result']['toast']}; return {'status': false, 'msg': res.data['result']['toast']};
} }
} }
// 查看视频同时在看人数
static Future onlineTotal({int? aid, String? bvid, int? cid}) async {
var res = await Request().get(Api.onlineTotal, data: {
'aid': aid,
'bvid': bvid,
'cid': cid,
});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
}
}
} }

View File

@ -7,6 +7,7 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/custom_toast.dart'; import 'package:pilipala/common/widgets/custom_toast.dart';
import 'package:pilipala/http/init.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/models/common/theme_type.dart';
import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
@ -27,6 +28,13 @@ void main() async {
await Request.setCookie(); await Request.setCookie();
await Data.init(); await Data.init();
await GStrorage.lazyInit(); await GStrorage.lazyInit();
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent,
));
}); });
} }
@ -35,15 +43,27 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color brandColor = const Color.fromARGB(255, 92, 182, 123);
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
// 主题色
Color defaultColor =
colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)]
['color'];
Color brandColor = defaultColor;
// 主题模式
ThemeType currentThemeValue = ThemeType.values[setting ThemeType currentThemeValue = ThemeType.values[setting
.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)]; .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( return DynamicColorBuilder(
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) { builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme? lightColorScheme; ColorScheme? lightColorScheme;
ColorScheme? darkColorScheme; ColorScheme? darkColorScheme;
if (lightDynamic != null && darkDynamic != null) { if (lightDynamic != null && darkDynamic != null && isDynamicColor) {
// dynamic取色成功 // dynamic取色成功
lightColorScheme = lightDynamic.harmonized(); lightColorScheme = lightDynamic.harmonized();
darkColorScheme = darkDynamic.harmonized(); darkColorScheme = darkDynamic.harmonized();
@ -93,9 +113,17 @@ class MyApp extends StatelessWidget {
fallbackLocale: const Locale("zh", "CN"), fallbackLocale: const Locale("zh", "CN"),
getPages: Routes.getPages, getPages: Routes.getPages,
home: const MainApp(), home: const MainApp(),
builder: FlutterSmartDialog.init( builder: (BuildContext context, Widget? child) {
toastBuilder: (String msg) => CustomToast(msg: msg), return FlutterSmartDialog(
), toastBuilder: (String msg) => CustomToast(msg: msg),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor:
MediaQuery.of(context).textScaleFactor * textScale),
child: child!,
),
);
},
navigatorObservers: [ navigatorObservers: [
VideoDetailPage.routeObserver, VideoDetailPage.routeObserver,
SearchPage.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

@ -360,7 +360,7 @@ class GoodItem {
String? brief; String? brief;
String? cover; String? cover;
String? id; dynamic id;
String? jumpDesc; String? jumpDesc;
String? jumpUrl; String? jumpUrl;
String? name; String? name;

View File

@ -93,26 +93,19 @@ extension AudioQualityDesc on AudioQuality {
} }
enum VideoDecodeFormats { enum VideoDecodeFormats {
DVH1,
AV1, AV1,
HEVC, HEVC,
AVC, AVC,
} }
extension VideoDecodeFormatsDesc on VideoDecodeFormats { extension VideoDecodeFormatsDesc on VideoDecodeFormats {
static final List<String> _descList = [ static final List<String> _descList = ['DVH1', 'AV1', 'HEVC', 'AVC'];
'AV1',
'HEVC',
'AVC',
];
get description => _descList[index]; get description => _descList[index];
} }
extension VideoDecodeFormatsCode on VideoDecodeFormats { extension VideoDecodeFormatsCode on VideoDecodeFormats {
static final List<String> _codeList = [ static final List<String> _codeList = ['dvh1', 'av01', 'hev1', 'avc1'];
'av01',
'hev1',
'avc1',
];
get code => _codeList[index]; get code => _codeList[index];
static VideoDecodeFormats? fromCode(String code) { static VideoDecodeFormats? fromCode(String code) {

View File

@ -47,6 +47,11 @@ class _AboutPageState extends State<AboutPage> {
'PiliPala', 'PiliPala',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
const SizedBox(height: 6),
Text(
'使用Flutter开发的哔哩哔哩第三方客户端',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
const SizedBox(height: 20), const SizedBox(height: 20),
Obx( Obx(
() => ListTile( () => ListTile(
@ -82,16 +87,6 @@ class _AboutPageState extends State<AboutPage> {
height: 30, height: 30,
color: Theme.of(context).colorScheme.onInverseSurface, color: Theme.of(context).colorScheme.onInverseSurface,
), ),
ListTile(
onTap: () {},
title: const Text('作者'),
trailing: Text('guozhigq', style: subTitleStyle),
),
ListTile(
onTap: () {},
title: const Text('酷安'),
trailing: Text('影若风', style: subTitleStyle),
),
ListTile( ListTile(
onTap: () => _aboutController.githubUrl(), onTap: () => _aboutController.githubUrl(),
title: const Text('Github'), title: const Text('Github'),
@ -123,6 +118,11 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('TG频道'), title: const Text('TG频道'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
), ),
ListTile(
onTap: () => _aboutController.aPay(),
title: const Text('赞助'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
Divider( Divider(
thickness: 8, thickness: 8,
height: 30, height: 30,
@ -141,11 +141,12 @@ class AboutController extends GetxController {
late LatestDataModel remoteAppInfo; late LatestDataModel remoteAppInfo;
RxBool isUpdate = true.obs; RxBool isUpdate = true.obs;
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
late LatestDataModel data;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
init(); // init();
// 获取当前版本 // 获取当前版本
getCurrentApp(); getCurrentApp();
// 获取最新的版本 // 获取最新的版本
@ -153,16 +154,16 @@ class AboutController extends GetxController {
} }
// 获取设备信息 // 获取设备信息
Future init() async { // Future init() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); // DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) { // if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; // AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
print(androidInfo.supportedAbis); // print(androidInfo.supportedAbis);
} else if (Platform.isIOS) { // } else if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo; // IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
print(iosInfo); // print(iosInfo);
} // }
} // }
// 获取当前版本 // 获取当前版本
Future getCurrentApp() async { Future getCurrentApp() async {
@ -173,7 +174,7 @@ class AboutController extends GetxController {
// 获取远程版本 // 获取远程版本
Future getRemoteApp() async { Future getRemoteApp() async {
var result = await Request().get(Api.latestApp); var result = await Request().get(Api.latestApp);
LatestDataModel data = LatestDataModel.fromJson(result.data); data = LatestDataModel.fromJson(result.data);
remoteAppInfo = data; remoteAppInfo = data;
remoteVersion.value = data.tagName!; remoteVersion.value = data.tagName!;
isUpdate.value = isUpdate.value =
@ -183,15 +184,7 @@ class AboutController extends GetxController {
// 跳转下载/本地更新 // 跳转下载/本地更新
Future onUpdate() async { Future onUpdate() async {
// final dir = await getApplicationSupportDirectory(); Utils.matchVersion(data);
// final path = '${dir.path}/pilipala.apk';
// var result = await Request()
// .downloadFile(remoteAppInfo.assets!.first.downloadUrl, path);
// print(result);
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala/releases'),
mode: LaunchMode.externalApplication,
);
} }
// 跳转github // 跳转github
@ -242,4 +235,16 @@ class AboutController extends GetxController {
), ),
); );
} }
aPay() {
try {
launchUrl(
Uri.parse(
'alipayqr://platformapi/startapp?saId=10000007&qrcode=https://qr.alipay.com/fkx14623ddwl1ping3ddd73'),
mode: LaunchMode.externalApplication,
);
} catch (e) {
print(e);
}
}
} }

View File

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

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/utils/storage.dart';
class BangumiPanel extends StatefulWidget { class BangumiPanel extends StatefulWidget {
final List<EpisodeItem> pages; final List<EpisodeItem> pages;
@ -24,12 +26,20 @@ class _BangumiPanelState extends State<BangumiPanel> {
late int currentIndex; late int currentIndex;
final ScrollController listViewScrollCtr = ScrollController(); final ScrollController listViewScrollCtr = ScrollController();
final ScrollController listViewScrollCtr_2 = ScrollController(); final ScrollController listViewScrollCtr_2 = ScrollController();
Box userInfoCache = GStrorage.userInfo;
dynamic userInfo;
// 默认未开通
int vipStatus = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!); currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
scrollToIndex(); scrollToIndex();
userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) {
vipStatus = userInfo.vipStatus;
}
} }
@override @override
@ -126,7 +136,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
} }
void changeFucCall(item, i) async { void changeFucCall(item, i) async {
if (item.badge != null) { if (item.badge != null && vipStatus != 1) {
SmartDialog.showToast('需要大会员'); SmartDialog.showToast('需要大会员');
return; return;
} }

View File

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

View File

@ -13,6 +13,7 @@ import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart'; import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -50,17 +51,21 @@ class DynamicsController extends GetxController {
}, },
]; ];
bool flag = false; bool flag = false;
RxInt initialValue = 1.obs; RxInt initialValue = 0.obs;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs; RxBool userLogin = false.obs;
var userInfo; var userInfo;
RxBool isLoadingDynamic = false.obs; RxBool isLoadingDynamic = false.obs;
Box setting = GStrorage.setting;
@override @override
void onInit() { void onInit() {
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null; userLogin.value = userInfo != null;
super.onInit(); super.onInit();
initialValue.value =
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
dynamicsType = DynamicsType.values[initialValue.value].obs;
} }
Future queryFollowDynamic({type = 'init'}) async { Future queryFollowDynamic({type = 'init'}) async {
@ -99,7 +104,7 @@ class DynamicsController extends GetxController {
} }
onSelectType(value) async { onSelectType(value) async {
dynamicsType.value = filterTypeList[value - 1]['value']; dynamicsType.value = filterTypeList[value]['value'];
dynamicsList.value = [DynamicItemModel()]; dynamicsList.value = [DynamicItemModel()];
page = 1; page = 1;
initialValue.value = value; initialValue.value = value;
@ -109,16 +114,21 @@ class DynamicsController extends GetxController {
pushDetail(item, floor, {action = 'all'}) async { pushDetail(item, floor, {action = 'all'}) async {
feedBack(); feedBack();
/// 点击评论action 直接查看评论
if (action == 'comment') { if (action == 'comment') {
Get.toNamed('/dynamicDetail', Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor, 'action': action}); arguments: {'item': item, 'floor': floor, 'action': action});
return false; return false;
} }
switch (item!.type) { switch (item!.type) {
/// 转发的动态
case 'DYNAMIC_TYPE_FORWARD': case 'DYNAMIC_TYPE_FORWARD':
Get.toNamed('/dynamicDetail', Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor}); arguments: {'item': item, 'floor': floor});
break; break;
/// 图文动态查看
case 'DYNAMIC_TYPE_DRAW': case 'DYNAMIC_TYPE_DRAW':
Get.toNamed('/dynamicDetail', Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor}); arguments: {'item': item, 'floor': floor});
@ -134,6 +144,8 @@ class DynamicsController extends GetxController {
SmartDialog.showToast(err.toString()); SmartDialog.showToast(err.toString());
} }
break; break;
/// 专栏文章查看
case 'DYNAMIC_TYPE_ARTICLE': case 'DYNAMIC_TYPE_ARTICLE':
String title = item.modules.moduleDynamic.major.opus.title; String title = item.modules.moduleDynamic.major.opus.title;
String url = item.modules.moduleDynamic.major.opus.jumpUrl; String url = item.modules.moduleDynamic.major.opus.jumpUrl;
@ -144,7 +156,10 @@ class DynamicsController extends GetxController {
break; break;
case 'DYNAMIC_TYPE_PGC': case 'DYNAMIC_TYPE_PGC':
print('番剧'); print('番剧');
SmartDialog.showToast('暂未支持的类型,请联系开发者');
break; break;
/// 纯文字动态查看
case 'DYNAMIC_TYPE_WORD': case 'DYNAMIC_TYPE_WORD':
print('纯文本'); print('纯文本');
Get.toNamed('/dynamicDetail', Get.toNamed('/dynamicDetail',
@ -168,10 +183,19 @@ class DynamicsController extends GetxController {
}); });
break; break;
/// TODO /// 合集查看
case 'DYNAMIC_TYPE_UGC_SEASON': case 'DYNAMIC_TYPE_UGC_SEASON':
print('合集'); DynamicArchiveModel ugcSeason =
item.modules.moduleDynamic.major.ugcSeason;
int aid = ugcSeason.aid!;
String bvid = IdUtils.av2bv(aid);
String cover = ugcSeason.cover!;
int cid = await SearchHttp.ab2c(bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
arguments: {'pic': cover, 'heroTag': bvid});
break; break;
/// 番剧查看
case 'DYNAMIC_TYPE_PGC_UNION': case 'DYNAMIC_TYPE_PGC_UNION':
print('DYNAMIC_TYPE_PGC_UNION 番剧'); print('DYNAMIC_TYPE_PGC_UNION 番剧');
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc; DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
@ -247,7 +271,7 @@ class DynamicsController extends GetxController {
void resetSearch() { void resetSearch() {
mid.value = -1; mid.value = -1;
dynamicsType.value = DynamicsType.values[0]; dynamicsType.value = DynamicsType.values[0];
initialValue.value = 1; initialValue.value = 0;
SmartDialog.showToast('还原默认加载'); SmartDialog.showToast('还原默认加载');
dynamicsList.value = [DynamicItemModel()]; dynamicsList.value = [DynamicItemModel()];
queryFollowDynamic(); queryFollowDynamic();

View File

@ -1,8 +1,10 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/reply.dart'; import 'package:pilipala/http/reply.dart';
import 'package:pilipala/models/common/reply_sort_type.dart'; import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class DynamicDetailController extends GetxController { class DynamicDetailController extends GetxController {
DynamicDetailController(this.oid, this.type); DynamicDetailController(this.oid, this.type);
@ -16,9 +18,10 @@ class DynamicDetailController extends GetxController {
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs; RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
RxInt acount = 0.obs; RxInt acount = 0.obs;
ReplySortType sortType = ReplySortType.time; ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs; RxString sortTypeTitle = ReplySortType.time.titles.obs;
RxString sortTypeLabel = ReplySortType.time.labels.obs; RxString sortTypeLabel = ReplySortType.time.labels.obs;
Box setting = GStrorage.setting;
@override @override
void onInit() { void onInit() {
@ -29,6 +32,11 @@ class DynamicDetailController extends GetxController {
acount.value = acount.value =
int.parse(item!.modules!.moduleStat!.comment!.count ?? '0'); int.parse(item!.modules!.moduleStat!.comment!.count ?? '0');
} }
int deaultReplySortIndex =
setting.get(SettingBoxKey.replySortType, defaultValue: 0);
_sortType = ReplySortType.values[deaultReplySortIndex];
sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels;
} }
Future queryReplyList({reqType = 'init'}) async { Future queryReplyList({reqType = 'init'}) async {
@ -39,7 +47,7 @@ class DynamicDetailController extends GetxController {
oid: oid!, oid: oid!,
pageNum: currentPage + 1, pageNum: currentPage + 1,
type: type!, type: type!,
sort: sortType.index, sort: _sortType.index,
); );
if (res['status']) { if (res['status']) {
List<ReplyItemModel> replies = res['data'].replies; List<ReplyItemModel> replies = res['data'].replies;
@ -76,20 +84,20 @@ class DynamicDetailController extends GetxController {
// 排序搜索评论 // 排序搜索评论
queryBySort() { queryBySort() {
feedBack(); feedBack();
switch (sortType) { switch (_sortType) {
case ReplySortType.time: case ReplySortType.time:
sortType = ReplySortType.like; _sortType = ReplySortType.like;
break; break;
case ReplySortType.like: case ReplySortType.like:
sortType = ReplySortType.reply; _sortType = ReplySortType.reply;
break; break;
case ReplySortType.reply: case ReplySortType.reply:
sortType = ReplySortType.time; _sortType = ReplySortType.time;
break; break;
default: default:
} }
sortTypeTitle.value = sortType.titles; sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = sortType.labels; sortTypeLabel.value = _sortType.labels;
replyList.clear(); replyList.clear();
queryReplyList(reqType: 'init'); queryReplyList(reqType: 'init');
} }

View File

@ -132,7 +132,7 @@ class _DynamicsPageState extends State<DynamicsPage>
initialValue: initialValue:
_dynamicsController.initialValue.value, _dynamicsController.initialValue.value,
children: { children: {
1: Text( 0: Text(
'全部', '全部',
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context) fontSize: Theme.of(context)
@ -140,19 +140,19 @@ class _DynamicsPageState extends State<DynamicsPage>
.labelMedium! .labelMedium!
.fontSize), .fontSize),
), ),
2: Text('投稿', 1: Text('投稿',
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context) fontSize: Theme.of(context)
.textTheme .textTheme
.labelMedium! .labelMedium!
.fontSize)), .fontSize)),
3: Text('番剧', 2: Text('番剧',
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context) fontSize: Theme.of(context)
.textTheme .textTheme
.labelMedium! .labelMedium!
.fontSize)), .fontSize)),
4: Text('专栏', 3: Text('专栏',
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context) fontSize: Theme.of(context)
.textTheme .textTheme

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
@ -6,23 +8,45 @@ import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class FavController extends GetxController { class FavController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<FavFolderData> favFolderData = FavFolderData().obs; Rx<FavFolderData> favFolderData = FavFolderData().obs;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo; UserInfoData? userInfo;
int currentPage = 1;
int pageSize = 10;
RxBool hasMore = true.obs;
Future<dynamic> queryFavFolder() async { Future<dynamic> queryFavFolder({type = 'init'}) async {
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
if (userInfo == null) { if (userInfo == null) {
return {'status': false, 'msg': '账号未登录'}; return {'status': false, 'msg': '账号未登录'};
} }
if (!hasMore.value) {
return;
}
var res = await await UserHttp.userfavFolder( var res = await await UserHttp.userfavFolder(
pn: 1, pn: currentPage,
ps: 10, ps: pageSize,
mid: userInfo!.mid!, mid: userInfo!.mid!,
); );
if (res['status']) { if (res['status']) {
favFolderData.value = res['data']; if (type == 'init') {
favFolderData.value = res['data'];
} else {
if (res['data'].list.isNotEmpty) {
favFolderData.value.list!.addAll(res['data'].list);
favFolderData.update((val) {});
}
}
hasMore.value = res['data'].hasMore;
currentPage++;
} else {
SmartDialog.showToast(res['msg']);
} }
return res; return res;
} }
Future onLoad() async {
queryFavFolder(type: 'onload');
}
} }

View File

@ -1,3 +1,4 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
@ -14,11 +15,23 @@ class FavPage extends StatefulWidget {
class _FavPageState extends State<FavPage> { class _FavPageState extends State<FavPage> {
final FavController _favController = Get.put(FavController()); final FavController _favController = Get.put(FavController());
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
late ScrollController scrollController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_futureBuilderFuture = _favController.queryFavFolder(); _futureBuilderFuture = _favController.queryFavFolder();
scrollController = _favController.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_favController.onLoad();
});
}
},
);
} }
@override @override
@ -40,6 +53,7 @@ class _FavPageState extends State<FavPage> {
if (data['status']) { if (data['status']) {
return Obx( return Obx(
() => ListView.builder( () => ListView.builder(
controller: scrollController,
itemCount: _favController.favFolderData.value.list!.length, itemCount: _favController.favFolderData.value.list!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return FavItem( return FavItem(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -142,7 +142,8 @@ class _ArchivePanelState extends State<ArchivePanel>
} }
class LoadMoreListSource extends LoadingMoreBase<VListItemModel> { class LoadMoreListSource extends LoadingMoreBase<VListItemModel> {
final ArchiveController _archiveController = Get.put(ArchiveController()); final ArchiveController _archiveController =
Get.put(ArchiveController(), tag: Get.arguments['heroTag']);
@override @override
Future<bool> loadData([bool isloadMoreAction = false]) async { Future<bool> loadData([bool isloadMoreAction = false]) async {

View File

@ -118,7 +118,8 @@ class _MemberDynamicPanelState extends State<MemberDynamicPanel>
} }
class LoadMoreListSource extends LoadingMoreBase<DynamicItemModel> { class LoadMoreListSource extends LoadingMoreBase<DynamicItemModel> {
final _dynamicController = Get.put(MemberDynamicPanelController()); final _dynamicController =
Get.put(MemberDynamicPanelController(), tag: Get.arguments['heroTag']);
@override @override
Future<bool> loadData([bool isloadMoreAction = false]) async { Future<bool> loadData([bool isloadMoreAction = false]) async {

View File

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

View File

@ -45,11 +45,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
return OpenContainer( return OpenContainer(
closedElevation: 0, closedElevation: 0,
openElevation: 0, openElevation: 0,
onClosed: (_) async { onClosed: (_) => _searchController.onClear(),
// 在 openBuilder 关闭时触发的回调函数
await Future.delayed(const Duration(milliseconds: 500));
_searchController.onClear();
},
openColor: Theme.of(context).colorScheme.background, openColor: Theme.of(context).colorScheme.background,
middleColor: Theme.of(context).colorScheme.background, middleColor: Theme.of(context).colorScheme.background,
closedColor: Theme.of(context).colorScheme.background, closedColor: Theme.of(context).colorScheme.background,

View File

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

View File

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

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart'; import 'widgets/switch_item.dart';
@ -11,8 +14,28 @@ class ExtraSetting extends StatefulWidget {
} }
class _ExtraSettingState extends State<ExtraSetting> { class _ExtraSettingState extends State<ExtraSetting> {
Box setting = GStrorage.setting;
late dynamic defaultReplySort;
late dynamic defaultDynamicType;
@override
void initState() {
super.initState();
// 默认优先显示最新评论
defaultReplySort =
setting.get(SettingBoxKey.replySortType, defaultValue: 0);
// 优先展示全部动态 all
defaultDynamicType =
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: false, centerTitle: false,
@ -23,8 +46,58 @@ class _ExtraSettingState extends State<ExtraSetting> {
), ),
), ),
body: ListView( body: ListView(
children: const [ children: [
SetSwitchItem( ListTile(
dense: false,
title: Text('评论展示', style: titleStyle),
subtitle: Text(
'当前优先展示「${ReplySortType.values[defaultReplySort].titles}',
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultReplySort,
icon: const Icon(Icons.more_vert_outlined, size: 22),
onSelected: (item) {
defaultReplySort = item;
setting.put(SettingBoxKey.replySortType, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in ReplySortType.values) ...[
PopupMenuItem(
value: i.index,
child: Text(i.titles),
),
]
],
),
),
ListTile(
dense: false,
title: Text('动态展示', style: titleStyle),
subtitle: Text(
'当前优先展示「${DynamicsType.values[defaultDynamicType].labels}',
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultDynamicType,
icon: const Icon(Icons.more_vert_outlined, size: 22),
onSelected: (item) {
defaultDynamicType = item;
setting.put(SettingBoxKey.defaultDynamicType, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in DynamicsType.values) ...[
PopupMenuItem(
value: i.index,
child: Text(i.labels),
),
]
],
),
),
const SetSwitchItem(
title: '检查更新', title: '检查更新',
subTitle: '每次启动时检查是否需要更新', subTitle: '每次启动时检查是否需要更新',
setKey: SettingBoxKey.autoUpdate, setKey: SettingBoxKey.autoUpdate,

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,94 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
class SetDiaplayMode extends StatefulWidget {
const SetDiaplayMode({super.key});
@override
State<SetDiaplayMode> createState() => _SetDiaplayModeState();
}
class _SetDiaplayModeState extends State<SetDiaplayMode> {
List<DisplayMode> modes = <DisplayMode>[];
DisplayMode? active;
DisplayMode? preferred;
final ValueNotifier<int> page = ValueNotifier<int>(0);
late final PageController controller = PageController()
..addListener(() {
page.value = controller.page!.round();
});
@override
void initState() {
super.initState();
init();
SchedulerBinding.instance.addPostFrameCallback((_) {
fetchAll();
});
}
Future<void> fetchAll() async {
preferred = await FlutterDisplayMode.preferred;
active = await FlutterDisplayMode.active;
// GStorage().setDisplayModeType(preferred!);
setState(() {});
}
Future<void> init() async {
try {
modes = await FlutterDisplayMode.supported;
} on PlatformException catch (e) {
print(e);
}
// var res = await GStorage().getDisplayModeType();
// preferred = modes.toList().firstWhere((el) => el == res);
FlutterDisplayMode.setPreferredMode(preferred!);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('屏幕帧率设置')),
body: SafeArea(
top: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (modes.isEmpty) const Text('Nothing here'),
Padding(
padding: const EdgeInsets.only(left: 25, top: 10, bottom: 5),
child: Text(
'没有生效重启app试试',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
Expanded(
child: ListView.builder(
itemCount: modes.length,
itemBuilder: (_, int i) {
final DisplayMode mode = modes[i];
return RadioListTile<DisplayMode>(
value: mode,
title: mode == DisplayMode.auto
? const Text('自动')
: Text('$mode${mode == active ? " [系统]" : ""}'),
groupValue: preferred,
onChanged: (DisplayMode? newMode) async {
await FlutterDisplayMode.setPreferredMode(newMode!);
await Future<dynamic>.delayed(
const Duration(milliseconds: 100),
);
await fetchAll();
},
);
},
),
),
],
),
),
);
}
}

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

@ -66,6 +66,18 @@ class _PlaySettingState extends State<PlaySetting> {
setKey: SettingBoxKey.enableHA, setKey: SettingBoxKey.enableHA,
defaultVal: true, defaultVal: true,
), ),
const SetSwitchItem(
title: '观看人数',
subTitle: '展示同时在看人数',
setKey: SettingBoxKey.enableOnlineTotal,
defaultVal: false,
),
const SetSwitchItem(
title: '亮度记忆',
subTitle: '返回时自动调整视频亮度',
setKey: SettingBoxKey.enableAutoBrightness,
defaultVal: false,
),
ListTile( ListTile(
dense: false, dense: false,
title: Text('默认画质', style: titleStyle), title: Text('默认画质', style: titleStyle),

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -5,6 +7,7 @@ import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/switch_item.dart';
class StyleSetting extends StatefulWidget { class StyleSetting extends StatefulWidget {
const StyleSetting({super.key}); const StyleSetting({super.key});
@ -66,6 +69,12 @@ class _StyleSettingState extends State<StyleSetting> {
), ),
), ),
), ),
const SetSwitchItem(
title: 'iOS路由切换',
subTitle: 'iOS路由切换样式需重启',
setKey: SettingBoxKey.iosTransition,
defaultVal: false,
),
ListTile( ListTile(
dense: false, dense: false,
onTap: () { onTap: () {
@ -184,6 +193,25 @@ class _StyleSettingState extends State<StyleSetting> {
style: subTitleStyle)), style: subTitleStyle)),
trailing: const Icon(Icons.arrow_right_alt_outlined), trailing: const Icon(Icons.arrow_right_alt_outlined),
), ),
ListTile(
dense: false,
onTap: () => Get.toNamed('/colorSetting'),
title: Text('应用主题', style: titleStyle),
trailing: const Icon(Icons.arrow_forward_ios, size: 17),
),
ListTile(
dense: false,
onTap: () => Get.toNamed('/fontSizeSetting'),
title: Text('字体大小', style: titleStyle),
trailing: const Icon(Icons.arrow_forward_ios, size: 17),
),
if (Platform.isAndroid)
ListTile(
dense: false,
onTap: () => Get.toNamed('/displayModeSetting'),
title: Text('屏幕帧率', style: titleStyle),
trailing: const Icon(Icons.arrow_forward_ios, size: 17),
)
], ],
), ),
); );

View File

@ -32,6 +32,15 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
val = Setting.get(widget.setKey, defaultValue: widget.defaultVal ?? false); val = Setting.get(widget.setKey, defaultValue: widget.defaultVal ?? false);
} }
void switchChange(value) {
val = value ?? !val;
Setting.put(widget.setKey, val);
if (widget.setKey == SettingBoxKey.autoUpdate && value == true) {
Utils.checkUpdata();
}
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!; TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
@ -41,9 +50,7 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
.copyWith(color: Theme.of(context).colorScheme.outline); .copyWith(color: Theme.of(context).colorScheme.outline);
return ListTile( return ListTile(
enableFeedback: true, enableFeedback: true,
onTap: () { onTap: () => switchChange(null),
Setting.put(widget.setKey, !val);
},
title: Text(widget.title!, style: titleStyle), title: Text(widget.title!, style: titleStyle),
subtitle: widget.subTitle != null subtitle: widget.subTitle != null
? Text(widget.subTitle!, style: subTitleStyle) ? Text(widget.subTitle!, style: subTitleStyle)
@ -51,22 +58,16 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
trailing: Transform.scale( trailing: Transform.scale(
scale: 0.8, scale: 0.8,
child: Switch( child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>( thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) { (Set<MaterialState> states) {
if (states.isNotEmpty && states.first == MaterialState.selected) { if (states.isNotEmpty && states.first == MaterialState.selected) {
return const Icon(Icons.done); return const Icon(Icons.done);
} }
return null; // All other states will use the default thumbIcon. return null; // All other states will use the default thumbIcon.
}), }),
value: val, value: val,
onChanged: (value) { onChanged: (val) => switchChange(val),
val = value; ),
Setting.put(widget.setKey, value);
if (widget.setKey == SettingBoxKey.autoUpdate && value == true) {
Utils.checkUpdata();
}
setState(() {});
}),
), ),
); );
} }

View File

@ -14,6 +14,7 @@ import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:screen_brightness/screen_brightness.dart';
class VideoDetailController extends GetxController class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
@ -68,6 +69,8 @@ class VideoDetailController extends GetxController
late String videoUrl; late String videoUrl;
late String audioUrl; late String audioUrl;
late Duration defaultST; late Duration defaultST;
// 亮度
double? brightness;
// 默认记录历史记录 // 默认记录历史记录
bool enableHeart = true; bool enableHeart = true;
var userInfo; var userInfo;
@ -137,8 +140,17 @@ class VideoDetailController extends GetxController
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
List<VideoItem> videoList = List<VideoItem> videoList =
data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList(); data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList();
firstVideo = videoList try {
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code)); firstVideo = videoList
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code));
} catch (_) {
// 当前格式不可用
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code))!;
firstVideo = videoList
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code));
}
videoUrl = firstVideo.baseUrl!; videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl /// 根据currentAudioQa 重新设置audioUrl
@ -152,6 +164,12 @@ class VideoDetailController extends GetxController
} }
Future playerInit({video, audio, seekToTime, duration}) async { Future playerInit({video, audio, seekToTime, duration}) async {
/// 设置/恢复 屏幕亮度
if (brightness != null) {
ScreenBrightness().setScreenBrightness(brightness!);
} else {
ScreenBrightness().resetScreenBrightness();
}
await plPlayerController.setDataSource( await plPlayerController.setDataSource(
DataSource( DataSource(
videoSource: video ?? videoUrl, videoSource: video ?? videoUrl,
@ -234,17 +252,25 @@ class VideoDetailController extends GetxController
defaultValue: VideoDecodeFormats.values.last.code))!; defaultValue: VideoDecodeFormats.values.last.code))!;
try { try {
// 当前视频没有对应格式返回第一个 // 当前视频没有对应格式返回第一个
currentDecodeFormats = currentDecodeFormats = supportDecodeFormats
supportDecodeFormats.contains(supportDecodeFormats) .contains(currentDecodeFormats)
? supportDecodeFormats ? currentDecodeFormats
: supportDecodeFormats.first; : VideoDecodeFormatsCode.fromString(supportDecodeFormats.first)!;
} catch (_) {} } catch (e) {
print(e);
}
/// 取出符合当前解码格式的videoItem /// 取出符合当前解码格式的videoItem
firstVideo = videosList try {
.firstWhere((e) => e.codecs!.startsWith(currentDecodeFormats.code)); firstVideo = videosList.firstWhere(
(e) => e.codecs!.startsWith(currentDecodeFormats.code));
} catch (_) {
firstVideo = videosList.first;
}
videoUrl = firstVideo.baseUrl!; videoUrl = firstVideo.baseUrl!;
} catch (_) {} } catch (err) {
print(err);
}
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量 /// 优先顺序 设置中指定质量 -> 当前可选的最高质量
late AudioItem firstAudio; late AudioItem firstAudio;

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -51,6 +53,12 @@ class VideoIntroController extends GetxController {
RxInt lastPlayCid = 0.obs; RxInt lastPlayCid = 0.obs;
var userInfo; var userInfo;
// 同时观看
bool isShowOnlineTotal = false;
RxInt totel = 1.obs;
Timer? timer;
bool isPaused = false;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -78,6 +86,12 @@ class VideoIntroController extends GetxController {
} }
userLogin = userInfo != null; userLogin = userInfo != null;
lastPlayCid.value = int.parse(Get.parameters['cid']!); lastPlayCid.value = int.parse(Get.parameters['cid']!);
isShowOnlineTotal =
setting.get(SettingBoxKey.enableOnlineTotal, defaultValue: false);
if (isShowOnlineTotal) {
queryOnlineTotal();
startTimer(); // 在页面加载时启动定时器
}
} }
// 获取视频简介&分p // 获取视频简介&分p
@ -417,4 +431,33 @@ class VideoIntroController extends GetxController {
lastPlayCid.value = cid; lastPlayCid.value = cid;
await queryVideoIntro(); await queryVideoIntro();
} }
void startTimer() {
const duration = Duration(seconds: 10); // 设置定时器间隔为10秒
timer = Timer.periodic(duration, (Timer timer) {
if (!isPaused) {
queryOnlineTotal(); // 定时器回调函数,发起请求
}
});
}
// 查看同时在看人数
Future queryOnlineTotal() async {
var result = await VideoHttp.onlineTotal(
aid: IdUtils.bv2av(bvid),
bvid: bvid,
cid: lastPlayCid.value,
);
if (result['status']) {
totel.value = int.parse(result['data']['total']);
}
}
@override
void onClose() {
if (timer != null) {
timer!.cancel(); // 销毁页面时取消定时器
}
super.onClose();
}
} }

View File

@ -111,6 +111,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
late final dynamic owner; late final dynamic owner;
late final dynamic follower; late final dynamic follower;
late final dynamic followStatus; late final dynamic followStatus;
late int mid;
late String memberHeroTag;
@override @override
void initState() { void initState() {
@ -160,14 +162,15 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 用户主页 // 用户主页
onPushMember() { onPushMember() {
feedBack(); feedBack();
int mid = !loadingStatus mid = !loadingStatus
? widget.videoDetail!.owner!.mid ? widget.videoDetail!.owner!.mid
: videoItem['owner'].mid; : videoItem['owner'].mid;
memberHeroTag = Utils.makeHeroTag(mid);
String face = !loadingStatus String face = !loadingStatus
? widget.videoDetail!.owner!.face ? widget.videoDetail!.owner!.face
: videoItem['owner'].face; : videoItem['owner'].face;
Get.toNamed('/member?mid=$mid', Get.toNamed('/member?mid=$mid',
arguments: {'face': face, 'heroTag': (mid + 99).toString()}); arguments: {'face': face, 'heroTag': memberHeroTag});
} }
@override @override
@ -255,6 +258,17 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
color: t.colorScheme.outline, color: t.colorScheme.outline,
), ),
), ),
const SizedBox(width: 10),
if (videoIntroController.isShowOnlineTotal)
Obx(
() => Text(
'${videoIntroController.totel.value}人在看',
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
),
], ],
), ),
), ),

View File

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

View File

@ -28,7 +28,25 @@ class _SeasonPanelState extends State<SeasonPanel> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
episodes = widget.ugcSeason.sections!.first.episodes!;
/// 根据 cid 找到对应集,找到对应 episodes
/// 有多个episodes时只显示其中一个
/// TODO 同时显示多个合集
List<SectionItem> sections = widget.ugcSeason.sections!;
for (int i = 0; i < sections.length; i++) {
List<EpisodeItem> episodesList = sections[i].episodes!;
for (int j = 0; j < episodesList.length; j++) {
if (episodesList[j].cid == widget.cid) {
episodes = episodesList;
continue;
}
}
}
/// 取对应 season_id 的 episodes
// episodes = widget.ugcSeason.sections!
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
// .episodes!;
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid); currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
} }
@ -136,7 +154,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
'${currentIndex + 1}/${widget.ugcSeason.epCount}', '${currentIndex + 1}/${episodes.length}',
style: Theme.of(context).textTheme.labelMedium, style: Theme.of(context).textTheme.labelMedium,
), ),
const SizedBox(width: 6), const SizedBox(width: 6),

View File

@ -1,10 +1,13 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/reply.dart'; import 'package:pilipala/http/reply.dart';
import 'package:pilipala/models/common/reply_sort_type.dart'; import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class VideoReplyController extends GetxController { class VideoReplyController extends GetxController {
VideoReplyController( VideoReplyController(
@ -25,13 +28,26 @@ class VideoReplyController extends GetxController {
bool isLoadingMore = false; bool isLoadingMore = false;
RxString noMore = ''.obs; RxString noMore = ''.obs;
int ps = 20; int ps = 20;
RxInt count = 0.obs;
// 当前回复的回复 // 当前回复的回复
ReplyItemModel? currentReplyItem; ReplyItemModel? currentReplyItem;
ReplySortType sortType = ReplySortType.time; ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs; RxString sortTypeTitle = ReplySortType.time.titles.obs;
RxString sortTypeLabel = ReplySortType.time.labels.obs; RxString sortTypeLabel = ReplySortType.time.labels.obs;
Box setting = GStrorage.setting;
@override
void onInit() {
super.onInit();
int deaultReplySortIndex =
setting.get(SettingBoxKey.replySortType, defaultValue: 0);
_sortType = ReplySortType.values[deaultReplySortIndex];
sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels;
}
Future queryReplyList({type = 'init'}) async { Future queryReplyList({type = 'init'}) async {
isLoadingMore = true; isLoadingMore = true;
if (type == 'init') { if (type == 'init') {
@ -45,7 +61,7 @@ class VideoReplyController extends GetxController {
pageNum: currentPage + 1, pageNum: currentPage + 1,
ps: ps, ps: ps,
type: ReplyType.video.index, type: ReplyType.video.index,
sort: sortType.index, sort: _sortType.index,
); );
if (res['status']) { if (res['status']) {
List<ReplyItemModel> replies = res['data'].replies; List<ReplyItemModel> replies = res['data'].replies;
@ -81,6 +97,7 @@ class VideoReplyController extends GetxController {
replyList.addAll(replies); replyList.addAll(replies);
} }
} }
count.value = res['data'].page.count;
isLoadingMore = false; isLoadingMore = false;
return res; return res;
} }
@ -92,23 +109,26 @@ class VideoReplyController extends GetxController {
// 排序搜索评论 // 排序搜索评论
queryBySort() { queryBySort() {
feedBack(); EasyThrottle.throttle('queryBySort', const Duration(seconds: 1), () {
switch (sortType) { feedBack();
case ReplySortType.time: switch (_sortType) {
sortType = ReplySortType.like; case ReplySortType.time:
break; _sortType = ReplySortType.like;
case ReplySortType.like: break;
sortType = ReplySortType.reply; case ReplySortType.like:
break; _sortType = ReplySortType.reply;
case ReplySortType.reply: break;
sortType = ReplySortType.time; case ReplySortType.reply:
break; _sortType = ReplySortType.time;
default: break;
} default:
sortTypeTitle.value = sortType.titles; }
sortTypeLabel.value = sortType.labels; sortTypeTitle.value = _sortType.titles;
currentPage = 0; sortTypeLabel.value = _sortType.labels;
replyList.clear(); currentPage = 0;
queryReplyList(type: 'init'); noMore.value = '';
replyList.clear();
queryReplyList(type: 'init');
});
} }
} }

View File

@ -160,9 +160,9 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
scale: animation, child: child); scale: animation, child: child);
}, },
child: Text( child: Text(
_videoReplyController.sortTypeTitle.value, '${_videoReplyController.count.value}条回复',
key: ValueKey<String>( key: ValueKey<int>(
_videoReplyController.sortTypeTitle.value), _videoReplyController.count.value),
), ),
), ),
), ),

View File

@ -591,7 +591,8 @@ InlineSpan buildContent(
if (content.jumpUrl.isNotEmpty && hasMatchMember) { if (content.jumpUrl.isNotEmpty && hasMatchMember) {
List urlKeys = content.jumpUrl.keys.toList(); List urlKeys = content.jumpUrl.keys.toList();
matchUrl = matchMember.splitMapJoin( matchUrl = matchMember.splitMapJoin(
RegExp("(?:${urlKeys.join("|")})"), /// RegExp.escape() 转义特殊字符
RegExp(RegExp.escape(urlKeys.join("|"))),
onMatch: (Match match) { onMatch: (Match match) {
String matchStr = match[0]!; String matchStr = match[0]!;
spanChilds.add( spanChilds.add(

View File

@ -37,12 +37,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
PlPlayerController? plPlayerController; PlPlayerController? plPlayerController;
final ScrollController _extendNestCtr = ScrollController(); final ScrollController _extendNestCtr = ScrollController();
late StreamController<double> appbarStream; late StreamController<double> appbarStream;
final VideoIntroController videoIntroController =
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
PlayerStatus playerStatus = PlayerStatus.playing; PlayerStatus playerStatus = PlayerStatus.playing;
// bool isShowCover = true; // bool isShowCover = true;
double doubleOffset = 0; double doubleOffset = 0;
Box localCache = GStrorage.localCache; Box localCache = GStrorage.localCache;
Box setting = GStrorage.setting;
late double statusBarHeight; late double statusBarHeight;
final videoHeight = Get.size.width * 9 / 16; final videoHeight = Get.size.width * 9 / 16;
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
@ -95,7 +98,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override @override
void dispose() { void dispose() {
plPlayerController!.pause();
plPlayerController!.dispose(); plPlayerController!.dispose();
super.dispose(); super.dispose();
} }
@ -103,7 +105,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override @override
// 离开当前页面时 // 离开当前页面时
void didPushNext() async { void didPushNext() async {
/// 开启
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)) {
videoDetailController.brightness = plPlayerController!.brightness.value;
}
videoDetailController.defaultST = plPlayerController!.position.value; videoDetailController.defaultST = plPlayerController!.position.value;
videoIntroController.isPaused = true;
plPlayerController!.pause(); plPlayerController!.pause();
super.didPushNext(); super.didPushNext();
} }
@ -112,6 +119,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// 返回当前页面时 // 返回当前页面时
void didPopNext() async { void didPopNext() async {
videoDetailController.playerInit(); videoDetailController.playerInit();
videoIntroController.isPaused = false;
if (_extendNestCtr.position.pixels == 0) { if (_extendNestCtr.position.pixels == 0) {
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
plPlayerController!.play(); plPlayerController!.play();

View File

@ -387,9 +387,12 @@ class _HeaderControlState extends State<HeaderControl> {
// 当前选中的解码格式 // 当前选中的解码格式
VideoDecodeFormats currentDecodeFormats = VideoDecodeFormats currentDecodeFormats =
widget.videoDetailCtr!.currentDecodeFormats; widget.videoDetailCtr!.currentDecodeFormats;
VideoItem firstVideo = widget.videoDetailCtr!.firstVideo;
// 当前视频可用的解码格式 // 当前视频可用的解码格式
List<FormatItem> videoFormat = videoInfo.supportFormats!; List<FormatItem> videoFormat = videoInfo.supportFormats!;
List list = videoFormat.first.codecs!; List list = videoFormat
.firstWhere((e) => e.quality == firstVideo.quality!.code)
.codecs!;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -483,10 +486,12 @@ class _HeaderControlState extends State<HeaderControl> {
size: 15, size: 15,
color: Colors.white, color: Colors.white,
), ),
fuc: () { fuc: () async {
// 销毁播放器实例 // 销毁播放器实例
widget.controller!.dispose(type: 'all'); await widget.controller!.dispose(type: 'all');
Get.offAll(const MainApp()); if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
}
}, },
), ),
const Spacer(), const Spacer(),
@ -506,9 +511,7 @@ class _HeaderControlState extends State<HeaderControl> {
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
onPressed: () { onPressed: () => showSetSpeedSheet(),
_.togglePlaybackSpeed();
},
child: Text( child: Text(
'${_.playbackSpeed.toString()}X', '${_.playbackSpeed.toString()}X',
style: textStyle, style: textStyle,

View File

@ -619,7 +619,7 @@ class PlPlayerController {
try { try {
brightness.value = brightnes; brightness.value = brightnes;
ScreenBrightness().setScreenBrightness(brightnes); ScreenBrightness().setScreenBrightness(brightnes);
setVideoBrightness(); // setVideoBrightness();
} catch (e) { } catch (e) {
throw 'Failed to set brightness'; throw 'Failed to set brightness';
} }
@ -662,27 +662,24 @@ class PlPlayerController {
} }
/// 缓存fit /// 缓存fit
Future<void> setVideoFit() async { // Future<void> setVideoFit() async {
videoStorage.put(VideoBoxKey.videoBrightness, _videoFit.value.name); // videoStorage.put(VideoBoxKey.videoBrightness, _videoFit.value.name);
} // }
/// 读取fit /// 读取fit
Future<void> getVideoFit() async { // Future<void> getVideoFit() async {
String fitValue = // String fitValue =
videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 'contain'); // videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 'contain');
_videoFit.value = videoFitType // _videoFit.value = videoFitType
.firstWhere((element) => element['attr'] == fitValue)['attr']; // .firstWhere((element) => element['attr'] == fitValue)['attr'];
} // }
/// 缓存亮度
Future<void> setVideoBrightness() async {}
/// 读取亮度 /// 读取亮度
Future<void> getVideoBrightness() async { // Future<void> getVideoBrightness() async {
double brightnessValue = // double brightnessValue =
videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 0.5); // videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 0.5);
setBrightness(brightnessValue); // setBrightness(brightnessValue);
} // }
set controls(bool visible) { set controls(bool visible) {
_showControls.value = visible; _showControls.value = visible;
@ -760,36 +757,41 @@ class PlPlayerController {
Future<void> dispose({String type = 'single'}) async { Future<void> dispose({String type = 'single'}) async {
// 每次减1最后销毁 // 每次减1最后销毁
if (type == 'single') { if (type == 'single' && playerCount.value > 1) {
_playerCount.value -= 1; _playerCount.value -= 1;
_heartDuration = 0; _heartDuration = 0;
if (playerCount.value > 0) { pause();
return; return;
}
} }
_playerCount.value = 0;
try {
_timer?.cancel();
_timerForVolume?.cancel();
_timerForGettingVolume?.cancel();
timerForTrackingMouse?.cancel();
_timerForSeek?.cancel();
videoFitChangedTimer?.cancel();
// _position.close();
_playerEventSubs?.cancel();
// _sliderPosition.close();
// _sliderTempPosition.close();
// _isSliderMoving.close();
// _duration.close();
// _buffered.close();
// _showControls.close();
// _controlsLock.close();
_timer?.cancel(); // playerStatus.status.close();
_timerForVolume?.cancel(); // dataStatus.status.close();
_timerForGettingVolume?.cancel();
timerForTrackingMouse?.cancel();
_timerForSeek?.cancel();
videoFitChangedTimer?.cancel();
_position.close();
_playerEventSubs?.cancel();
_sliderPosition.close();
_sliderTempPosition.close();
_isSliderMoving.close();
_duration.close();
_buffered.close();
_showControls.close();
_controlsLock.close();
playerStatus.status.close(); removeListeners();
dataStatus.status.close(); await _videoPlayerController?.dispose();
_videoPlayerController = null;
removeListeners(); _instance = null;
await _videoPlayerController?.dispose(); // 关闭所有视频页面恢复亮度
_videoPlayerController = null; resetBrightness();
_instance = null; } catch (err) {
print(err);
}
} }
} }

View File

@ -156,6 +156,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}); });
} }
}); });
widget.controller.brightness.value = value;
} }
Future<void> triggerFullScreen() async { Future<void> triggerFullScreen() async {

View File

@ -1,4 +1,8 @@
// ignore_for_file: must_be_immutable
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/about/index.dart'; import 'package:pilipala/pages/about/index.dart';
import 'package:pilipala/pages/blacklist/index.dart'; import 'package:pilipala/pages/blacklist/index.dart';
import 'package:pilipala/pages/dynamics/deatil/index.dart'; import 'package:pilipala/pages/dynamics/deatil/index.dart';
@ -17,6 +21,9 @@ import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/searchResult/index.dart'; import 'package:pilipala/pages/searchResult/index.dart';
import 'package:pilipala/pages/setting/extra_setting.dart'; import 'package:pilipala/pages/setting/extra_setting.dart';
import 'package:pilipala/pages/setting/pages/color_select.dart';
import 'package:pilipala/pages/setting/pages/display_mode.dart';
import 'package:pilipala/pages/setting/pages/font_size_select.dart';
import 'package:pilipala/pages/setting/play_setting.dart'; import 'package:pilipala/pages/setting/play_setting.dart';
import 'package:pilipala/pages/setting/privacy_setting.dart'; import 'package:pilipala/pages/setting/privacy_setting.dart';
import 'package:pilipala/pages/setting/style_setting.dart'; import 'package:pilipala/pages/setting/style_setting.dart';
@ -25,15 +32,20 @@ import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/pages/webview/index.dart'; import 'package:pilipala/pages/webview/index.dart';
import 'package:pilipala/pages/setting/index.dart'; import 'package:pilipala/pages/setting/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart';
Box setting = GStrorage.setting;
bool iosTransition =
setting.get(SettingBoxKey.iosTransition, defaultValue: false);
class Routes { class Routes {
static final List<GetPage> getPages = [ static final List<GetPage> getPages = [
// 首页(推荐) // 首页(推荐)
GetPage(name: '/', page: () => const HomePage()), CustomGetPage(name: '/', page: () => const HomePage()),
// 热门 // 热门
GetPage(name: '/hot', page: () => const HotPage()), CustomGetPage(name: '/hot', page: () => const HotPage()),
// 视频详情 // 视频详情
GetPage(name: '/video', page: () => const VideoDetailPage()), CustomGetPage(name: '/video', page: () => const VideoDetailPage()),
// 图片预览 // 图片预览
GetPage( GetPage(
name: '/preview', name: '/preview',
@ -43,49 +55,77 @@ class Routes {
showCupertinoParallax: false, showCupertinoParallax: false,
), ),
// //
GetPage(name: '/webview', page: () => const WebviewPage()), CustomGetPage(name: '/webview', page: () => const WebviewPage()),
// 设置 // 设置
GetPage(name: '/setting', page: () => const SettingPage()), CustomGetPage(name: '/setting', page: () => const SettingPage()),
// //
GetPage(name: '/media', page: () => const MediaPage()), CustomGetPage(name: '/media', page: () => const MediaPage()),
// //
GetPage(name: '/fav', page: () => const FavPage()), CustomGetPage(name: '/fav', page: () => const FavPage()),
// //
GetPage(name: '/favDetail', page: () => const FavDetailPage()), CustomGetPage(name: '/favDetail', page: () => const FavDetailPage()),
// 稍后再看 // 稍后再看
GetPage(name: '/later', page: () => const LaterPage()), CustomGetPage(name: '/later', page: () => const LaterPage()),
// 历史记录 // 历史记录
GetPage(name: '/history', page: () => const HistoryPage()), CustomGetPage(name: '/history', page: () => const HistoryPage()),
// 搜索页面 // 搜索页面
GetPage(name: '/search', page: () => const SearchPage()), CustomGetPage(name: '/search', page: () => const SearchPage()),
// 搜索结果 // 搜索结果
GetPage(name: '/searchResult', page: () => const SearchResultPage()), CustomGetPage(name: '/searchResult', page: () => const SearchResultPage()),
// 动态 // 动态
GetPage(name: '/dynamics', page: () => const DynamicsPage()), CustomGetPage(name: '/dynamics', page: () => const DynamicsPage()),
// 动态详情 // 动态详情
GetPage(name: '/dynamicDetail', page: () => const DynamicDetailPage()), CustomGetPage(
name: '/dynamicDetail', page: () => const DynamicDetailPage()),
// 关注 // 关注
GetPage(name: '/follow', page: () => const FollowPage()), CustomGetPage(name: '/follow', page: () => const FollowPage()),
// 粉丝 // 粉丝
GetPage(name: '/fan', page: () => const FansPage()), CustomGetPage(name: '/fan', page: () => const FansPage()),
// 直播详情 // 直播详情
GetPage(name: '/liveRoom', page: () => const LiveRoomPage()), CustomGetPage(name: '/liveRoom', page: () => const LiveRoomPage()),
// 用户中心 // 用户中心
GetPage(name: '/member', page: () => const MemberPage()), CustomGetPage(name: '/member', page: () => const MemberPage()),
// 二级回复 // 二级回复
GetPage(name: '/replyReply', page: () => const VideoReplyReplyPanel()), CustomGetPage(
name: '/replyReply', page: () => const VideoReplyReplyPanel()),
// 播放设置 // 播放设置
GetPage(name: '/playSetting', page: () => const PlaySetting()), CustomGetPage(name: '/playSetting', page: () => const PlaySetting()),
// 外观设置 // 外观设置
GetPage(name: '/styleSetting', page: () => const StyleSetting()), CustomGetPage(name: '/styleSetting', page: () => const StyleSetting()),
// 隐私设置 // 隐私设置
GetPage(name: '/privacySetting', page: () => const PrivacySetting()), CustomGetPage(name: '/privacySetting', page: () => const PrivacySetting()),
// 其他设置 // 其他设置
GetPage(name: '/extraSetting', page: () => const ExtraSetting()), CustomGetPage(name: '/extraSetting', page: () => const ExtraSetting()),
// //
GetPage(name: '/blackListPage', page: () => const BlackListPage()), CustomGetPage(name: '/blackListPage', page: () => const BlackListPage()),
CustomGetPage(name: '/colorSetting', page: () => const ColorSelectPage()),
CustomGetPage(
name: '/fontSizeSetting', page: () => const FontSizeSelectPage()),
// 屏幕帧率
CustomGetPage(
name: '/displayModeSetting', page: () => const SetDiaplayMode()),
// 关于 // 关于
GetPage(name: '/about', page: () => const AboutPage()), CustomGetPage(name: '/about', page: () => const AboutPage()),
]; ];
} }
class CustomGetPage extends GetPage {
bool? fullscreen = false;
CustomGetPage({
name,
page,
this.fullscreen,
transitionDuration,
}) : super(
name: name,
page: page,
curve: Curves.linear,
transition: iosTransition ? Transition.cupertino : Transition.native,
showCupertinoParallax: false,
popGesture: false,
transitionDuration: transitionDuration,
fullscreenDialog: fullscreen != null && fullscreen,
);
}

View File

@ -82,24 +82,36 @@ class GStrorage {
} }
class SettingBoxKey { class SettingBoxKey {
static const String themeMode = 'themeMode'; /// 播放器
static const String feedBackEnable = 'feedBackEnable'; static const String btmProgressBehavior = 'btmProgressBehavior';
static const String defaultFontSize = 'fontSize';
static const String defaultVideoQa = 'defaultVideoQa';
static const String defaultAudioQa = 'defaultAudioQa';
static const String defaultDecode = 'defaultDecode';
static const String defaultVideoSpeed = 'defaultVideoSpeed'; static const String defaultVideoSpeed = 'defaultVideoSpeed';
static const String autoUpgradeEnable = 'autoUpgradeEnable'; 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 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 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 enableOnlineTotal = 'enableOnlineTotal';
static const String enableAutoBrightness = 'enableAutoBrightness';
/// 隐私
static const String blackMidsList = 'blackMidsList'; static const String blackMidsList = 'blackMidsList';
/// 其他
static const String autoUpdate = 'autoUpdate'; static const String autoUpdate = 'autoUpdate';
static const String btmProgressBehavior = 'btmProgressBehavior'; static const String replySortType = 'replySortType';
static const String defaultDynamicType = 'defaultDynamicType';
/// 外观
static const String themeMode = 'themeMode';
static const String defaultTextScale = 'textScale';
static const String dynamicColor = 'dynamicColor'; // bool
static const String customColor = 'customColor'; // 自定义主题色
static const String iosTransition = 'iosTransition'; // ios路由
} }
class LocalCacheKey { class LocalCacheKey {

View File

@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get_utils/get_utils.dart'; import 'package:get/get_utils/get_utils.dart';
@ -210,6 +211,7 @@ class Utils {
bool isUpdate = Utils.needUpdate(currentInfo.version, data.tagName!); bool isUpdate = Utils.needUpdate(currentInfo.version, data.tagName!);
if (isUpdate) { if (isUpdate) {
SmartDialog.show( SmartDialog.show(
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text('🎉 发现新版本 '), title: const Text('🎉 发现新版本 '),
@ -228,22 +230,27 @@ class Utils {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => SmartDialog.dismiss(),
child: Text( child: Text(
'稍后', '稍后',
style: style:
TextStyle(color: Theme.of(context).colorScheme.outline), TextStyle(color: Theme.of(context).colorScheme.outline),
)), ),
),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await SmartDialog.dismiss(); await SmartDialog.dismiss();
launchUrl( launchUrl(
Uri.parse( Uri.parse('https://www.123pan.com/s/9sVqVv-flu0A.html'),
'https://github.com/guozhigq/pilipala/releases'), mode: LaunchMode.externalApplication,
mode: LaunchMode.externalApplication, );
); },
}, child: const Text('网盘下载'),
child: const Text('去下载')), ),
TextButton(
onPressed: () => matchVersion(data),
child: const Text('Github下载'),
),
], ],
); );
}, },
@ -251,4 +258,27 @@ class Utils {
} }
return true; return true;
} }
// 下载适用于当前系统的安装包
static Future matchVersion(data) async {
await SmartDialog.dismiss();
// 获取设备信息
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
// [arm64-v8a]
String abi = androidInfo.supportedAbis.first;
late String downloadUrl;
for (var i in data.assets) {
if (i.downloadUrl.contains(abi)) {
downloadUrl = i.downloadUrl;
}
}
// 应用外下载
launchUrl(
Uri.parse(downloadUrl),
mode: LaunchMode.externalApplication,
);
}
}
} }

View File

@ -438,6 +438,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.3.1"
flutter_displaymode:
dependency: "direct main"
description:
name: flutter_displaymode
sha256: "42c5e9abd13d28ed74f701b60529d7f8416947e58256e6659c5550db719c57ef"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:

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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.3 version: 1.0.5
environment: environment:
sdk: ">=2.19.6 <3.0.0" sdk: ">=2.19.6 <3.0.0"
@ -117,6 +117,8 @@ dependencies:
flutter_svg: ^2.0.7 flutter_svg: ^2.0.7
# 防抖节流 # 防抖节流
easy_debounce: ^2.0.3 easy_debounce: ^2.0.3
# 高帧率
flutter_displaymode: ^0.6.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: