Merge branch 'main' into feature-updateVideoDetailStructure
This commit is contained in:
@ -20,10 +20,10 @@ class IntroDetail extends StatelessWidget {
|
||||
sheetHeight = localCache.get('sheetHeight');
|
||||
TextStyle smallTitle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
height: sheetHeight,
|
||||
child: Column(
|
||||
|
||||
@ -3,7 +3,6 @@ import 'dart:async';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nil/nil.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/utils/main_stream.dart';
|
||||
@ -142,10 +141,10 @@ class _BangumiPageState extends State<BangumiPage>
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return nil;
|
||||
return const SizedBox();
|
||||
}
|
||||
} else {
|
||||
return nil;
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -216,7 +215,7 @@ class _BangumiPageState extends State<BangumiPage>
|
||||
(BuildContext context, int index) {
|
||||
return bangumiList!.isNotEmpty
|
||||
? BangumiCardV(bangumiItem: bangumiList[index])
|
||||
: nil;
|
||||
: const SizedBox();
|
||||
},
|
||||
childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10,
|
||||
),
|
||||
|
||||
@ -162,13 +162,12 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.surfaceContainerHighest
|
||||
.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
thumbDecoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
|
||||
@ -19,7 +19,7 @@ Widget addWidget(item, context, type, {floor = 1}) {
|
||||
};
|
||||
Color bgColor = floor == 1
|
||||
? Theme.of(context).dividerColor.withOpacity(0.08)
|
||||
: Theme.of(context).colorScheme.background;
|
||||
: Theme.of(context).colorScheme.surface;
|
||||
switch (type) {
|
||||
case 'ADDITIONAL_TYPE_UGC':
|
||||
// 转发的投稿
|
||||
|
||||
@ -52,7 +52,7 @@ class AuthorPanel extends StatelessWidget {
|
||||
color: item.modules.moduleAuthor!.vip != null &&
|
||||
item.modules.moduleAuthor!.vip['status'] > 0
|
||||
? const Color.fromARGB(255, 251, 100, 163)
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
@ -30,6 +31,31 @@ class _UpPanelState extends State<UpPanel> {
|
||||
liveList = widget.upData.liveList!;
|
||||
}
|
||||
|
||||
void onClickUp(data, i) {
|
||||
currentMid = data.mid;
|
||||
Get.find<DynamicsController>().mid.value = data.mid;
|
||||
Get.find<DynamicsController>().upInfo.value = data;
|
||||
Get.find<DynamicsController>().onSelectUp(data.mid);
|
||||
int liveLen = liveList.length;
|
||||
int upLen = upList.length;
|
||||
double itemWidth = contentWidth + itemPadding.horizontal;
|
||||
double screenWidth = MediaQuery.sizeOf(context).width;
|
||||
double moveDistance = 0.0;
|
||||
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
|
||||
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
|
||||
moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
|
||||
} else {
|
||||
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
|
||||
}
|
||||
data.hasUpdate = false;
|
||||
scrollController.animateTo(
|
||||
moveDistance,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.linear,
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
listFormat();
|
||||
@ -43,7 +69,7 @@ class _UpPanelState extends State<UpPanel> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -69,7 +95,7 @@ class _UpPanelState extends State<UpPanel> {
|
||||
),
|
||||
Container(
|
||||
height: 90,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
@ -120,30 +146,10 @@ class _UpPanelState extends State<UpPanel> {
|
||||
onTap: () {
|
||||
feedBack();
|
||||
if (data.type == 'up') {
|
||||
currentMid = data.mid;
|
||||
Get.find<DynamicsController>().mid.value = data.mid;
|
||||
Get.find<DynamicsController>().upInfo.value = data;
|
||||
Get.find<DynamicsController>().onSelectUp(data.mid);
|
||||
int liveLen = liveList.length;
|
||||
int upLen = upList.length;
|
||||
double itemWidth = contentWidth + itemPadding.horizontal;
|
||||
double screenWidth = MediaQuery.sizeOf(context).width;
|
||||
double moveDistance = 0.0;
|
||||
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
|
||||
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
|
||||
moveDistance =
|
||||
(i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
|
||||
} else {
|
||||
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
|
||||
}
|
||||
data.hasUpdate = false;
|
||||
scrollController.animateTo(
|
||||
moveDistance,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
setState(() {});
|
||||
EasyThrottle.throttle('follow', const Duration(milliseconds: 300),
|
||||
() {
|
||||
onClickUp(data, i);
|
||||
});
|
||||
} else if (data.type == 'live') {
|
||||
LiveItemModel liveItem = LiveItemModel.fromJson({
|
||||
'title': data.title,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/pages/fav/index.dart';
|
||||
import 'package:pilipala/pages/fav/widgets/item.dart';
|
||||
@ -93,7 +94,12 @@ class _FavPageState extends State<FavPage> {
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const Text('请求中');
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
itemCount: 10,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@ -244,7 +244,7 @@ class HistoryItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
videoItem.progress != 0
|
||||
videoItem.progress != 0 && videoItem.duration != 0
|
||||
? Positioned(
|
||||
left: 3,
|
||||
right: 3,
|
||||
|
||||
@ -10,9 +10,8 @@ class HistorySearchController extends GetxController {
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
RxString searchKeyWord = ''.obs;
|
||||
String hintText = '搜索';
|
||||
RxString loadingStatus = 'init'.obs;
|
||||
RxBool loadingStatus = false.obs;
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
bool hasRequest = false;
|
||||
late int mid;
|
||||
RxString uname = ''.obs;
|
||||
int pn = 1;
|
||||
@ -36,8 +35,7 @@ class HistorySearchController extends GetxController {
|
||||
|
||||
// 提交搜索内容
|
||||
void submit() {
|
||||
loadingStatus.value = 'loading';
|
||||
if (hasRequest) {
|
||||
if (!loadingStatus.value) {
|
||||
pn = 1;
|
||||
searchHistories();
|
||||
}
|
||||
@ -48,6 +46,7 @@ class HistorySearchController extends GetxController {
|
||||
if (type == 'onLoad' && loadingText.value == '没有更多了') {
|
||||
return;
|
||||
}
|
||||
loadingStatus.value = true;
|
||||
var res = await UserHttp.searchHistory(
|
||||
pn: pn,
|
||||
keyword: controller.value.text,
|
||||
@ -63,9 +62,8 @@ class HistorySearchController extends GetxController {
|
||||
loadingText.value = '没有更多了';
|
||||
}
|
||||
pn += 1;
|
||||
hasRequest = true;
|
||||
}
|
||||
loadingStatus.value = 'finish';
|
||||
loadingStatus.value = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -86,6 +84,6 @@ class HistorySearchController extends GetxController {
|
||||
historyList.removeWhere((e) => e.kid == kid);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
loadingStatus.value = 'finish';
|
||||
// loadingStatus.value = fasle;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/pages/history/widgets/item.dart';
|
||||
|
||||
@ -16,20 +15,19 @@ class HistorySearchPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HistorySearchPageState extends State<HistorySearchPage> {
|
||||
final HistorySearchController _historySearchCtr =
|
||||
Get.put(HistorySearchController());
|
||||
final HistorySearchController _hisCtr = Get.put(HistorySearchController());
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController = _historySearchCtr.scrollController;
|
||||
scrollController = _hisCtr.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
|
||||
_historySearchCtr.onLoad();
|
||||
_hisCtr.onLoad();
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -50,19 +48,19 @@ class _HistorySearchPageState extends State<HistorySearchPage> {
|
||||
titleSpacing: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _historySearchCtr.submit(),
|
||||
onPressed: () => _hisCtr.submit(),
|
||||
icon: const Icon(Icons.search_outlined, size: 22)),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
title: Obx(
|
||||
() => TextField(
|
||||
autofocus: true,
|
||||
focusNode: _historySearchCtr.searchFocusNode,
|
||||
controller: _historySearchCtr.controller.value,
|
||||
focusNode: _hisCtr.searchFocusNode,
|
||||
controller: _hisCtr.controller.value,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (value) => _historySearchCtr.onChange(value),
|
||||
onChanged: (value) => _hisCtr.onChange(value),
|
||||
decoration: InputDecoration(
|
||||
hintText: _historySearchCtr.hintText,
|
||||
hintText: _hisCtr.hintText,
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
@ -70,103 +68,61 @@ class _HistorySearchPageState extends State<HistorySearchPage> {
|
||||
size: 22,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
onPressed: () => _historySearchCtr.onClear(),
|
||||
onPressed: () => _hisCtr.onClear(),
|
||||
),
|
||||
),
|
||||
onSubmitted: (String value) => _historySearchCtr.submit(),
|
||||
onSubmitted: (String value) => _hisCtr.submit(),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Obx(
|
||||
() => Column(
|
||||
children: _historySearchCtr.loadingStatus.value == 'init'
|
||||
? [const SizedBox()]
|
||||
: [
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: _historySearchCtr.searchHistories(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => _historySearchCtr.historyList.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount:
|
||||
_historySearchCtr.historyList.length +
|
||||
1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index ==
|
||||
_historySearchCtr
|
||||
.historyList.length) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_historySearchCtr
|
||||
.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return HistoryItem(
|
||||
videoItem: _historySearchCtr
|
||||
.historyList[index],
|
||||
ctr: _historySearchCtr,
|
||||
onChoose: null,
|
||||
onUpdateMultiple: () => null,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: _historySearchCtr.loadingStatus.value ==
|
||||
'loading'
|
||||
? const SizedBox(child: Text('加载中...'))
|
||||
: const CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
NoData(),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
() {
|
||||
return _hisCtr.loadingStatus.value && _hisCtr.historyList.isEmpty
|
||||
? ListView.builder(
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
)
|
||||
: _hisCtr.historyList.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: _hisCtr.historyList.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _hisCtr.historyList.length) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).padding.bottom + 60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_hisCtr.loadingText.value,
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.outline,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 骨架屏
|
||||
return ListView.builder(
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
return HistoryItem(
|
||||
videoItem: _hisCtr.historyList[index],
|
||||
ctr: _hisCtr,
|
||||
onChoose: null,
|
||||
onUpdateMultiple: () => null,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
NoData(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/login.dart';
|
||||
import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
|
||||
import 'package:pilipala/models/login/index.dart';
|
||||
import 'package:pilipala/utils/login.dart';
|
||||
|
||||
class LoginPageController extends GetxController {
|
||||
final GlobalKey mobFormKey = GlobalKey<FormState>();
|
||||
@ -26,9 +29,23 @@ class LoginPageController extends GetxController {
|
||||
|
||||
final Gt3FlutterPlugin captcha = Gt3FlutterPlugin();
|
||||
|
||||
// 倒计时60s
|
||||
RxInt seconds = 60.obs;
|
||||
late Timer timer;
|
||||
RxBool smsCodeSendStatus = false.obs;
|
||||
|
||||
// 默认密码登录
|
||||
RxInt loginType = 0.obs;
|
||||
|
||||
late String captchaKey;
|
||||
|
||||
late int tel;
|
||||
late int webSmsCode;
|
||||
|
||||
RxInt validSeconds = 180.obs;
|
||||
late Timer validTimer;
|
||||
late String qrcodeKey;
|
||||
|
||||
// 监听pageView切换
|
||||
void onPageChange(int index) {
|
||||
currentIndex.value = index;
|
||||
@ -43,6 +60,7 @@ class LoginPageController extends GetxController {
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
passwordTextFieldNode.requestFocus();
|
||||
(mobFormKey.currentState as FormState).save();
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,18 +104,64 @@ class LoginPageController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证码登录
|
||||
void loginInByCode() {
|
||||
if ((msgCodeFormKey.currentState as FormState).validate()) {}
|
||||
// web端密码登录
|
||||
void loginInByWebPassword() async {
|
||||
if ((passwordFormKey.currentState as FormState).validate()) {
|
||||
getCaptcha((data) async {
|
||||
CaptchaDataModel captchaData = data;
|
||||
var webKeyRes = await LoginHttp.getWebKey();
|
||||
if (webKeyRes['status']) {
|
||||
String rhash = webKeyRes['data']['hash'];
|
||||
String key = webKeyRes['data']['key'];
|
||||
dynamic publicKey = RSAKeyParser().parse(key);
|
||||
String passwordEncryptyed = Encrypter(RSA(publicKey: publicKey))
|
||||
.encrypt(rhash + passwordTextController.text)
|
||||
.base64;
|
||||
var res = await LoginHttp.loginInByWebPwd(
|
||||
username: tel,
|
||||
password: passwordEncryptyed,
|
||||
token: captchaData.token!,
|
||||
challenge: captchaData.geetest!.challenge!,
|
||||
validate: captchaData.validate!,
|
||||
seccode: captchaData.seccode!,
|
||||
);
|
||||
if (res['status']) {
|
||||
await LoginUtils.confirmLogin('', null);
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast(webKeyRes['msg']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// app端验证码
|
||||
void getMsgCode() async {
|
||||
// web端验证码登录
|
||||
void loginInByCode() async {
|
||||
if ((msgCodeFormKey.currentState as FormState).validate()) {
|
||||
(msgCodeFormKey.currentState as FormState).save();
|
||||
var res = await LoginHttp.loginInByWebSmsCode(
|
||||
cid: 86,
|
||||
tel: tel,
|
||||
code: webSmsCode,
|
||||
captchaKey: captchaKey,
|
||||
);
|
||||
if (res['status']) {
|
||||
await LoginUtils.confirmLogin('', null);
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取app端验证码
|
||||
void getAppMsgCode() async {
|
||||
getCaptcha((data) async {
|
||||
CaptchaDataModel captchaData = data;
|
||||
var res = await LoginHttp.sendAppSmsCode(
|
||||
cid: 86,
|
||||
tel: 13734077064,
|
||||
tel: tel,
|
||||
token: captchaData.token!,
|
||||
challenge: captchaData.geetest!.challenge!,
|
||||
validate: captchaData.validate!,
|
||||
@ -121,7 +185,7 @@ class LoginPageController extends GetxController {
|
||||
captcha.addEventHandler(onShow: (Map<String, dynamic> message) async {
|
||||
SmartDialog.dismiss();
|
||||
}, onClose: (Map<String, dynamic> message) async {
|
||||
SmartDialog.showToast('关闭验证');
|
||||
SmartDialog.showToast('取消验证');
|
||||
}, onResult: (Map<String, dynamic> message) async {
|
||||
debugPrint("Captcha result: $message");
|
||||
String code = message["code"];
|
||||
@ -201,4 +265,72 @@ class LoginPageController extends GetxController {
|
||||
captcha.startCaptcha(registerData);
|
||||
} else {}
|
||||
}
|
||||
|
||||
// 获取web端验证码
|
||||
void getWebMsgCode() async {
|
||||
getCaptcha((data) async {
|
||||
CaptchaDataModel captchaData = data;
|
||||
var res = await LoginHttp.sendWebSmsCode(
|
||||
cid: 86,
|
||||
tel: tel,
|
||||
token: captchaData.token!,
|
||||
challenge: captchaData.geetest!.challenge!,
|
||||
validate: captchaData.validate!,
|
||||
seccode: captchaData.seccode!,
|
||||
);
|
||||
if (res['status']) {
|
||||
captchaKey = res['data']['captcha_key'];
|
||||
SmartDialog.showToast('验证码已发送');
|
||||
// 倒计时60s
|
||||
smsCodeSendStatus.value = true;
|
||||
startTimer();
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 验证码倒计时
|
||||
void startTimer() {
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (seconds.value > 0) {
|
||||
seconds.value--;
|
||||
} else {
|
||||
seconds.value = 60;
|
||||
smsCodeSendStatus.value = false;
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取登录二维码
|
||||
Future getWebQrcode() async {
|
||||
var res = await LoginHttp.getWebQrcode();
|
||||
validSeconds.value = 180;
|
||||
if (res['status']) {
|
||||
qrcodeKey = res['data']['qrcode_key'];
|
||||
validTimer = Timer.periodic(const Duration(seconds: 1), (validTimer) {
|
||||
if (validSeconds.value > 0) {
|
||||
validSeconds.value--;
|
||||
queryWebQrcodeStatus();
|
||||
} else {
|
||||
getWebQrcode();
|
||||
validTimer.cancel();
|
||||
}
|
||||
});
|
||||
return res;
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询二维码登录状态
|
||||
Future queryWebQrcodeStatus() async {
|
||||
var res = await LoginHttp.queryWebQrcodeStatus(qrcodeKey);
|
||||
if (res['status']) {
|
||||
await LoginUtils.confirmLogin('', null);
|
||||
validTimer.cancel();
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
@ -37,6 +38,105 @@ class _LoginPageState extends State<LoginPage> {
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '浏览器打开',
|
||||
onPressed: () {
|
||||
Get.offNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': 'https://passport.bilibili.com/h5-app/passport/login',
|
||||
'type': 'login',
|
||||
'pageTitle': '登录bilibili',
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.language),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '二维码登录',
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, StateSetter setState) {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('扫码登录'),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
|
||||
content: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
width: 200,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: FutureBuilder(
|
||||
future: _loginPageCtr.getWebQrcode(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
Map data = snapshot.data as Map;
|
||||
return QrImageView(
|
||||
data: data['data']['url'],
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Obx(() {
|
||||
return Text(
|
||||
'有效期: ${_loginPageCtr.validSeconds.value}s',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
);
|
||||
}),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
'检查登录状态',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.fontSize,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.qr_code),
|
||||
),
|
||||
const SizedBox(width: 22),
|
||||
],
|
||||
),
|
||||
body: PageView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@ -93,35 +193,12 @@ class _LoginPageState extends State<LoginPage> {
|
||||
validator: (v) {
|
||||
return v!.trim().isNotEmpty ? null : "手机号码不能为空";
|
||||
},
|
||||
onSaved: (val) {
|
||||
print(val);
|
||||
},
|
||||
onSaved: (val) => _loginPageCtr.tel = int.parse(val!),
|
||||
onEditingComplete: () {
|
||||
_loginPageCtr.nextStep();
|
||||
},
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Get.offNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url':
|
||||
'https://passport.bilibili.com/h5-app/passport/login',
|
||||
'type': 'login',
|
||||
'pageTitle': '登录bilibili',
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2),
|
||||
child: Text(
|
||||
'使用网页端登录',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -236,7 +313,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
.primary, // 设置按钮背景色
|
||||
),
|
||||
onPressed: () =>
|
||||
_loginPageCtr.loginInByAppPassword(),
|
||||
_loginPageCtr.loginInByWebPassword(),
|
||||
child: const Text('确认登录'),
|
||||
)
|
||||
],
|
||||
@ -308,21 +385,28 @@ class _LoginPageState extends State<LoginPage> {
|
||||
? null
|
||||
: "验证码不能为空";
|
||||
},
|
||||
onSaved: (val) {
|
||||
print(val);
|
||||
},
|
||||
onSaved: (val) => _loginPageCtr.webSmsCode =
|
||||
int.parse(val!),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 4,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
_loginPageCtr.getMsgCode(),
|
||||
child: const Text('获取验证码'),
|
||||
Obx(() {
|
||||
return Positioned(
|
||||
right: 8,
|
||||
top: 0,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
onPressed: _loginPageCtr
|
||||
.smsCodeSendStatus.value
|
||||
? null
|
||||
: () =>
|
||||
_loginPageCtr.getWebMsgCode(),
|
||||
child: _loginPageCtr
|
||||
.smsCodeSendStatus.value
|
||||
? Text(
|
||||
'重新获取(${_loginPageCtr.seconds.value}s)')
|
||||
: const Text('获取验证码')),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -105,7 +105,7 @@ class _MediaPageState extends State<MediaPage>
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
onTap: () => Get.toNamed('/fav'),
|
||||
leading: null,
|
||||
dense: true,
|
||||
title: Padding(
|
||||
|
||||
@ -6,7 +6,6 @@ import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/common/theme_type.dart';
|
||||
import 'package:pilipala/models/user/info.dart';
|
||||
import 'package:pilipala/models/user/stat.dart';
|
||||
import 'package:pilipala/utils/route_push.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class MineController extends GetxController {
|
||||
@ -34,8 +33,8 @@ class MineController extends GetxController {
|
||||
|
||||
onLogin() async {
|
||||
if (!userLogin.value) {
|
||||
RoutePush.loginPush();
|
||||
// Get.toNamed('/loginPage');
|
||||
// RoutePush.loginPush();
|
||||
Get.toNamed('/loginPage');
|
||||
} else {
|
||||
int mid = userInfo.value.mid!;
|
||||
String face = userInfo.value.face!;
|
||||
|
||||
@ -5,18 +5,25 @@ class SearchText extends StatelessWidget {
|
||||
final Function? onSelect;
|
||||
final int? searchTextIdx;
|
||||
final Function? onLongSelect;
|
||||
final bool isSelect;
|
||||
const SearchText({
|
||||
super.key,
|
||||
this.searchText,
|
||||
this.onSelect,
|
||||
this.searchTextIdx,
|
||||
this.onLongSelect,
|
||||
this.isSelect = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
color: isSelect
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHighest
|
||||
.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.zero,
|
||||
@ -34,7 +41,10 @@ class SearchText extends StatelessWidget {
|
||||
child: Text(
|
||||
searchText!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
color: isSelect
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -16,14 +16,18 @@ class SearchPanelController extends GetxController {
|
||||
RxString order = ''.obs;
|
||||
// 视频时长筛选 仅用于搜索视频
|
||||
RxInt duration = 0.obs;
|
||||
// 视频分区筛选 仅用于搜索视频 -1时不传
|
||||
RxInt tids = (-1).obs;
|
||||
|
||||
Future onSearch({type = 'init'}) async {
|
||||
var result = await SearchHttp.searchByType(
|
||||
searchType: searchType!,
|
||||
keyword: keyword!,
|
||||
page: page.value,
|
||||
order: searchType!.type != 'video' ? null : order.value,
|
||||
duration: searchType!.type != 'video' ? null : duration.value);
|
||||
searchType: searchType!,
|
||||
keyword: keyword!,
|
||||
page: page.value,
|
||||
order: searchType!.type != 'video' ? null : order.value,
|
||||
duration: searchType!.type != 'video' ? null : duration.value,
|
||||
tids: searchType!.type != 'video' ? null : tids.value,
|
||||
);
|
||||
if (result['status']) {
|
||||
if (type == 'onRefresh') {
|
||||
resultList.value = result['data'].list;
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/video_card_h.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
import 'package:pilipala/pages/search/widgets/search_text.dart';
|
||||
import 'package:pilipala/pages/search_panel/index.dart';
|
||||
|
||||
class SearchVideoPanel extends StatelessWidget {
|
||||
@ -94,7 +95,7 @@ class SearchVideoPanel extends StatelessWidget {
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () => controller.onShowFilterDialog(ctr),
|
||||
onPressed: () => controller.onShowFilterSheet(ctr),
|
||||
icon: Icon(
|
||||
Icons.filter_list_outlined,
|
||||
size: 18,
|
||||
@ -165,7 +166,33 @@ class VideoPanelController extends GetxController {
|
||||
{'label': '30-60分钟', 'value': 3},
|
||||
{'label': '60分钟+', 'value': 4},
|
||||
];
|
||||
List<Map<String, dynamic>> partFiltersList = [
|
||||
{'label': '全部', 'value': -1},
|
||||
{'label': '动画', 'value': 1},
|
||||
{'label': '番剧', 'value': 13},
|
||||
{'label': '国创', 'value': 167},
|
||||
{'label': '音乐', 'value': 3},
|
||||
{'label': '舞蹈', 'value': 129},
|
||||
{'label': '游戏', 'value': 4},
|
||||
{'label': '知识', 'value': 36},
|
||||
{'label': '科技', 'value': 188},
|
||||
{'label': '运动', 'value': 234},
|
||||
{'label': '汽车', 'value': 223},
|
||||
{'label': '生活', 'value': 160},
|
||||
{'label': '美食', 'value': 211},
|
||||
{'label': '动物', 'value': 217},
|
||||
{'label': '鬼畜', 'value': 119},
|
||||
{'label': '时尚', 'value': 155},
|
||||
{'label': '资讯', 'value': 202},
|
||||
{'label': '娱乐', 'value': 5},
|
||||
{'label': '影视', 'value': 181},
|
||||
{'label': '记录', 'value': 177},
|
||||
{'label': '电影', 'value': 23},
|
||||
{'label': '电视', 'value': 11},
|
||||
];
|
||||
|
||||
RxInt currentTimeFilterval = 0.obs;
|
||||
RxInt currentPartFilterval = (-1).obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -219,4 +246,100 @@ class VideoPanelController extends GetxController {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onShowFilterSheet(searchPanelCtr) {
|
||||
showModalBottomSheet(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, StateSetter setState) {
|
||||
return Container(
|
||||
color: Theme.of(Get.context!).colorScheme.surface,
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ListTile(
|
||||
title: Text('内容时长'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 14,
|
||||
right: 14,
|
||||
bottom: 14,
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
direction: Axis.horizontal,
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
for (var i in timeFiltersList)
|
||||
Obx(
|
||||
() => SearchText(
|
||||
searchText: i['label'],
|
||||
searchTextIdx: i['value'],
|
||||
isSelect:
|
||||
currentTimeFilterval.value == i['value'],
|
||||
onSelect: (value) async {
|
||||
currentTimeFilterval.value = i['value'];
|
||||
setState(() {});
|
||||
SmartDialog.showToast("「${i['label']}」的筛选结果");
|
||||
SearchPanelController ctr =
|
||||
Get.find<SearchPanelController>(
|
||||
tag: 'video${searchPanelCtr.keyword!}');
|
||||
ctr.duration.value = i['value'];
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '获取中');
|
||||
await ctr.onRefresh();
|
||||
SmartDialog.dismiss();
|
||||
},
|
||||
onLongSelect: (value) => {},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const ListTile(
|
||||
title: Text('内容分区'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
child: Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
direction: Axis.horizontal,
|
||||
textDirection: TextDirection.ltr,
|
||||
children: [
|
||||
for (var i in partFiltersList)
|
||||
SearchText(
|
||||
searchText: i['label'],
|
||||
searchTextIdx: i['value'],
|
||||
isSelect: currentPartFilterval.value == i['value'],
|
||||
onSelect: (value) async {
|
||||
currentPartFilterval.value = i['value'];
|
||||
setState(() {});
|
||||
SmartDialog.showToast("「${i['label']}」的筛选结果");
|
||||
SearchPanelController ctr =
|
||||
Get.find<SearchPanelController>(
|
||||
tag: 'video${searchPanelCtr.keyword!}');
|
||||
ctr.tids.value = i['value'];
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '获取中');
|
||||
await ctr.onRefresh();
|
||||
SmartDialog.dismiss();
|
||||
},
|
||||
onLongSelect: (value) => {},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ class _FontSizeSelectPageState extends State<FontSizeSelectPage> {
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.3))),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@ -41,7 +41,8 @@ class _LogsPageState extends State<LogsPage> {
|
||||
.replaceAll('DEVICE INFO', '设备信息')
|
||||
.replaceAll('APP INFO', '应用信息')
|
||||
.replaceAll('ERROR', '错误信息')
|
||||
.replaceAll('STACK TRACE', '错误堆栈');
|
||||
.replaceAll('STACK TRACE', '错误堆栈')
|
||||
.replaceAll('#', 'Line');
|
||||
}).toList();
|
||||
List<Map<String, dynamic>> result = [];
|
||||
for (String i in contentList) {
|
||||
@ -50,7 +51,7 @@ class _LogsPageState extends State<LogsPage> {
|
||||
.split("\n")
|
||||
.map((l) {
|
||||
if (l.startsWith("Crash occurred on")) {
|
||||
date = DateTime.parse(
|
||||
date = DateTime.tryParse(
|
||||
l.split("Crash occurred on")[1].trim().split('.')[0],
|
||||
);
|
||||
return "";
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/utils/route_push.dart';
|
||||
import 'controller.dart';
|
||||
@ -87,7 +88,12 @@ class _SubPageState extends State<SubPage> {
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const Text('请求中');
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
itemCount: 10,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@ -4,6 +4,7 @@ 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:lottie/lottie.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
@ -96,11 +97,14 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const SliverToBoxAdapter(
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: Lottie.asset(
|
||||
'assets/loading.json',
|
||||
width: 200,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -589,8 +593,4 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Widget StaffPanel(BuildContext context, videoIntroController) {
|
||||
// return
|
||||
// }
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ class _FavPanelState extends State<FavPanel> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: sheetHeight,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
AppBar(
|
||||
|
||||
@ -57,7 +57,7 @@ class _GroupPanelState extends State<GroupPanel> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: sheetHeight,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
AppBar(
|
||||
|
||||
@ -12,7 +12,7 @@ class MenuRow extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
padding: const EdgeInsets.only(top: 9, bottom: 9, left: 12),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
@ -84,7 +84,7 @@ class MenuRow extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: selectStatus
|
||||
? Theme.of(context).colorScheme.onBackground
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/reply.dart';
|
||||
@ -15,7 +14,6 @@ class VideoReplyController extends GetxController {
|
||||
this.rpid,
|
||||
this.replyLevel,
|
||||
);
|
||||
final ScrollController scrollController = ScrollController();
|
||||
// 视频aid 请求时使用的oid
|
||||
int? aid;
|
||||
// 层级 2为楼中楼
|
||||
|
||||
@ -67,13 +67,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
vsync: this, duration: const Duration(milliseconds: 300));
|
||||
|
||||
_futureBuilderFuture = _videoReplyController.queryReplyList();
|
||||
|
||||
scrollController = ScrollController();
|
||||
fabAnimationCtr.forward();
|
||||
scrollListener();
|
||||
}
|
||||
|
||||
void scrollListener() {
|
||||
scrollController = _videoReplyController.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
@ -185,7 +184,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
var data = snapshot.data;
|
||||
if (data['status']) {
|
||||
if (_videoReplyController.replyList.isNotEmpty ||
|
||||
(data && data['status'])) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
() => _videoReplyController.isLoadingMore &&
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import 'package:appscheme/appscheme.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -11,6 +12,7 @@ import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/pages/preview/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply_new/index.dart';
|
||||
import 'package:pilipala/utils/app_scheme.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
@ -643,34 +645,17 @@ InlineSpan buildContent(
|
||||
'',
|
||||
);
|
||||
} else {
|
||||
final String pathSegment = Uri.parse(matchStr).path;
|
||||
Map matchRes = IdUtils.matchAvorBv(input: pathSegment);
|
||||
List matchKeys = matchRes.keys.toList();
|
||||
if (matchKeys.isNotEmpty) {
|
||||
UrlUtils.matchUrlPush(
|
||||
matchRes.containsKey('AV')
|
||||
? matchRes['AV']! as int
|
||||
: matchRes['BV'],
|
||||
title,
|
||||
matchStr,
|
||||
);
|
||||
} else {
|
||||
final String redirectUrl =
|
||||
await UrlUtils.parseRedirectUrl(matchStr);
|
||||
// if (redirectUrl == matchStr) {
|
||||
// Clipboard.setData(ClipboardData(text: matchStr));
|
||||
// SmartDialog.showToast('地址可能有误');
|
||||
// return;
|
||||
// }
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': redirectUrl,
|
||||
'type': 'url',
|
||||
'pageTitle': title
|
||||
},
|
||||
);
|
||||
}
|
||||
Uri uri = Uri.parse(matchStr);
|
||||
SchemeEntity scheme = SchemeEntity(
|
||||
scheme: uri.scheme,
|
||||
host: uri.host,
|
||||
port: uri.port,
|
||||
path: uri.path,
|
||||
query: uri.queryParameters,
|
||||
source: '',
|
||||
dataString: matchStr,
|
||||
);
|
||||
PiliSchame.fullPathPush(scheme);
|
||||
}
|
||||
} else {
|
||||
if (appUrlSchema.startsWith('bilibili://search')) {
|
||||
|
||||
@ -170,7 +170,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@ -78,7 +78,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: widget.source == 'videoDetail' ? widget.sheetHeight : null,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.source == 'videoDetail')
|
||||
|
||||
@ -99,7 +99,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
fullScreenStatusListener();
|
||||
if (Platform.isAndroid) {
|
||||
floating = vdCtr.floating!;
|
||||
autoEnterPip();
|
||||
}
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
lifecycleListener();
|
||||
@ -128,8 +127,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
|
||||
// 播放器状态监听
|
||||
void playerListener(PlayerStatus? status) async {
|
||||
playerStatus.value = status!;
|
||||
void playerListener(PlayerStatus status) async {
|
||||
playerStatus.value = status;
|
||||
autoEnterPip(status: status);
|
||||
if (status == PlayerStatus.completed) {
|
||||
// 结束播放退出全屏
|
||||
if (autoExitFullcreen) {
|
||||
@ -181,6 +181,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
plPlayerController!.addStatusLister(playerListener);
|
||||
vdCtr.autoPlay.value = true;
|
||||
vdCtr.isShowCover.value = false;
|
||||
autoEnterPip(status: PlayerStatus.playing);
|
||||
}
|
||||
|
||||
void fullScreenStatusListener() {
|
||||
@ -287,10 +288,12 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
.subscribe(this, ModalRoute.of(context)! as PageRoute);
|
||||
}
|
||||
|
||||
void autoEnterPip() {
|
||||
void autoEnterPip({PlayerStatus? status}) {
|
||||
final String routePath = Get.currentRoute;
|
||||
if (autoPiP && routePath.startsWith('/video')) {
|
||||
floating.toggleAutoPip(autoEnter: autoPiP);
|
||||
floating.toggleAutoPip(
|
||||
autoEnter: autoPiP && status == PlayerStatus.playing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -314,6 +317,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
case 'show' || 'restart':
|
||||
plPlayerController?.danmakuController?.clear();
|
||||
break;
|
||||
case 'pause':
|
||||
vdCtr.hiddenReplyReplyPanel();
|
||||
if (vdCtr.videoType == SearchType.video) {
|
||||
videoIntroController.hiddenEpisodeBottomSheet();
|
||||
}
|
||||
if (vdCtr.videoType == SearchType.media_bangumi) {
|
||||
bangumiIntroController.hiddenEpisodeBottomSheet();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -525,11 +537,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
key: vdCtr.scaffoldKey,
|
||||
backgroundColor: Colors.black,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(0),
|
||||
child: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
backgroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
@ -559,8 +570,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
return SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
// 假装使用一个非空变量,避免Obx检测不到而罢工
|
||||
pinned: vdCtr.autoPlay.value,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
@ -568,47 +578,42 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
backgroundColor: Colors.black,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: PopScope(
|
||||
canPop: plPlayerController?.isFullScreen.value !=
|
||||
true,
|
||||
onPopInvoked: (bool didPop) {
|
||||
if (plPlayerController?.isFullScreen.value ==
|
||||
true) {
|
||||
plPlayerController!
|
||||
.triggerFullScreen(status: false);
|
||||
}
|
||||
if (MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape) {
|
||||
verticalScreen();
|
||||
}
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context,
|
||||
BoxConstraints boxConstraints) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
if (isShowing)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 0),
|
||||
child: videoPlayerPanel,
|
||||
),
|
||||
canPop:
|
||||
plPlayerController?.isFullScreen.value != true,
|
||||
onPopInvoked: (bool didPop) {
|
||||
if (plPlayerController?.isFullScreen.value ==
|
||||
true) {
|
||||
plPlayerController!
|
||||
.triggerFullScreen(status: false);
|
||||
}
|
||||
if (MediaQuery.of(context).orientation ==
|
||||
Orientation.landscape) {
|
||||
verticalScreen();
|
||||
}
|
||||
},
|
||||
child: Hero(
|
||||
tag: heroTag,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
if (isShowing) videoPlayerPanel,
|
||||
|
||||
/// 关闭自动播放时 手动播放
|
||||
Obx(
|
||||
() => Visibility(
|
||||
visible: !vdCtr.autoPlay.value &&
|
||||
vdCtr.isShowCover.value,
|
||||
child: Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: handlePlayPanel(),
|
||||
),
|
||||
),
|
||||
/// 关闭自动播放时 手动播放
|
||||
Obx(
|
||||
() => Visibility(
|
||||
visible: !vdCtr.autoPlay.value &&
|
||||
vdCtr.isShowCover.value,
|
||||
child: Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: handlePlayPanel(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -627,55 +632,51 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
: pinnedHeaderHeight;
|
||||
},
|
||||
onlyOneScrollInBody: true,
|
||||
body: ColoredBox(
|
||||
key: Key(heroTag),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
tabbarBuild(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: vdCtr.tabCtr,
|
||||
children: <Widget>[
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return CustomScrollView(
|
||||
key: const PageStorageKey<String>('简介'),
|
||||
slivers: <Widget>[
|
||||
if (vdCtr.videoType == SearchType.video) ...[
|
||||
VideoIntroPanel(bvid: vdCtr.bvid),
|
||||
] else if (vdCtr.videoType ==
|
||||
SearchType.media_bangumi) ...[
|
||||
Obx(() => BangumiIntroPanel(
|
||||
cid: vdCtr.cid.value)),
|
||||
],
|
||||
SliverToBoxAdapter(
|
||||
child: Divider(
|
||||
indent: 12,
|
||||
endIndent: 12,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.06),
|
||||
),
|
||||
),
|
||||
if (vdCtr.videoType == SearchType.video &&
|
||||
vdCtr.enableRelatedVideo)
|
||||
const RelatedVideoPanel(),
|
||||
body: Column(
|
||||
children: [
|
||||
tabbarBuild(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: vdCtr.tabCtr,
|
||||
children: <Widget>[
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return CustomScrollView(
|
||||
key: const PageStorageKey<String>('简介'),
|
||||
slivers: <Widget>[
|
||||
if (vdCtr.videoType == SearchType.video) ...[
|
||||
VideoIntroPanel(bvid: vdCtr.bvid),
|
||||
] else if (vdCtr.videoType ==
|
||||
SearchType.media_bangumi) ...[
|
||||
Obx(() =>
|
||||
BangumiIntroPanel(cid: vdCtr.cid.value)),
|
||||
],
|
||||
);
|
||||
},
|
||||
SliverToBoxAdapter(
|
||||
child: Divider(
|
||||
indent: 12,
|
||||
endIndent: 12,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.06),
|
||||
),
|
||||
),
|
||||
if (vdCtr.videoType == SearchType.video &&
|
||||
vdCtr.enableRelatedVideo)
|
||||
const RelatedVideoPanel(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Obx(
|
||||
() => VideoReplyPanel(
|
||||
bvid: vdCtr.bvid,
|
||||
oid: vdCtr.oid.value,
|
||||
),
|
||||
Obx(
|
||||
() => VideoReplyPanel(
|
||||
bvid: vdCtr.bvid,
|
||||
oid: vdCtr.oid.value,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -23,7 +23,7 @@ class AiDetail extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
sheetHeight = localCache.get('sheetHeight');
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
height: sheetHeight,
|
||||
child: Column(
|
||||
|
||||
@ -29,7 +29,7 @@ class ScrollAppBar extends StatelessWidget {
|
||||
opacity: scrollDistance / (videoHeight - kToolbarHeight),
|
||||
child: Container(
|
||||
height: statusBarHeight + kToolbarHeight,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
padding: EdgeInsets.only(top: statusBarHeight),
|
||||
child: AppBar(
|
||||
primary: false,
|
||||
|
||||
@ -93,7 +93,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
height: 460,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
@ -317,7 +317,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
height: 500,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
@ -377,7 +377,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
inactiveThumbColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
inactiveTrackColor:
|
||||
Theme.of(context).colorScheme.background,
|
||||
Theme.of(context).colorScheme.surface,
|
||||
splashRadius: 10.0,
|
||||
// boolean variable value
|
||||
value: shutdownTimerService.waitForPlayingCompleted,
|
||||
@ -570,7 +570,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
height: 310,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
@ -660,7 +660,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
height: 250,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
@ -734,7 +734,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
height: 250,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
@ -828,7 +828,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
height: 580,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
@ -1084,7 +1084,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
height: 250,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
|
||||
@ -76,7 +76,7 @@ class WebviewController extends GetxController {
|
||||
(url.startsWith(
|
||||
'https://passport.bilibili.com/web/sso/exchange_cookie') ||
|
||||
url.startsWith('https://m.bilibili.com/'))) {
|
||||
confirmLogin(url);
|
||||
LoginUtils.confirmLogin(url, controller);
|
||||
}
|
||||
},
|
||||
onWebResourceError: (WebResourceError error) {},
|
||||
@ -97,54 +97,4 @@ class WebviewController extends GetxController {
|
||||
)
|
||||
..loadRequest(Uri.parse(url));
|
||||
}
|
||||
|
||||
confirmLogin(url) async {
|
||||
var content = '';
|
||||
if (url != null) {
|
||||
content = '${content + url}; \n';
|
||||
}
|
||||
try {
|
||||
await SetCookie.onSet();
|
||||
final result = await UserHttp.userInfo();
|
||||
if (result['status'] && result['data'].isLogin) {
|
||||
SmartDialog.showToast('登录成功');
|
||||
try {
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
if (!userInfoCache.isOpen) {
|
||||
userInfoCache = await Hive.openBox('userInfo');
|
||||
}
|
||||
await userInfoCache.put('userInfoCache', result['data']);
|
||||
|
||||
final HomeController homeCtr = Get.find<HomeController>();
|
||||
homeCtr.updateLoginStatus(true);
|
||||
homeCtr.userFace.value = result['data'].face;
|
||||
final MediaController mediaCtr = Get.find<MediaController>();
|
||||
mediaCtr.mid = result['data'].mid;
|
||||
await LoginUtils.refreshLoginStatus(true);
|
||||
} catch (err) {
|
||||
SmartDialog.show(builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('登录遇到问题'),
|
||||
content: Text(err.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => controller.reload(),
|
||||
child: const Text('确认'),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
Get.back();
|
||||
} else {
|
||||
// 获取用户信息失败
|
||||
SmartDialog.showToast(result['msg']);
|
||||
Clipboard.setData(ClipboardData(text: result['msg']));
|
||||
}
|
||||
} catch (e) {
|
||||
SmartDialog.showNotify(msg: e.toString(), notifyType: NotifyType.warning);
|
||||
content = content + e.toString();
|
||||
Clipboard.setData(ClipboardData(text: content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/utils/login.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'controller.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
@ -43,7 +44,8 @@ class _WebviewPageState extends State<WebviewPage> {
|
||||
Obx(
|
||||
() => _webviewController.type.value == 'login'
|
||||
? TextButton(
|
||||
onPressed: () => _webviewController.confirmLogin(null),
|
||||
onPressed: () =>
|
||||
LoginUtils.confirmLogin(null, _webviewController),
|
||||
child: const Text('刷新登录状态'),
|
||||
)
|
||||
: const SizedBox(),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/skeleton.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
@ -102,134 +103,83 @@ class _WhisperPageState extends State<WhisperPage> {
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: Column(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map? data = snapshot.data;
|
||||
if (data != null && data['status']) {
|
||||
RxList sessionList = _whisperController.sessionList;
|
||||
return Obx(
|
||||
() => sessionList.isEmpty
|
||||
? const SizedBox()
|
||||
: ListView.separated(
|
||||
itemCount: sessionList.length,
|
||||
shrinkWrap: true,
|
||||
physics:
|
||||
const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (_, int i) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
sessionList[i].unreadCount = 0;
|
||||
sessionList.refresh();
|
||||
Get.toNamed(
|
||||
'/whisperDetail',
|
||||
parameters: {
|
||||
'talkerId': sessionList[i]
|
||||
.talkerId
|
||||
.toString(),
|
||||
'name': sessionList[i]
|
||||
.accountInfo
|
||||
.name,
|
||||
'face': sessionList[i]
|
||||
.accountInfo
|
||||
.face,
|
||||
'mid': sessionList[i]
|
||||
.accountInfo
|
||||
.mid
|
||||
.toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
leading: Badge(
|
||||
isLabelVisible:
|
||||
sessionList[i].unreadCount > 0,
|
||||
label: Text(sessionList[i]
|
||||
.unreadCount
|
||||
.toString()),
|
||||
alignment: Alignment.topRight,
|
||||
child: NetworkImgLayer(
|
||||
width: 45,
|
||||
height: 45,
|
||||
type: 'avatar',
|
||||
src: sessionList[i]
|
||||
.accountInfo
|
||||
.face,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
sessionList[i].accountInfo.name),
|
||||
subtitle: Text(
|
||||
sessionList[i].lastMsg.content !=
|
||||
null &&
|
||||
sessionList[i]
|
||||
.lastMsg
|
||||
.content !=
|
||||
''
|
||||
? (sessionList[i]
|
||||
.lastMsg
|
||||
.content['text'] ??
|
||||
sessionList[i]
|
||||
.lastMsg
|
||||
.content['content'] ??
|
||||
sessionList[i]
|
||||
.lastMsg
|
||||
.content['title'] ??
|
||||
sessionList[i]
|
||||
.lastMsg
|
||||
.content[
|
||||
'reply_content'] ??
|
||||
'不支持的消息类型')
|
||||
: '不支持的消息类型',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline)),
|
||||
trailing: Text(
|
||||
Utils.dateFormat(sessionList[i]
|
||||
.lastMsg
|
||||
.timestamp),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder:
|
||||
(BuildContext context, int index) {
|
||||
return Divider(
|
||||
indent: 72,
|
||||
endIndent: 20,
|
||||
height: 6,
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return Center(
|
||||
child: Text(data?['msg'] ?? '请求异常'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map? data = snapshot.data;
|
||||
if (data != null && data['status']) {
|
||||
RxList sessionList = _whisperController.sessionList;
|
||||
return Obx(
|
||||
() => sessionList.isEmpty
|
||||
? const SizedBox()
|
||||
: ListView.separated(
|
||||
itemCount: sessionList.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (_, int i) {
|
||||
return SessionItem(
|
||||
sessionItem: sessionList[i],
|
||||
changeFucCall: () =>
|
||||
sessionList.refresh(),
|
||||
);
|
||||
},
|
||||
separatorBuilder:
|
||||
(BuildContext context, int index) {
|
||||
return Divider(
|
||||
indent: 72,
|
||||
endIndent: 20,
|
||||
height: 6,
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return Center(
|
||||
child: Text(data?['msg'] ?? '请求异常'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return ListView.builder(
|
||||
itemCount: 15,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, int i) {
|
||||
return Skeleton(
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
),
|
||||
title: Container(
|
||||
width: 100,
|
||||
height: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
subtitle: Container(
|
||||
width: 80,
|
||||
height: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -239,3 +189,67 @@ class _WhisperPageState extends State<WhisperPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SessionItem extends StatelessWidget {
|
||||
final dynamic sessionItem;
|
||||
final Function changeFucCall;
|
||||
|
||||
const SessionItem({
|
||||
super.key,
|
||||
required this.sessionItem,
|
||||
required this.changeFucCall,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = sessionItem.lastMsg.content;
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
sessionItem.unreadCount = 0;
|
||||
changeFucCall.call();
|
||||
Get.toNamed(
|
||||
'/whisperDetail',
|
||||
parameters: {
|
||||
'talkerId': sessionItem.talkerId.toString(),
|
||||
'name': sessionItem.accountInfo.name,
|
||||
'face': sessionItem.accountInfo.face,
|
||||
'mid': sessionItem.accountInfo.mid.toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
leading: Badge(
|
||||
isLabelVisible: sessionItem.unreadCount > 0,
|
||||
label: Text(sessionItem.unreadCount.toString()),
|
||||
alignment: Alignment.topRight,
|
||||
child: NetworkImgLayer(
|
||||
width: 45,
|
||||
height: 45,
|
||||
type: 'avatar',
|
||||
src: sessionItem.accountInfo.face,
|
||||
),
|
||||
),
|
||||
title: Text(sessionItem.accountInfo.name),
|
||||
subtitle: Text(
|
||||
content != null && content != ''
|
||||
? (content['text'] ??
|
||||
content['content'] ??
|
||||
content['title'] ??
|
||||
content['reply_content'] ??
|
||||
'不支持的消息类型')
|
||||
: '不支持的消息类型',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.copyWith(color: Theme.of(context).colorScheme.outline)),
|
||||
trailing: Text(
|
||||
Utils.dateFormat(sessionItem.lastMsg.timestamp),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.copyWith(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,115 +259,114 @@ class ChatItem extends StatelessWidget {
|
||||
);
|
||||
case MsgType.auto_reply_push:
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.withOpacity(0.4),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.withOpacity(0.4),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(6),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
content['main_title'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
content['main_title'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
for (var i in content['sub_cards']) ...<Widget>[
|
||||
const SizedBox(height: 6),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}',
|
||||
caseSensitive: false);
|
||||
Iterable<Match> matches =
|
||||
bvRegex.allMatches(i['jump_url']);
|
||||
if (matches.isNotEmpty) {
|
||||
Match match = matches.first;
|
||||
String bvid = match.group(0)!;
|
||||
try {
|
||||
SmartDialog.showLoading();
|
||||
final int cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
final String heroTag = Utils.makeHeroTag(bvid);
|
||||
SmartDialog.dismiss<dynamic>().then(
|
||||
(e) => Get.toNamed<dynamic>(
|
||||
'/video?bvid=$bvid&cid=$cid',
|
||||
arguments: <String, String?>{
|
||||
'pic': i['cover_url'],
|
||||
'heroTag': heroTag,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast('未匹配到 BV 号');
|
||||
Get.toNamed('/webview',
|
||||
arguments: {'url': i['jump_url']});
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
),
|
||||
for (var i in content['sub_cards']) ...<Widget>[
|
||||
const SizedBox(height: 6),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
RegExp bvRegex =
|
||||
RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false);
|
||||
Iterable<Match> matches =
|
||||
bvRegex.allMatches(i['jump_url']);
|
||||
if (matches.isNotEmpty) {
|
||||
Match match = matches.first;
|
||||
String bvid = match.group(0)!;
|
||||
try {
|
||||
SmartDialog.showLoading();
|
||||
final int cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
final String heroTag = Utils.makeHeroTag(bvid);
|
||||
SmartDialog.dismiss<dynamic>().then(
|
||||
(e) => Get.toNamed<dynamic>(
|
||||
'/video?bvid=$bvid&cid=$cid',
|
||||
arguments: <String, String?>{
|
||||
'pic': i['cover_url'],
|
||||
'heroTag': heroTag,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast('未匹配到 BV 号');
|
||||
Get.toNamed('/webview',
|
||||
arguments: {'url': i['jump_url']});
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 130,
|
||||
height: 130 * 9 / 16,
|
||||
src: i['cover_url'],
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 130,
|
||||
height: 130 * 9 / 16,
|
||||
src: i['cover_url'],
|
||||
Text(
|
||||
i['field1'],
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
i['field2'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context).withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
i['field3'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context).withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
i['field1'],
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
i['field2'],
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context).withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
Utils.timeFormat(int.parse(i['field3'])),
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.6,
|
||||
height: 1.5,
|
||||
color: textColor(context).withOpacity(0.6),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
],
|
||||
),
|
||||
);
|
||||
default:
|
||||
return Text(
|
||||
content != null && content != ''
|
||||
|
||||
Reference in New Issue
Block a user