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

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(
padding: const EdgeInsets.only(top: 4),
child: NetworkImgLayer(
src: pictureItem.url,
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
width: box.maxWidth / 2,
height: box.maxWidth *
0.5 *
(pictureItem.height != null && pictureItem.width != null
? pictureItem.height! / pictureItem.width!
: 1),
),
),
height: height,
child: Stack(
children: [
Positioned.fill(
child: NetworkImgLayer(
src: pictureItem.url,
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,
),
const CustomTabs(),
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,34 +266,30 @@ 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(
padding: const EdgeInsets.symmetric(horizontal: 14.0),
scrollDirection: Axis.horizontal,
itemCount: _homeController.tabs.length,
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(width: 10);
},
itemBuilder: (BuildContext context, int index) {
String label = _homeController.tabs[index]['label'];
return Obx(
() => CustomChip(
onTap: () => onTap(index),
label: label,
selected: index == _homeController.initialIndex.value,
),
);
},
child: Obx(
() => ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 14.0),
scrollDirection: Axis.horizontal,
itemCount: _homeController.tabs.length,
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(width: 10);
},
itemBuilder: (BuildContext context, int index) {
String label = _homeController.tabs[index]['label'];
return Obx(
() => CustomChip(
onTap: () => onTap(index),
label: label,
selected: index == _homeController.initialIndex.value,
),
);
},
),
),
);
}
@ -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,14 +371,12 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSecondaryContainer,
),
const SizedBox(width: 10),
Expanded(
child: Obx(
() => Text(
searchController.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: colorScheme.outline),
),
Obx(
() => Text(
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();
if (selectedIndex != 0) {
pageController.jumpTo(0);
}
SmartDialog.showToast("再按一次退出Pili");
return Future.value(false); // 不退出应用
return; // 不退出应用
}
return Future.value(true); // 退出应用
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,36 +119,48 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1),
child: enableMYBar
? NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: e['icon'],
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
)
: BottomNavigationBar(
currentIndex: selectedIndex,
onTap: (value) => setIndex(value),
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: e['icon'],
activeIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
child: Obx(
() => enableMYBar
? NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: _mainController.selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
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'],
);
}).toList(),
],
)
: BottomNavigationBar(
currentIndex: _mainController.selectedIndex,
onTap: (value) => setIndex(value),
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
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

@ -26,10 +26,9 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(builder: (context, boxConstraints) {
double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.of(context).textScaleFactor) /
2;
StyleString.cardSpace *
6 /
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),
child: NetworkImgLayer(
src: pictureItem['img_src'],
width: box.maxWidth / 2,
height: box.maxWidth *
0.5 *
pictureItem['img_height'] /
pictureItem['img_width'],
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: 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,26 +97,32 @@ 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),
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: TextField(
controller: _replyContentController,
minLines: 1,
maxLines: null,
autofocus: false,
focusNode: replyContentFocusNode,
decoration: const InputDecoration(
hintText: "输入回复内容",
border: InputBorder.none,
hintStyle: TextStyle(
fontSize: 14,
)),
style: Theme.of(context).textTheme.bodyLarge,
top: 12, right: 15, left: 15, bottom: 10),
child: SingleChildScrollView(
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: TextField(
controller: _replyContentController,
minLines: 1,
maxLines: null,
autofocus: false,
focusNode: replyContentFocusNode,
decoration: const InputDecoration(
hintText: "输入回复内容",
border: InputBorder.none,
hintStyle: TextStyle(
fontSize: 14,
)),
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
),
@ -168,22 +141,23 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
width: 36,
height: 36,
child: IconButton(
onPressed: () {
FocusScope.of(context)
.requestFocus(replyContentFocusNode);
},
icon: Icon(Icons.keyboard,
size: 22,
color: Theme.of(context).colorScheme.onBackground),
highlightColor:
Theme.of(context).colorScheme.onInverseSurface,
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith((states) {
return Theme.of(context).highlightColor;
}),
)),
onPressed: () {
FocusScope.of(context)
.requestFocus(replyContentFocusNode);
},
icon: Icon(Icons.keyboard,
size: 22,
color: Theme.of(context).colorScheme.onBackground),
highlightColor:
Theme.of(context).colorScheme.onInverseSurface,
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
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