Merge branch 'main' into pr/434

This commit is contained in:
orz12
2024-01-27 10:28:55 +08:00
43 changed files with 719 additions and 586 deletions

107
.gitignore vendored
View File

@ -21,6 +21,29 @@ migrate_working_dir/
# is commented out by default.
#.vscode/
# Flutter repo-specific
/bin/cache/
/bin/internal/bootstrap.bat
/bin/internal/bootstrap.sh
/bin/mingit/
/dev/benchmarks/mega_gallery/
/dev/bots/.recipe_deps
/dev/bots/android_tools/
/dev/devicelab/ABresults*.json
/dev/docs/doc/
/dev/docs/api_docs.zip
/dev/docs/flutter.docs.zip
/dev/docs/lib/
/dev/docs/pubspec.yaml
/dev/integration_tests/**/xcuserdata
/dev/integration_tests/**/Pods
/packages/flutter/coverage/
version
analysis_benchmark.json
# packages file containing multi-root paths
.packages.generated
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
@ -31,15 +54,83 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
/*/flutter/[Gg]enerated*[Pp]lugin*
# Android related
**/android/**/gradle-wrapper.jar
.gradle/
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# macOS
**/Flutter/ephemeral/
**/Pods/
**/macos/Flutter/GeneratedPluginRegistrant.swift
**/macos/Flutter/ephemeral
**/xcuserdata/
# Windows
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Linux
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
# Coverage
coverage/
# Symbols
app.*.symbols
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
!.vscode/settings.json

39
change_log/1.0.17.0125.md Normal file
View File

@ -0,0 +1,39 @@
## 1.0.17
### 功能
+ 视频全屏时隐藏进度条
+ 动态内容增加投稿跳转
+ 未开启自动播放时点击封面播放
+ 弹幕发送标识
+ 定时关闭
+ 推荐视频卡片拉黑up功能
+ 首页tabbar编辑排序
### 修复
+ 连续跳转搜索页未刷新
+ 搜索结果为空时页面异常
+ 评论区链接解析
+ 视频全屏状态栏背景色
+ 私信对话气泡位置
+ 设置up关注分组样式
+ 每次推荐请求数据相同
+ iOS代理网络异常
+ 双击切换播放状态无声
+ 设置自定义倍速白屏
+ 免登录查看1080p
### 优化
+ 首页web端推荐观看数展示
+ 首页web端推荐接口更新
+ 首页样式
+ 搜索页跳转
+ 弹幕资源优化
+ 图片渲染占用内存优化(部分)
+ 两次返回退出应用
+ schame 补充
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/extension.dart';
import '../../utils/storage.dart';
import '../constants.dart';
@ -12,55 +13,75 @@ class NetworkImgLayer extends StatelessWidget {
this.src,
required this.width,
required this.height,
this.cacheW,
this.cacheH,
this.type,
this.fadeOutDuration,
this.fadeInDuration,
// 图片质量 默认1%
this.quality,
this.origAspectRatio,
});
final String? src;
final double? width;
final double? height;
final double? cacheW;
final double? cacheH;
final double width;
final double height;
final String? type;
final Duration? fadeOutDuration;
final Duration? fadeInDuration;
final int? quality;
final double? origAspectRatio;
@override
Widget build(BuildContext context) {
final double pr = MediaQuery.of(context).devicePixelRatio;
final int picQuality =
setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10) as int;
final String imageUrl =
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? 100}q.webp';
int? memCacheWidth, memCacheHeight;
double aspectRatio = (width / height).toDouble();
// double pr = 2;
return src != ''
void setMemCacheSizes() {
if (aspectRatio > 1) {
memCacheHeight = height.cacheSize(context);
} else if (aspectRatio < 1) {
memCacheWidth = width.cacheSize(context);
} else {
if (origAspectRatio != null && origAspectRatio! > 1) {
memCacheWidth = width.cacheSize(context);
} else if (origAspectRatio != null && origAspectRatio! < 1) {
memCacheHeight = height.cacheSize(context);
} else {
memCacheWidth = width.cacheSize(context);
memCacheHeight = height.cacheSize(context);
}
}
}
setMemCacheSizes();
if (memCacheWidth == null && memCacheHeight == null) {
memCacheWidth = width.toInt();
}
return src != '' && src != null
? ClipRRect(
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(type == 'avatar'
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(
type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
: StyleString.imgRadius.x,
),
child: CachedNetworkImage(
imageUrl:
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? picQuality}q.webp',
width: width ?? double.infinity,
height: height ?? double.infinity,
maxWidthDiskCache: ((cacheW ?? width!) * pr).toInt(),
// maxHeightDiskCache: (cacheH ?? height!).toInt(),
memCacheWidth: ((cacheW ?? width!) * pr).toInt(),
// memCacheHeight: (cacheH ?? height!).toInt(),
imageUrl: imageUrl,
width: width,
height: height,
memCacheWidth: memCacheWidth,
memCacheHeight: memCacheHeight,
fit: BoxFit.cover,
fadeOutDuration:
fadeOutDuration ?? const Duration(milliseconds: 200),
fadeOutDuration ?? const Duration(milliseconds: 120),
fadeInDuration:
fadeInDuration ?? const Duration(milliseconds: 200),
// filterQuality: FilterQuality.high,
fadeInDuration ?? const Duration(milliseconds: 120),
filterQuality: FilterQuality.high,
errorWidget: (BuildContext context, String url, Object error) =>
placeholder(context),
placeholder: (BuildContext context, String url) =>
@ -72,9 +93,9 @@ class NetworkImgLayer extends StatelessWidget {
Widget placeholder(BuildContext context) {
return Container(
width: width ?? double.infinity,
height: height ?? double.infinity,
clipBehavior: Clip.hardEdge,
width: width,
height: height,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
borderRadius: BorderRadius.circular(type == 'avatar'
@ -88,9 +109,12 @@ class NetworkImgLayer extends StatelessWidget {
type == 'avatar'
? 'assets/images/noface.jpeg'
: 'assets/images/loading.png',
width: 300,
height: 300,
)),
width: width,
height: height,
cacheWidth: width.cacheSize(context),
cacheHeight: height.cacheSize(context),
),
),
);
}
}

View File

@ -69,7 +69,7 @@ class VideoCardH extends StatelessWidget {
final double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.of(context).textScaleFactor) /
MediaQuery.textScalerOf(context).scale(1.0)) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),

View File

@ -326,7 +326,8 @@ class VideoStat extends StatelessWidget {
maxLines: 1,
text: TextSpan(
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
fontSize: MediaQuery.textScalerOf(context)
.scale(Theme.of(context).textTheme.labelSmall!.fontSize!),
color: Theme.of(context).colorScheme.outline,
),
children: [

View File

@ -185,7 +185,7 @@ class Api {
static const String searchDefault = '/x/web-interface/wbi/search/default';
// 搜索关键词
static const String serachSuggest =
static const String searchSuggest =
'https://s.search.bilibili.com/main/suggest';
// 分类搜索
@ -467,4 +467,7 @@ class Api {
/// page_size
static const getSeasonDetailApi =
'/x/polymer/web-space/seasons_archives_list';
/// 获取未读动态数
static const getUnreadDynamic = '/x/web-interface/dynamic/entrance';
}

17
lib/http/common.dart Normal file
View File

@ -0,0 +1,17 @@
import 'index.dart';
class CommonHttp {
static Future unReadDynamic() async {
var res = await Request().get(Api.getUnreadDynamic,
data: {'alltype_offset': 0, 'video_offset': '', 'article_offset': 0});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']['dyn_basic_infos']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -8,6 +8,7 @@ import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
// import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../utils/storage.dart';
import '../utils/utils.dart';
import 'constants.dart';
@ -77,10 +78,11 @@ class Request {
static setOptionsHeaders(userInfo, bool status) {
if (status) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
dio.options.headers['x-bili-aurora-eid'] =
IdUtils.genAuroraEid(userInfo.mid);
}
dio.options.headers['env'] = 'prod';
dio.options.headers['app-key'] = 'android64';
dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
dio.options.headers['x-bili-aurora-zone'] = 'sh001';
dio.options.headers['referer'] = 'https://www.bilibili.com/';
}

View File

@ -70,14 +70,14 @@ class ApiInterceptor extends Interceptor {
case DioExceptionType.sendTimeout:
return '发送请求超时,请检查网络设置';
case DioExceptionType.unknown:
final String res = await checkConect();
final String res = await checkConnect();
return '$res \n 网络异常,请稍后重试!';
// default:
// return 'Dio异常';
}
}
static Future<String> checkConect() async {
static Future<String> checkConnect() async {
final ConnectivityResult connectivityResult =
await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.mobile) {

View File

@ -36,7 +36,7 @@ class SearchHttp {
// 获取搜索建议
static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest,
var res = await Request().get(Api.searchSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data is String) {
Map<String, dynamic> resultMap = json.decode(res.data);

View File

@ -158,9 +158,8 @@ class MyApp extends StatelessWidget {
return FlutterSmartDialog(
toastBuilder: (String msg) => CustomToast(msg: msg),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor:
MediaQuery.of(context).textScaleFactor * textScale),
data: MediaQuery.of(context)
.copyWith(textScaler: TextScaler.linear(textScale)),
child: child!,
),
);

View File

@ -9,6 +9,7 @@ enum TabType { live, rcmd, hot, bangumi }
extension TabTypeDesc on TabType {
String get description => ['直播', '推荐', '热门', '番剧'][index];
String get id => ['live', 'rcmd', 'hot', 'bangumi'][index];
}
List tabsConfig = [

View File

@ -1,8 +1,3 @@
import 'package:hive/hive.dart';
part 'result.g.dart';
@HiveType(typeId: 0)
class RecVideoItemAppModel {
RecVideoItemAppModel({
this.id,
@ -27,47 +22,27 @@ class RecVideoItemAppModel {
this.adInfo,
});
@HiveField(0)
int? id;
@HiveField(1)
int? aid;
@HiveField(2)
String? bvid;
@HiveField(3)
int? cid;
@HiveField(4)
String? pic;
@HiveField(5)
RcmdStat? stat;
@HiveField(6)
int? duration;
@HiveField(7)
String? title;
@HiveField(8)
int? isFollowed;
@HiveField(9)
RcmdOwner? owner;
@HiveField(10)
RcmdReason? rcmdReason;
@HiveField(11)
String? goto;
@HiveField(12)
int? param;
@HiveField(13)
String? uri;
@HiveField(14)
String? talkBack;
// 番剧
@HiveField(15)
String? bangumiView;
@HiveField(16)
String? bangumiFollow;
@HiveField(17)
String? bangumiBadge;
@HiveField(18)
String? cardType;
@HiveField(19)
Map? adInfo;
RecVideoItemAppModel.fromJson(Map<String, dynamic> json) {
@ -116,18 +91,14 @@ class RecVideoItemAppModel {
}
}
@HiveType(typeId: 1)
class RcmdStat {
RcmdStat({
this.view,
this.like,
this.danmu,
});
@HiveField(0)
String? view;
@HiveField(1)
String? like;
@HiveField(2)
String? danmu;
RcmdStat.fromJson(Map<String, dynamic> json) {
@ -136,13 +107,10 @@ class RcmdStat {
}
}
@HiveType(typeId: 2)
class RcmdOwner {
RcmdOwner({this.name, this.mid});
@HiveField(0)
String? name;
@HiveField(1)
int? mid;
RcmdOwner.fromJson(Map<String, dynamic> json) {
@ -155,13 +123,11 @@ class RcmdOwner {
}
}
@HiveType(typeId: 8)
class RcmdReason {
RcmdReason({
this.content,
});
@HiveField(0)
String? content;
RcmdReason.fromJson(Map<String, dynamic> json) {

View File

@ -1,209 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'result.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RecVideoItemAppModelAdapter extends TypeAdapter<RecVideoItemAppModel> {
@override
final int typeId = 0;
@override
RecVideoItemAppModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RecVideoItemAppModel(
id: fields[0] as int?,
aid: fields[1] as int?,
bvid: fields[2] as String?,
cid: fields[3] as int?,
pic: fields[4] as String?,
stat: fields[5] as RcmdStat?,
duration: fields[6] as int?,
title: fields[7] as String?,
isFollowed: fields[8] as int?,
owner: fields[9] as RcmdOwner?,
rcmdReason: fields[10] as RcmdReason?,
goto: fields[11] as String?,
param: fields[12] as int?,
uri: fields[13] as String?,
talkBack: fields[14] as String?,
bangumiView: fields[15] as String?,
bangumiFollow: fields[16] as String?,
bangumiBadge: fields[17] as String?,
cardType: fields[18] as String?,
adInfo: (fields[19] as Map?)?.cast<dynamic, dynamic>(),
);
}
@override
void write(BinaryWriter writer, RecVideoItemAppModel obj) {
writer
..writeByte(20)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.aid)
..writeByte(2)
..write(obj.bvid)
..writeByte(3)
..write(obj.cid)
..writeByte(4)
..write(obj.pic)
..writeByte(5)
..write(obj.stat)
..writeByte(6)
..write(obj.duration)
..writeByte(7)
..write(obj.title)
..writeByte(8)
..write(obj.isFollowed)
..writeByte(9)
..write(obj.owner)
..writeByte(10)
..write(obj.rcmdReason)
..writeByte(11)
..write(obj.goto)
..writeByte(12)
..write(obj.param)
..writeByte(13)
..write(obj.uri)
..writeByte(14)
..write(obj.talkBack)
..writeByte(15)
..write(obj.bangumiView)
..writeByte(16)
..write(obj.bangumiFollow)
..writeByte(17)
..write(obj.bangumiBadge)
..writeByte(18)
..write(obj.cardType)
..writeByte(19)
..write(obj.adInfo);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RecVideoItemAppModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdStatAdapter extends TypeAdapter<RcmdStat> {
@override
final int typeId = 1;
@override
RcmdStat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdStat(
view: fields[0] as String?,
like: fields[1] as String?,
danmu: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdStat obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.view)
..writeByte(1)
..write(obj.like)
..writeByte(2)
..write(obj.danmu);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdStatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdOwnerAdapter extends TypeAdapter<RcmdOwner> {
@override
final int typeId = 2;
@override
RcmdOwner read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdOwner(
name: fields[0] as String?,
mid: fields[1] as int?,
);
}
@override
void write(BinaryWriter writer, RcmdOwner obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.mid);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdOwnerAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdReasonAdapter extends TypeAdapter<RcmdReason> {
@override
final int typeId = 8;
@override
RcmdReason read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdReason(
content: fields[0] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdReason obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.content);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdReasonAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -86,6 +86,14 @@ class _AboutPageState extends State<AboutPage> {
style: subTitleStyle,
),
),
ListTile(
onTap: () => _aboutController.webSiteUrl(),
title: const Text('访问官网'),
trailing: Text(
'https://pilipalanet.mysxl.cn',
style: subTitleStyle,
),
),
ListTile(
onTap: () => _aboutController.panDownload(),
title: const Text('网盘下载'),
@ -244,4 +252,12 @@ class AboutController extends GetxController {
print(e);
}
}
// 官网
webSiteUrl() {
launchUrl(
Uri.parse('https://pilipalanet.mysxl.cn'),
mode: LaunchMode.externalApplication,
);
}
}

View File

@ -224,7 +224,7 @@ class _BangumiPageState extends State<BangumiPage>
// 列数
crossAxisCount: 3,
mainAxisExtent: Get.size.width / 3 / 0.65 +
32 * MediaQuery.of(context).textScaleFactor,
MediaQuery.textScalerOf(context).scale(32.0),
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {

View File

@ -1,5 +1,6 @@
// 内容
import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/preview/index.dart';
@ -48,6 +49,13 @@ class _ContentState extends State<Content> {
WidgetSpan(
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
double maxWidth = box.maxWidth.truncateToDouble();
double maxHeight = box.maxWidth * 0.6; // 设置最大高度
double height = maxWidth *
0.5 *
(pictureItem.height != null && pictureItem.width != null
? pictureItem.height! / pictureItem.width!
: 1);
return GestureDetector(
onTap: () {
showDialog(
@ -58,18 +66,29 @@ class _ContentState extends State<Content> {
},
);
},
child: Padding(
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
width: box.maxWidth / 2,
height: height,
child: Stack(
children: [
Positioned.fill(
child: NetworkImgLayer(
src: pictureItem.url,
width: box.maxWidth / 2,
height: box.maxWidth *
0.5 *
(pictureItem.height != null && pictureItem.width != null
? pictureItem.height! / pictureItem.width!
: 1),
width: maxWidth / 2,
height: height,
),
),
height > maxHeight
? const PBadge(
text: '长图',
right: 8,
bottom: 8,
)
: const SizedBox(),
],
)),
);
},
),
@ -83,6 +102,7 @@ class _ContentState extends State<Content> {
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
double maxWidth = box.maxWidth.truncateToDouble();
return GestureDetector(
onTap: () {
showDialog(
@ -95,8 +115,10 @@ class _ContentState extends State<Content> {
},
child: NetworkImgLayer(
src: pics[i].url,
width: box.maxWidth,
height: box.maxWidth,
width: maxWidth,
height: maxWidth,
origAspectRatio:
pics[i].width!.toInt() / pics[i].height!.toInt(),
),
);
},
@ -107,7 +129,7 @@ class _ContentState extends State<Content> {
WidgetSpan(
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
double maxWidth = box.maxWidth;
double maxWidth = box.maxWidth.truncateToDouble();
double crossCount = len < 3 ? 2 : 3;
double height = maxWidth /
crossCount *

View File

@ -5,15 +5,17 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/utils/storage.dart';
import '../../http/index.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin {
bool flag = false;
late List tabs;
late RxList tabs = [].obs;
RxInt initialIndex = 1.obs;
late TabController tabController;
late List tabsCtrList;
late List<Widget> tabsPageList;
Box userInfoCache = GStrorage.userInfo;
Box settingStorage = GStrorage.setting;
RxBool userLogin = false.obs;
RxString userFace = ''.obs;
var userInfo;
@ -21,6 +23,9 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
late final StreamController<bool> searchBarStream =
StreamController<bool>.broadcast();
late bool hideSearchBar;
late List defaultTabs;
late List<String> tabbarSort;
RxString defaultSearch = ''.obs;
@override
void onInit() {
@ -28,19 +33,13 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
userFace.value = userInfo != null ? userInfo.face : '';
// 进行tabs配置
tabs = tabsConfig;
tabsCtrList = tabsConfig.map((e) => e['ctr']).toList();
tabsPageList = tabsConfig.map<Widget>((e) => e['page']).toList();
tabController = TabController(
initialIndex: initialIndex.value,
length: tabs.length,
vsync: this,
);
setTabConfig();
hideSearchBar =
setting.get(SettingBoxKey.hideSearchBar, defaultValue: true);
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault();
}
}
void onRefresh() {
@ -62,4 +61,49 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
if (val) return;
userFace.value = userInfo != null ? userInfo.face : '';
}
void setTabConfig() async {
defaultTabs = tabsConfig;
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
defaultValue: ['live', 'rcmd', 'hot', 'bangumi']);
tabs.value = defaultTabs
.where((i) => tabbarSort.contains((i['type'] as TabType).id))
.toList();
if (tabbarSort.contains(TabType.rcmd.id)) {
initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id);
} else {
initialIndex.value = 0;
}
tabsCtrList = tabs.map((e) => e['ctr']).toList();
tabsPageList = tabs.map<Widget>((e) => e['page']).toList();
tabController = TabController(
initialIndex: initialIndex.value,
length: tabs.length,
vsync: this,
);
// 监听 tabController 切换
tabController.animation!.addListener(() {
if (tabController.indexIsChanging) {
if (initialIndex.value != tabController.index) {
initialIndex.value = tabController.index;
}
} else {
final int temp = tabController.animation!.value.round();
if (initialIndex.value != temp) {
initialIndex.value = temp;
tabController.index = initialIndex.value;
}
}
});
}
void searchDefault() async {
var res = await Request().get(Api.searchDefault);
if (res.data['code'] == 0) {
defaultSearch.value = res.data['data']['name'];
}
}
}

View File

@ -1,10 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import './controller.dart';
@ -46,6 +46,13 @@ class _HomePageState extends State<HomePage>
@override
Widget build(BuildContext context) {
super.build(context);
Brightness currentBrightness = MediaQuery.of(context).platformBrightness;
// 设置状态栏图标的亮度
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarIconBrightness: currentBrightness == Brightness.light
? Brightness.dark
: Brightness.light,
));
return Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
@ -82,7 +89,11 @@ class _HomePageState extends State<HomePage>
ctr: _homeController,
callback: showUserBottomSheet,
),
if (_homeController.tabs.length > 1) ...[
const CustomTabs(),
] else ...[
const SizedBox(height: 6),
],
Expanded(
child: TabBarView(
controller: _homeController.tabController,
@ -129,9 +140,10 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 500),
height: snapshot.data ? top + 52 : top,
padding: EdgeInsets.fromLTRB(14, top, 14, 0),
padding: EdgeInsets.fromLTRB(14, top + 6, 14, 0),
child: UserInfoWidget(
top: top,
ctr: ctr,
userLogin: isUserLoggedIn,
userFace: ctr?.userFace.value,
callback: () => callback!(),
@ -150,18 +162,20 @@ class UserInfoWidget extends StatelessWidget {
required this.userLogin,
required this.userFace,
required this.callback,
required this.ctr,
}) : super(key: key);
final double top;
final RxBool userLogin;
final String? userFace;
final VoidCallback? callback;
final HomeController? ctr;
@override
Widget build(BuildContext context) {
return Row(
children: [
const SearchBar(),
SearchBar(ctr: ctr),
if (userLogin.value) ...[
const SizedBox(width: 4),
ClipRect(
@ -242,17 +256,6 @@ class CustomTabs extends StatefulWidget {
class _CustomTabsState extends State<CustomTabs> {
final HomeController _homeController = Get.put(HomeController());
int currentTabIndex = 1;
@override
void initState() {
super.initState();
_homeController.tabController.addListener(listen);
}
void listen() {
_homeController.initialIndex.value = _homeController.tabController.index;
}
void onTap(int index) {
feedBack();
@ -263,18 +266,13 @@ class _CustomTabsState extends State<CustomTabs> {
_homeController.tabController.index = index;
}
@override
void dispose() {
super.dispose();
_homeController.tabController.removeListener(listen);
}
@override
Widget build(BuildContext context) {
return Container(
height: 44,
margin: const EdgeInsets.only(top: 4),
child: ListView.separated(
child: Obx(
() => ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 14.0),
scrollDirection: Axis.horizontal,
itemCount: _homeController.tabs.length,
@ -292,6 +290,7 @@ class _CustomTabsState extends State<CustomTabs> {
);
},
),
),
);
}
}
@ -338,11 +337,15 @@ class CustomChip extends StatelessWidget {
}
class SearchBar extends StatelessWidget {
const SearchBar({super.key});
const SearchBar({
Key? key,
required this.ctr,
}) : super(key: key);
final HomeController? ctr;
@override
Widget build(BuildContext context) {
final SSearchController searchController = Get.put(SSearchController());
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Expanded(
child: Container(
@ -356,7 +359,10 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSecondaryContainer.withOpacity(0.05),
child: InkWell(
splashColor: colorScheme.primaryContainer.withOpacity(0.3),
onTap: () => Get.toNamed('/search'),
onTap: () => Get.toNamed(
'/search',
parameters: {'hintText': ctr!.defaultSearch.value},
),
child: Row(
children: [
const SizedBox(width: 14),
@ -365,16 +371,14 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSecondaryContainer,
),
const SizedBox(width: 10),
Expanded(
child: Obx(
Obx(
() => Text(
searchController.defaultSearch.value,
ctr!.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: colorScheme.outline),
),
),
),
],
),
),

View File

@ -162,8 +162,9 @@ class _LivePageState extends State<LivePage>
crossAxisCount: crossAxisCount,
mainAxisExtent:
Get.size.width / crossAxisCount / StyleString.aspectRatio +
(crossAxisCount == 1 ? 48 : 68) *
MediaQuery.of(context).textScaleFactor,
MediaQuery.textScalerOf(context).scale(
(crossAxisCount == 1 ? 48 : 68),
),
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {

View File

@ -1,9 +1,11 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/common.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.dart';
@ -27,6 +29,7 @@ class MainController extends GetxController {
size: 21,
),
'label': "首页",
'count': 0,
},
{
'icon': const Icon(
@ -38,6 +41,7 @@ class MainController extends GetxController {
size: 21,
),
'label': "动态",
'count': 0,
},
{
'icon': const Icon(
@ -49,6 +53,7 @@ class MainController extends GetxController {
size: 21,
),
'label': "媒体库",
'count': 0,
}
].obs;
final StreamController<bool> bottomBarStream =
@ -56,6 +61,10 @@ class MainController extends GetxController {
Box setting = GStrorage.setting;
DateTime? _lastPressedAt;
late bool hideTabBar;
late PageController pageController;
int selectedIndex = 0;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
@override
void onInit() {
@ -64,17 +73,47 @@ class MainController extends GetxController {
Utils.checkUpdata();
}
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
var userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
getUnreadDynamic();
}
Future<bool> onBackPressed(BuildContext context) {
void onBackPressed(BuildContext context) {
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt!) >
const Duration(seconds: 2)) {
// 两次点击时间间隔超过2秒重新记录时间戳
_lastPressedAt = DateTime.now();
SmartDialog.showToast("再按一次退出Pili");
return Future.value(false); // 不退出应用
if (selectedIndex != 0) {
pageController.jumpTo(0);
}
return Future.value(true); // 退出应用
SmartDialog.showToast("再按一次退出Pili");
return; // 不退出应用
}
SystemNavigator.pop(); // 退出应用
}
void getUnreadDynamic() async {
if (!userLogin.value) {
return;
}
int dynamicItemIndex =
navigationBars.indexWhere((item) => item['label'] == "动态");
var res = await CommonHttp.unReadDynamic();
var data = res['data'];
if (dynamicItemIndex != -1) {
navigationBars[dynamicItemIndex]['count'] =
data == null ? 0 : data.length; // 修改 count 属性为新的值
}
navigationBars.refresh();
}
void clearUnread() async {
int dynamicItemIndex =
navigationBars.indexWhere((item) => item['label'] == "动态");
if (dynamicItemIndex != -1) {
navigationBars[dynamicItemIndex]['count'] = 0; // 修改 count 属性为新的值
}
navigationBars.refresh();
}
}

View File

@ -24,8 +24,6 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
final DynamicsController _dynamicController = Get.put(DynamicsController());
final MediaController _mediaController = Get.put(MediaController());
PageController? _pageController;
int selectedIndex = 0;
int? _lastSelectTime; //上次点击时间
Box setting = GStrorage.setting;
late bool enableMYBar;
@ -34,13 +32,14 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
void initState() {
super.initState();
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
_pageController = PageController(initialPage: selectedIndex);
_mainController.pageController =
PageController(initialPage: _mainController.selectedIndex);
enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
}
void setIndex(int value) async {
feedBack();
_pageController!.jumpToPage(value);
_mainController.pageController.jumpToPage(value);
var currentPage = _mainController.pages[value];
if (currentPage is HomePage) {
if (_homeController.flag) {
@ -68,6 +67,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
}
_dynamicController.flag = true;
_mainController.clearUnread();
} else {
_dynamicController.flag = false;
}
@ -94,14 +94,17 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
localCache.put('sheetHeight', sheetHeight);
localCache.put('statusBarHeight', statusBarHeight);
return PopScope(
onPopInvoked: (bool status) => _mainController.onBackPressed(context),
canPop: false,
onPopInvoked: (bool didPop) async {
_mainController.onBackPressed(context);
},
child: Scaffold(
extendBody: true,
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
controller: _mainController.pageController,
onPageChanged: (index) {
selectedIndex = index;
_mainController.selectedIndex = index;
setState(() {});
},
children: _mainController.pages,
@ -116,14 +119,20 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1),
child: enableMYBar
child: Obx(
() => enableMYBar
? NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: selectedIndex,
selectedIndex: _mainController.selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: e['icon'],
icon: Badge(
label: Text(e['count'].toString()),
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible: e['count'] > 0,
child: e['icon'],
),
selectedIcon: e['selectIcon'],
label: e['label'],
);
@ -131,7 +140,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
],
)
: BottomNavigationBar(
currentIndex: selectedIndex,
currentIndex: _mainController.selectedIndex,
onTap: (value) => setIndex(value),
iconSize: 16,
selectedFontSize: 12,
@ -139,13 +148,19 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: e['icon'],
icon: Badge(
label: Text(e['count'].toString()),
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
isLabelVisible: e['count'] > 0,
child: e['icon'],
),
activeIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
),
);
},
),

View File

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

View File

@ -13,7 +13,6 @@ class RcmdController extends GetxController {
// RxList<RecVideoItemModel> webVideoList = <RecVideoItemModel>[].obs;
bool isLoadingMore = true;
OverlayEntry? popupDialog;
Box recVideo = GStrorage.recVideo;
Box setting = GStrorage.setting;
RxInt crossAxisCount = 2.obs;
late bool enableSaveLastData;
@ -25,15 +24,6 @@ class RcmdController extends GetxController {
super.onInit();
crossAxisCount.value =
setting.get(SettingBoxKey.customRows, defaultValue: 2);
// 读取app端缓存内容
// if (recVideo.get('cacheList') != null &&
// recVideo.get('cacheList').isNotEmpty) {
// List<RecVideoItemAppModel> list = [];
// for (var i in recVideo.get('cacheList')) {
// list.add(i);
// }
// videoList.value = list;
// }
enableSaveLastData =
setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
defaultRcmdType =
@ -84,10 +74,6 @@ class RcmdController extends GetxController {
} else if (type == 'onLoad') {
videoList.addAll(res['data']);
}
// 目前仅支持app端系列保存缓存
if (defaultRcmdType != 'web') {
recVideo.put('cacheList', res['data']);
}
_currentPage += 1;
// 若videoList数量太小可能会影响翻页此时再次请求
// 为避免请求到的数据太少时还在反复请求要求本次返回数据大于1条才触发

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart';
@ -27,9 +26,6 @@ class SSearchController extends GetxController {
@override
void onInit() {
super.onInit();
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault();
}
// 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) {
if (Get.parameters['keyword'] != null) {
@ -130,12 +126,4 @@ class SSearchController extends GetxController {
historyList.refresh();
histiryWord.put('cacheList', []);
}
void searchDefault() async {
var res = await Request().get(Api.searchDefault);
if (res.data['code'] == 0) {
searchKeyWord.value =
hintText = defaultSearch.value = res.data['data']['name'];
}
}
}

View File

@ -28,8 +28,7 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.of(context).textScaleFactor) /
2;
MediaQuery.textScalerOf(context).scale(2.0));
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,

View File

@ -17,7 +17,7 @@ Widget searchLivePanel(BuildContext context, ctr, list) {
mainAxisSpacing: StyleString.cardSpace + 3,
mainAxisExtent:
MediaQuery.sizeOf(context).width / 2 / StyleString.aspectRatio +
66 * MediaQuery.of(context).textScaleFactor),
MediaQuery.textScalerOf(context).scale(66.0)),
itemCount: list.length,
itemBuilder: (context, index) {
return LiveItem(liveItem: list![index]);

View File

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

View File

@ -90,7 +90,7 @@ class SearchVideoPanel extends StatelessWidget {
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => controller.onShowFilterDialog(),
onPressed: () => controller.onShowFilterDialog(ctr),
icon: Icon(
Icons.filter_list_outlined,
size: 18,
@ -175,7 +175,7 @@ class VideoPanelController extends GetxController {
super.onInit();
}
onShowFilterDialog() {
onShowFilterDialog(searchPanelCtr) {
SmartDialog.show(
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
@ -199,7 +199,8 @@ class VideoPanelController extends GetxController {
SmartDialog.dismiss();
SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr =
Get.find<SearchPanelController>(tag: 'video');
Get.find<SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}');
ctr.duration.value = i['value'];
SmartDialog.showLoading(msg: 'loooad');
await ctr.onRefresh();

View File

@ -86,7 +86,8 @@ class _SearchResultPageState extends State<SearchResultPage>
onTap: (index) {
if (index == _searchResultController!.tabIndex) {
Get.find<SearchPanelController>(
tag: SearchType.values[index].type)
tag: SearchType.values[index].type +
_searchResultController!.keyword!)
.animateToTop();
}

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/utils/storage.dart';
class TabbarSetPage extends StatefulWidget {
const TabbarSetPage({super.key});
@override
State<TabbarSetPage> createState() => _TabbarSetPageState();
}
class _TabbarSetPageState extends State<TabbarSetPage> {
Box settingStorage = GStrorage.setting;
late List defaultTabs;
late List<String> tabbarSort;
@override
void initState() {
super.initState();
defaultTabs = tabsConfig;
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
defaultValue: ['live', 'rcmd', 'hot', 'bangumi']);
}
void saveEdit() {
List<String> sortedTabbar = defaultTabs
.where((i) => tabbarSort.contains((i['type'] as TabType).id))
.map<String>((i) => (i['type'] as TabType).id)
.toList();
if (sortedTabbar.isEmpty) {
SmartDialog.showToast('请至少设置一项!');
return;
}
settingStorage.put(SettingBoxKey.tabbarSort, sortedTabbar);
SmartDialog.showToast('保存成功,下次启动时生效');
}
void onReorder(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final tabsItem = defaultTabs.removeAt(oldIndex);
defaultTabs.insert(newIndex, tabsItem);
});
}
@override
Widget build(BuildContext context) {
final listTiles = [
for (int i = 0; i < defaultTabs.length; i++) ...[
CheckboxListTile(
key: Key(defaultTabs[i]['label']),
value: tabbarSort.contains((defaultTabs[i]['type'] as TabType).id),
onChanged: (bool? newValue) {
String tabTypeId = (defaultTabs[i]['type'] as TabType).id;
if (!newValue!) {
tabbarSort.remove(tabTypeId);
} else {
tabbarSort.add(tabTypeId);
}
setState(() {});
},
title: Text(defaultTabs[i]['label']),
secondary: const Icon(Icons.drag_indicator_rounded),
)
]
];
return Scaffold(
appBar: AppBar(
title: const Text('Tabbar编辑'),
actions: [
TextButton(onPressed: () => saveEdit(), child: const Text('保存')),
const SizedBox(width: 12)
],
),
body: ReorderableListView(
onReorder: onReorder,
physics: const NeverScrollableScrollPhysics(),
footer: SizedBox(
height: MediaQuery.of(context).padding.bottom + 30,
),
children: listTiles,
),
);
}
}

View File

@ -254,6 +254,11 @@ class _StyleSettingState extends State<StyleSetting> {
onTap: () => Get.toNamed('/fontSizeSetting'),
title: Text('字体大小', style: titleStyle),
),
ListTile(
dense: false,
onTap: () => Get.toNamed('/tabbarSetting'),
title: Text('首页tabbar', style: titleStyle),
),
if (Platform.isAndroid)
ListTile(
dense: false,

View File

@ -9,7 +9,8 @@ import './controller.dart';
class RelatedVideoPanel extends StatelessWidget {
final ReleatedController _releatedController =
Get.put(ReleatedController(), tag: Get.arguments['heroTag']);
Get.put(ReleatedController(), tag: Get.arguments?['heroTag']);
RelatedVideoPanel({super.key});
@override
Widget build(BuildContext context) {

View File

@ -849,6 +849,13 @@ InlineSpan buildContent(
WidgetSpan(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints box) {
double maxHeight = box.maxWidth * 0.6; // 设置最大高度
// double width = (box.maxWidth / 2).truncateToDouble();
double height = ((box.maxWidth /
2 *
pictureItem['img_height'] /
pictureItem['img_width']))
.truncateToDouble();
return GestureDetector(
onTap: () {
showDialog(
@ -859,15 +866,28 @@ InlineSpan buildContent(
},
);
},
child: Padding(
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
width: box.maxWidth / 2,
height: height,
child: Stack(
children: [
Positioned.fill(
child: NetworkImgLayer(
src: pictureItem['img_src'],
width: box.maxWidth / 2,
height: box.maxWidth *
0.5 *
pictureItem['img_height'] /
pictureItem['img_width'],
height: height,
),
),
height > maxHeight
? const PBadge(
text: '长图',
right: 8,
bottom: 8,
)
: const SizedBox(),
],
),
),
);

View File

@ -2,12 +2,10 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class VideoReplyNewDialog extends StatefulWidget {
final int? oid;
@ -34,25 +32,16 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
final TextEditingController _replyContentController = TextEditingController();
final FocusNode replyContentFocusNode = FocusNode();
final GlobalKey _formKey = GlobalKey<FormState>();
double _keyboardHeight = 0.0; // 键盘高度
final _debouncer = Debouncer(milliseconds: 100); // 设置延迟时间
bool ableClean = false;
Timer? timer;
Box localCache = GStrorage.localCache;
late double sheetHeight;
@override
void initState() {
super.initState();
// 监听输入框聚焦
// replyContentFocusNode.addListener(_onFocus);
_replyContentController.addListener(_printLatestValue);
// 界面观察者 必须
WidgetsBinding.instance.addObserver(this);
// 自动聚焦
_autoFocus();
sheetHeight = localCache.get('sheetHeight');
}
_autoFocus() async {
@ -62,12 +51,6 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
}
}
_printLatestValue() {
setState(() {
ableClean = _replyContentController.text != '';
});
}
Future submitReplyAdd() async {
feedBack();
String message = _replyContentController.text;
@ -90,24 +73,6 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
}
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
WidgetsBinding.instance.addPostFrameCallback((_) {
// 键盘高度
final viewInsets = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio);
_debouncer.run(() {
if (mounted) {
setState(() {
_keyboardHeight =
_keyboardHeight == 0.0 ? viewInsets.bottom : _keyboardHeight;
});
}
});
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
@ -117,8 +82,10 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
@override
Widget build(BuildContext context) {
double keyboardHeight = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio)
.bottom;
return Container(
height: 500,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
@ -130,10 +97,15 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 200,
minHeight: 120,
),
child: Container(
padding: const EdgeInsets.only(
top: 6, right: 15, left: 15, bottom: 10),
top: 12, right: 15, left: 15, bottom: 10),
child: SingleChildScrollView(
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
@ -154,6 +126,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
),
),
),
),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
@ -183,7 +156,8 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
MaterialStateProperty.resolveWith((states) {
return Theme.of(context).highlightColor;
}),
)),
),
),
),
const Spacer(),
TextButton(
@ -196,7 +170,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
duration: const Duration(milliseconds: 300),
child: SizedBox(
width: double.infinity,
height: _keyboardHeight,
height: keyboardHeight,
),
),
],
@ -204,22 +178,3 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
);
}
}
typedef DebounceCallback = void Function();
class Debouncer {
DebounceCallback? callback;
final int? milliseconds;
Timer? _timer;
Debouncer({this.milliseconds});
run(DebounceCallback callback) {
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(Duration(milliseconds: milliseconds!), () {
callback();
});
}
}

View File

@ -122,6 +122,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.triggerFullScreen(status: false);
}
shutdownTimerService.handleWaitingFinished();
/// 顺序播放 列表循环
if (plPlayerController!.playRepeat != PlayRepeat.pause &&
plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
@ -234,7 +235,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoIntroController.isPaused = false;
if (_extendNestCtr.position.pixels == 0 && autoplay) {
await Future.delayed(const Duration(milliseconds: 300));
plPlayerController!.seekTo(videoDetailController.defaultST);
plPlayerController?.seekTo(videoDetailController.defaultST);
plPlayerController?.play();
}
plPlayerController?.addStatusLister(playerListener);
@ -308,7 +309,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
body: ExtendedNestedScrollView(
controller: _extendNestCtr,
headerSliverBuilder:
(BuildContext _context, bool innerBoxIsScrolled) {
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
Obx(
() => SliverAppBar(
@ -418,7 +419,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
),
),
Obx(
() => Visibility(
visible: videoDetailController

View File

@ -621,7 +621,7 @@ class PlPlayerController {
if (duration.value.inSeconds != 0) {
if (type != 'slider') {
/// 拖动进度条调节时,不等待第一帧,防止抖动
await _videoPlayerController!.stream.buffer.first;
await _videoPlayerController?.stream.buffer.first;
}
await _videoPlayerController?.seek(position);
// if (playerStatus.stopped) {
@ -786,7 +786,7 @@ class PlPlayerController {
volume.value = volumeNew;
try {
FlutterVolumeController.showSystemUI = false;
FlutterVolumeController.updateShowSystemUI(false);
await FlutterVolumeController.setVolume(volumeNew);
} catch (err) {
print(err);
@ -1086,12 +1086,13 @@ class PlPlayerController {
localCache.put(LocalCacheKey.danmakuOpacity, opacityVal);
localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);
localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal);
if (_videoPlayerController != null) {
var pp = _videoPlayerController!.platform as NativePlayer;
await pp.setProperty('audio-files', '');
removeListeners();
await _videoPlayerController?.dispose();
_videoPlayerController = null;
}
_instance = null;
// 关闭所有视频页面恢复亮度
resetBrightness();

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@ -130,7 +129,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false);
Future.microtask(() async {
try {
FlutterVolumeController.showSystemUI = true;
FlutterVolumeController.updateShowSystemUI(true);
_ctr.volumeValue.value = (await FlutterVolumeController.getVolume())!;
FlutterVolumeController.addListener((double value) {
if (mounted && !_ctr.volumeInterceptEventStream.value) {
@ -154,7 +153,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Future<void> setVolume(double value) async {
try {
FlutterVolumeController.showSystemUI = false;
FlutterVolumeController.updateShowSystemUI(false);
await FlutterVolumeController.setVolume(value);
} catch (_) {}
_ctr.volumeValue.value = value;
@ -703,7 +702,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
);
} else {
return nil;
return const SizedBox();
}
}),

View File

@ -36,6 +36,7 @@ import '../pages/setting/index.dart';
import '../pages/setting/pages/color_select.dart';
import '../pages/setting/pages/display_mode.dart';
import '../pages/setting/pages/font_size_select.dart';
import '../pages/setting/pages/home_tabbar_set.dart';
import '../pages/setting/pages/play_speed_set.dart';
import '../pages/setting/recommend_setting.dart';
import '../pages/setting/play_setting.dart';
@ -114,6 +115,8 @@ class Routes {
//
CustomGetPage(name: '/blackListPage', page: () => const BlackListPage()),
CustomGetPage(name: '/colorSetting', page: () => const ColorSelectPage()),
// 首页tabbar
CustomGetPage(name: '/tabbarSetting', page: () => const TabbarSetPage()),
CustomGetPage(
name: '/fontSizeSetting', page: () => const FontSizeSelectPage()),
// 屏幕帧率

7
lib/utils/extension.dart Normal file
View File

@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
extension ImageExtension on num {
int cacheSize(BuildContext context) {
return (this * MediaQuery.of(context).devicePixelRatio).round();
}
}

View File

@ -1,5 +1,6 @@
// ignore_for_file: constant_identifier_names
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
@ -72,4 +73,19 @@ class IdUtils {
}
return result;
}
// eid生成
static String? genAuroraEid(int uid) {
if (uid == 0) {
return null;
}
String uidString = uid.toString();
List<int> resultBytes = List.generate(
uidString.length,
(i) => uidString.codeUnitAt(i) ^ "ad1va46a7lza".codeUnitAt(i % 12),
);
String auroraEid = base64Url.encode(resultBytes);
auroraEid = auroraEid.replaceAll(RegExp(r'=*$', multiLine: true), '');
return auroraEid;
}
}

View File

@ -3,13 +3,11 @@ import 'dart:io';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pilipala/models/home/rcmd/result.dart';
import 'package:pilipala/models/model_owner.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/user/info.dart';
class GStrorage {
static late final Box<dynamic> recVideo;
static late final Box<dynamic> userInfo;
static late final Box<dynamic> historyword;
static late final Box<dynamic> localCache;
@ -21,13 +19,6 @@ class GStrorage {
final String path = dir.path;
await Hive.initFlutter('$path/hive');
regAdapter();
// 首页推荐视频
recVideo = await Hive.openBox(
'recVideo',
compactionStrategy: (int entries, int deletedEntries) {
return deletedEntries > 12;
},
);
// 登录用户信息
userInfo = await Hive.openBox(
'userInfo',
@ -54,10 +45,6 @@ class GStrorage {
}
static void regAdapter() {
Hive.registerAdapter(RecVideoItemAppModelAdapter());
Hive.registerAdapter(RcmdReasonAdapter());
Hive.registerAdapter(RcmdStatAdapter());
Hive.registerAdapter(RcmdOwnerAdapter());
Hive.registerAdapter(OwnerAdapter());
Hive.registerAdapter(UserInfoDataAdapter());
Hive.registerAdapter(LevelInfoAdapter());
@ -73,8 +60,6 @@ class GStrorage {
static Future<void> close() async {
// user.compact();
// user.close();
recVideo.compact();
recVideo.close();
userInfo.compact();
userInfo.close();
historyword.compact();
@ -151,7 +136,8 @@ class SettingBoxKey {
customRows = 'customRows', // 自定义列
enableMYBar = 'enableMYBar',
hideSearchBar = 'hideSearchBar', // 收起顶栏
hideTabBar = 'hideTabBar'; // 收起底栏
hideTabBar = 'hideTabBar', // 收起底栏
tabbarSort = 'tabbarSort'; // 首页tabbar
}
class LocalCacheKey {

View File

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