Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-06-19 11:29:03 +08:00
parent 5644e9a0e1
commit 6f48a97b4b
23 changed files with 1058 additions and 951 deletions

View File

@@ -266,7 +266,7 @@ class VideoCustomActions {
MineController.anonymity.value
? const Icon(MdiIcons.incognitoOff, size: 16)
: const Icon(MdiIcons.incognito, size: 16),
() => MineController.onChangeAnonymity(context),
MineController.onChangeAnonymity,
)
];
}

View File

@@ -662,7 +662,7 @@ class _ArticlePageState extends State<ArticlePage>
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: () => _articleCtr.queryBySort(),
onPressed: _articleCtr.queryBySort,
icon: const Icon(Icons.sort, size: 16),
label: Obx(
() => Text(

View File

@@ -1,8 +1,6 @@
import 'package:PiliPlus/models_new/fav/fav_folder/data.dart';
import 'package:PiliPlus/models_new/fav/fav_folder/list.dart';
import 'package:PiliPlus/models_new/video/video_tag/data.dart';
import 'package:PiliPlus/services/account_service.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:get/get.dart';
abstract class CommonIntroController extends GetxController {
@@ -25,13 +23,4 @@ abstract class CommonIntroController extends GetxController {
Future queryVideoInFolder();
Future<void> actionFavVideo();
void onChoose(bool checkValue, int index) {
feedBack();
FavFolderInfo item = favFolderData.value.list![index];
item
..favState = checkValue ? 1 : 0
..mediaCount = checkValue ? item.mediaCount + 1 : item.mediaCount - 1;
favFolderData.refresh();
}
}

View File

@@ -125,8 +125,9 @@ class _UpPanelState extends State<UpPanel> {
}
void _onSelect(UserItem data) {
widget.dynamicsController.currentMid = data.mid;
widget.dynamicsController.onSelectUp(data.mid);
widget.dynamicsController
..currentMid = data.mid
..onSelectUp(data.mid);
data.hasUpdate = false;

View File

@@ -138,28 +138,40 @@ class _VotePanelState extends State<VotePanel> {
];
Widget _buildOptions(int index) {
final opt = _voteInfo.options[index];
final selected = groupValue.contains(opt.optidx);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: PercentageChip(
label: opt.optdesc!,
percentage: _showPercentage ? _percentage[index] : null,
selected: selected,
onSelected: !_enabled || (groupValue.length >= _maxCnt && !selected)
? null
: (value) => _onSelected(value, opt.optidx!),
child: Builder(
builder: (context) {
final opt = _voteInfo.options[index];
final selected = groupValue.contains(opt.optidx);
return PercentageChip(
label: opt.optdesc!,
percentage: _showPercentage ? _percentage[index] : null,
selected: selected,
onSelected: !_enabled || (groupValue.length >= _maxCnt && !selected)
? null
: (value) => _onSelected(context, value, opt.optidx!),
);
},
),
);
}
void _onSelected(bool value, int optidx) {
void _onSelected(BuildContext context, bool value, int optidx) {
bool isMax = groupValue.length >= _maxCnt;
if (value) {
groupValue.add(optidx);
} else {
groupValue.remove(optidx);
}
setState(() {});
if ((isMax &&
_maxCnt != _voteInfo.options.length &&
groupValue.length < _maxCnt) ||
(groupValue.length >= _maxCnt && _maxCnt < _voteInfo.options.length)) {
setState(() {});
} else {
(context as Element).markNeedsBuild();
}
}
Widget _buildContext() {

View File

@@ -670,7 +670,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: () => _controller.queryBySort(),
onPressed: _controller.queryBySort,
icon: Icon(
Icons.sort,
size: 16,

View File

@@ -83,12 +83,12 @@ class _CreateFavPageState extends State<CreateFavPage> {
privacy: _isPublic ? 0 : 1,
cover: _cover ?? '',
intro: _introController.text,
).then((data) {
if (data['status']) {
Get.back(result: data['data']);
).then((res) {
if (res['status']) {
Get.back(result: res['data']);
SmartDialog.showToast('${_mediaId != null ? '编辑' : '创建'}成功');
} else {
SmartDialog.showToast(data['msg']);
SmartDialog.showToast(res['msg']);
}
});
},
@@ -117,7 +117,7 @@ class _CreateFavPageState extends State<CreateFavPage> {
);
}
Future<void> _pickImg(ThemeData theme) async {
Future<void> _pickImg(BuildContext context, ThemeData theme) async {
try {
XFile? pickedFile = await _imagePicker.pickImage(
source: ImageSource.gallery,
@@ -154,12 +154,12 @@ class _CreateFavPageState extends State<CreateFavPage> {
path: croppedFile.path,
bucket: 'medialist',
dir: 'cover',
).then((data) {
if (data['status']) {
_cover = data['data']['location'];
setState(() {});
).then((res) {
if (res['status']) {
_cover = res['data']['location'];
(context as Element).markNeedsBuild();
} else {
SmartDialog.showToast(data['msg']);
SmartDialog.showToast(res['msg']);
}
});
}
@@ -175,87 +175,91 @@ class _CreateFavPageState extends State<CreateFavPage> {
child: Column(
children: [
if (_attr == null || !FavUtil.isDefaultFav(_attr!)) ...[
ListTile(
tileColor: theme.colorScheme.onInverseSurface,
onTap: () {
EasyThrottle.throttle(
'imagePicker', const Duration(milliseconds: 500), () {
if (_cover?.isNotEmpty == true) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding:
const EdgeInsets.fromLTRB(0, 12, 0, 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
dense: true,
onTap: () {
Get.back();
_pickImg(theme);
},
title: const Text(
'替换封面',
style: TextStyle(fontSize: 14),
),
Builder(
builder: (context) {
return ListTile(
tileColor: theme.colorScheme.onInverseSurface,
onTap: () {
EasyThrottle.throttle(
'imagePicker', const Duration(milliseconds: 500), () {
if (_cover?.isNotEmpty == true) {
showDialog(
context: context,
builder: (_) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding:
const EdgeInsets.fromLTRB(0, 12, 0, 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
dense: true,
onTap: () {
Get.back();
_pickImg(context, theme);
},
title: const Text(
'替换封面',
style: TextStyle(fontSize: 14),
),
),
ListTile(
dense: true,
onTap: () {
Get.back();
_cover = null;
(context as Element).markNeedsBuild();
},
title: const Text(
'移除封面',
style: TextStyle(fontSize: 14),
),
),
],
),
ListTile(
dense: true,
onTap: () {
Get.back();
_cover = null;
setState(() {});
},
title: const Text(
'移除封面',
style: TextStyle(fontSize: 14),
),
),
],
),
);
},
);
},
);
} else {
_pickImg(theme);
}
});
},
leading: Text(
'封面',
style: leadingStyle,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_cover?.isNotEmpty == true)
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: LayoutBuilder(
builder: (context, constraints) {
return ClipRRect(
borderRadius:
const BorderRadius.all(Radius.circular(6)),
child: CachedNetworkImage(
imageUrl: ImageUtil.thumbnailUrl(_cover!),
height: constraints.maxHeight,
width: constraints.maxHeight * 16 / 9,
fit: BoxFit.cover,
),
);
},
),
),
const SizedBox(width: 10),
Icon(
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline,
} else {
_pickImg(context, theme);
}
});
},
leading: Text(
'封面',
style: leadingStyle,
),
],
),
trailing: Row(
spacing: 10,
mainAxisSize: MainAxisSize.min,
children: [
if (_cover?.isNotEmpty == true)
Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: LayoutBuilder(
builder: (context, constraints) {
return ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(6)),
child: CachedNetworkImage(
imageUrl: ImageUtil.thumbnailUrl(_cover!),
height: constraints.maxHeight,
width: constraints.maxHeight * 16 / 9,
fit: BoxFit.cover,
),
);
},
),
),
Icon(
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline,
),
],
),
);
},
),
const SizedBox(height: 16),
],
@@ -368,26 +372,30 @@ class _CreateFavPageState extends State<CreateFavPage> {
),
const SizedBox(height: 16),
],
ListTile(
onTap: () => setState(() {
_isPublic = !_isPublic;
}),
tileColor: theme.colorScheme.onInverseSurface,
leading: Text(
'公开',
style: leadingStyle,
),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
value: _isPublic,
onChanged: (value) {
setState(() {
_isPublic = value;
});
}),
),
Builder(
builder: (context) {
void onTap() {
_isPublic = !_isPublic;
(context as Element).markNeedsBuild();
}
return ListTile(
onTap: onTap,
tileColor: theme.colorScheme.onInverseSurface,
leading: Text(
'公开',
style: leadingStyle,
),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
value: _isPublic,
onChanged: (value) => onTap(),
),
),
);
},
),
const SizedBox(height: 16),
],

View File

@@ -215,12 +215,12 @@ class _FavDetailPageState extends State<FavDetailPage> {
context: context,
title: '确定删除该收藏夹?',
onConfirm: () =>
FavHttp.deleteFolder(mediaIds: [mediaId]).then((data) {
if (data['status']) {
FavHttp.deleteFolder(mediaIds: [mediaId]).then((res) {
if (res['status']) {
SmartDialog.showToast('删除成功');
Get.back(result: true);
} else {
SmartDialog.showToast(data['msg']);
SmartDialog.showToast(res['msg']);
}
}),
),

View File

@@ -52,26 +52,37 @@ class _FavPanelState extends State<FavPanel> {
FavFolderInfo item = widget.ctr.favFolderData.value.list![index];
return Material(
type: MaterialType.transparency,
child: ListTile(
onTap: () => setState(
() => widget.ctr.onChoose(item.favState != 1, index)),
dense: true,
leading: FavUtil.isPublicFav(item.attr)
? const Icon(Icons.folder_outlined)
: const Icon(Icons.lock_outline),
minLeadingWidth: 0,
title: Text(item.title),
subtitle: Text(
'${item.mediaCount}个内容 . ${FavUtil.isPublicFavText(item.attr)}',
),
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: item.favState == 1,
onChanged: (bool? checkValue) =>
setState(() => widget.ctr.onChoose(checkValue!, index)),
),
),
child: Builder(
builder: (context) {
void onTap() {
bool isChecked = item.favState == 1;
item
..favState = isChecked ? 0 : 1
..mediaCount =
isChecked ? item.mediaCount - 1 : item.mediaCount + 1;
(context as Element).markNeedsBuild();
}
return ListTile(
onTap: onTap,
dense: true,
leading: FavUtil.isPublicFav(item.attr)
? const Icon(Icons.folder_outlined)
: const Icon(Icons.lock_outline),
minLeadingWidth: 0,
title: Text(item.title),
subtitle: Text(
'${item.mediaCount}个内容 . ${FavUtil.isPublicFavText(item.attr)}',
),
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: item.favState == 1,
onChanged: (bool? checkValue) => onTap(),
),
),
);
},
),
);
},
@@ -136,7 +147,7 @@ class _FavPanelState extends State<FavPanel> {
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
TextButton(
onPressed: () => Get.back(),
onPressed: Get.back,
style: TextButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),

View File

@@ -25,7 +25,7 @@ class GroupPanel extends StatefulWidget {
class _GroupPanelState extends State<GroupPanel> {
LoadingState<List<MemberTagItemModel>> loadingState =
LoadingState<List<MemberTagItemModel>>.loading();
bool showDefaultBtn = true;
RxBool showDefaultBtn = true.obs;
@override
void initState() {
@@ -43,7 +43,7 @@ class _GroupPanelState extends State<GroupPanel> {
..map((item) {
return item.checked = widget.tags?.contains(item.tagid) == true;
}).toList();
showDefaultBtn = !tagsList.any((e) => e.checked == true);
showDefaultBtn.value = !tagsList.any((e) => e.checked == true);
loadingState = Success(tagsList);
} else {
loadingState = Error(res['msg']);
@@ -89,28 +89,32 @@ class _GroupPanelState extends State<GroupPanel> {
final item = response[index];
return Material(
type: MaterialType.transparency,
child: ListTile(
onTap: () {
item.checked = !item.checked!;
showDefaultBtn = !response.any((e) => e.checked == true);
setState(() {});
child: Builder(
builder: (context) {
void onTap() {
item.checked = !item.checked!;
(context as Element).markNeedsBuild();
showDefaultBtn.value =
!response.any((e) => e.checked == true);
}
return ListTile(
onTap: onTap,
dense: true,
leading: const Icon(Icons.group_outlined),
minLeadingWidth: 0,
title: Text(item.name ?? ''),
subtitle:
item.tip?.isNotEmpty == true ? Text(item.tip!) : null,
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: item.checked,
onChanged: (bool? checkValue) => onTap(),
),
),
);
},
dense: true,
leading: const Icon(Icons.group_outlined),
minLeadingWidth: 0,
title: Text(item.name ?? ''),
subtitle: item.tip?.isNotEmpty == true ? Text(item.tip!) : null,
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: item.checked,
onChanged: (bool? checkValue) {
item.checked = checkValue;
showDefaultBtn = !response.any((e) => e.checked == true);
setState(() {});
},
),
),
),
);
},
@@ -152,13 +156,13 @@ class _GroupPanelState extends State<GroupPanel> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => onSave(),
onPressed: onSave,
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 30, right: 30),
foregroundColor: theme.colorScheme.onPrimary,
backgroundColor: theme.colorScheme.primary,
),
child: Text(showDefaultBtn ? '保存至默认分组' : '保存'),
child: Obx(() => Text(showDefaultBtn.value ? '保存至默认分组' : '保存')),
),
],
),

View File

@@ -32,10 +32,14 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 20),
const Text('使用 bilibili 官方 App 扫码登录'),
const SizedBox(height: 20),
Obx(() => Text('剩余有效时间: ${_loginPageCtr.qrCodeLeftTime}',
Obx(
() => Text(
'剩余有效时间: ${_loginPageCtr.qrCodeLeftTime}',
style: TextStyle(
fontFeatures: const [FontFeature.tabularFigures()],
color: theme.colorScheme.primaryFixedDim))),
color: theme.colorScheme.primaryFixedDim),
),
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -107,29 +111,37 @@ class _LoginPageState extends State<LoginPage> {
}),
),
const SizedBox(height: 10),
Obx(() => Text(
_loginPageCtr.statusQRCode.value,
style: TextStyle(color: theme.colorScheme.secondaryFixedDim),
)),
Obx(() => GestureDetector(
onTap: () => Utils.copyText(
_loginPageCtr.codeInfo['data']?['url'] ?? '',
toastText: '已复制到剪贴板可粘贴至已登录的app私信处发送然后点击已发送的链接打开'),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Text(_loginPageCtr.codeInfo['data']?['url'] ?? "",
style: theme.textTheme.labelSmall!.copyWith(
color: theme.colorScheme.onSurface
.withValues(alpha: 0.4))),
),
)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text('请务必在 PiliPlus 开源仓库等可信渠道下载安装。',
Obx(
() => Text(
_loginPageCtr.statusQRCode.value,
style: TextStyle(color: theme.colorScheme.secondaryFixedDim),
),
),
Obx(
() => GestureDetector(
onTap: () => Utils.copyText(
_loginPageCtr.codeInfo['data']?['url'] ?? '',
toastText: '已复制到剪贴板可粘贴至已登录的app私信处发送然后点击已发送的链接打开'),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Text(
_loginPageCtr.codeInfo['data']?['url'] ?? "",
style: theme.textTheme.labelSmall!.copyWith(
color:
theme.colorScheme.onSurface.withValues(alpha: 0.4)))),
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
'请务必在 PiliPlus 开源仓库等可信渠道下载安装。',
style: theme.textTheme.labelSmall!.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
),
),
],
);
}
@@ -247,8 +259,9 @@ class _LoginPageState extends State<LoginPage> {
const EdgeInsets.fromLTRB(0.0, 2.0, 0.0, 16.0),
children: [
const Padding(
padding: EdgeInsets.fromLTRB(25, 0, 25, 10),
child: Text("试试扫码、手机号登录,或选择")),
padding: EdgeInsets.fromLTRB(25, 0, 25, 10),
child: Text("试试扫码、手机号登录,或选择"),
),
ListTile(
title: const Text(
'找回密码(手机版)',
@@ -303,20 +316,25 @@ class _LoginPageState extends State<LoginPage> {
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
'根据 bilibili 官方登录接口规范,密码将在本地加盐、加密后传输。\n'
'盐与公钥均由官方提供;以 RSA/ECB/PKCS1Padding 方式加密。\n'
'账号密码仅用于该登录接口,不予保存;本地仅存储登录凭证。\n'
'请务必在 PiliPlus 开源仓库等可信渠道下载安装。',
textAlign: TextAlign.center,
style: theme.textTheme.labelSmall!.copyWith(
color:
theme.colorScheme.onSurface.withValues(alpha: 0.4)))),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
'根据 bilibili 官方登录接口规范,密码将在本地加盐、加密后传输。\n'
'盐与公钥均由官方提供;以 RSA/ECB/PKCS1Padding 方式加密。\n'
'账号密码仅用于该登录接口,不予保存;本地仅存储登录凭证。\n'
'请务必在 PiliPlus 开源仓库等可信渠道下载安装。',
textAlign: TextAlign.center,
style: theme.textTheme.labelSmall!.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
),
),
],
);
}
late final List<Map<String, dynamic>> internationalDialingPrefix =
Constants.internationalDialingPrefix;
Widget loginBySmS(ThemeData theme) {
return Column(
children: [
@@ -324,55 +342,63 @@ class _LoginPageState extends State<LoginPage> {
const Text('使用手机短信验证码登录'),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: DecoratedBox(
decoration: UnderlineTabIndicator(
borderSide: BorderSide(
color: theme.colorScheme.outline.withValues(alpha: 0.4)),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: DecoratedBox(
decoration: UnderlineTabIndicator(
borderSide: BorderSide(
color: theme.colorScheme.outline.withValues(alpha: 0.4),
),
child: Row(
children: [
const SizedBox(width: 12),
Icon(
Icons.phone,
color: theme.colorScheme.onSurfaceVariant,
),
child: Row(
children: [
const SizedBox(width: 12),
Builder(
builder: (context) {
return PopupMenuButton<Map<String, dynamic>>(
padding: EdgeInsets.zero,
tooltip: '选择国际冠码,'
'当前为${_loginPageCtr.selectedCountryCodeId['cname']}'
'+${_loginPageCtr.selectedCountryCodeId['country_id']}',
onSelected: (Map<String, dynamic> type) {},
itemBuilder: (_) => internationalDialingPrefix
.map((Map<String, dynamic> item) {
return PopupMenuItem<Map<String, dynamic>>(
onTap: () {
_loginPageCtr.selectedCountryCodeId = item;
(context as Element).markNeedsBuild();
},
value: item,
child: Row(children: [
Text(item['cname']),
const Spacer(),
Text("+${item['country_id']}")
]),
);
}).toList(),
child: Row(
children: [
Icon(
Icons.phone,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 12),
Text(
"+${_loginPageCtr.selectedCountryCodeId['country_id']}"),
],
),
);
},
),
const SizedBox(width: 6),
SizedBox(
height: 24,
child: VerticalDivider(
color: theme.colorScheme.outline.withValues(alpha: 0.5),
),
const SizedBox(width: 12),
PopupMenuButton<Map<String, dynamic>>(
padding: EdgeInsets.zero,
tooltip: '选择国际冠码,'
'当前为${_loginPageCtr.selectedCountryCodeId['cname']}'
'+${_loginPageCtr.selectedCountryCodeId['country_id']}',
//position: PopupMenuPosition.under,
onSelected: (Map<String, dynamic> type) {},
itemBuilder: (BuildContext context) => Constants
.internationalDialingPrefix
.map((Map<String, dynamic> item) {
return PopupMenuItem<Map<String, dynamic>>(
onTap: () => setState(() {
_loginPageCtr.selectedCountryCodeId = item;
}),
value: item,
child: Row(children: [
Text(item['cname']),
const Spacer(),
Text("+${item['country_id']}")
]),
);
}).toList(),
child: Text(
"+${_loginPageCtr.selectedCountryCodeId['country_id']}"),
),
const SizedBox(width: 6),
SizedBox(
height: 24,
child: VerticalDivider(
color: theme.colorScheme.outline.withValues(alpha: 0.5),
),
),
const SizedBox(width: 6),
Expanded(
child: TextField(
),
const SizedBox(width: 6),
Expanded(
child: TextField(
controller: _loginPageCtr.telTextController,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
@@ -386,45 +412,51 @@ class _LoginPageState extends State<LoginPage> {
icon: const Icon(Icons.clear),
),
),
)),
],
),
)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: DecoratedBox(
decoration: UnderlineTabIndicator(
borderSide: BorderSide(
color: theme.colorScheme.outline.withValues(alpha: 0.4)),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _loginPageCtr.smsCodeTextController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.sms_outlined),
border: InputBorder.none,
labelText: '验证码',
),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
],
),
),
Obx(() => TextButton.icon(
onPressed: _loginPageCtr.smsSendCooldown > 0
? null
: _loginPageCtr.sendSmsCode,
icon: const Icon(Icons.send),
label: Text(_loginPageCtr.smsSendCooldown > 0
? '等待${_loginPageCtr.smsSendCooldown}'
: '获取验证码'),
)),
],
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: DecoratedBox(
decoration: UnderlineTabIndicator(
borderSide: BorderSide(
color: theme.colorScheme.outline.withValues(alpha: 0.4),
),
)),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _loginPageCtr.smsCodeTextController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.sms_outlined),
border: InputBorder.none,
labelText: '验证码',
),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
],
),
),
Obx(
() => TextButton.icon(
onPressed: _loginPageCtr.smsSendCooldown > 0
? null
: _loginPageCtr.sendSmsCode,
icon: const Icon(Icons.send),
label: Text(_loginPageCtr.smsSendCooldown > 0
? '等待${_loginPageCtr.smsSendCooldown}'
: '获取验证码'),
),
),
],
),
),
),
const SizedBox(height: 20),
OutlinedButton.icon(
onPressed: _loginPageCtr.loginBySmsCode,
@@ -433,15 +465,17 @@ class _LoginPageState extends State<LoginPage> {
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
'手机号仅用于 bilibili 官方发送验证码与登录接口,不予保存;\n'
'本地仅存储登录凭证。\n'
'请务必在 PiliPlus 开源仓库等可信渠道下载安装。',
textAlign: TextAlign.center,
style: theme.textTheme.labelSmall!.copyWith(
color:
theme.colorScheme.onSurface.withValues(alpha: 0.4)))),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
'手机号仅用于 bilibili 官方发送验证码与登录接口,不予保存;\n'
'本地仅存储登录凭证。\n'
'请务必在 PiliPlus 开源仓库等可信渠道下载安装。',
textAlign: TextAlign.center,
style: theme.textTheme.labelSmall!.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
),
),
],
);
}

View File

@@ -292,7 +292,7 @@ class _MatchInfoPageState extends State<MatchInfoPage> {
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: () => _controller.queryBySort(),
onPressed: _controller.queryBySort,
icon: Icon(
Icons.sort,
size: 16,

View File

@@ -83,7 +83,7 @@ class MineController extends GetxController {
}
}
static void onChangeAnonymity(BuildContext context) {
static void onChangeAnonymity() {
if (Accounts.account.isEmpty) {
SmartDialog.showToast('请先登录');
return;

View File

@@ -33,20 +33,21 @@ class _MinePageState extends State<MinePage> {
style: theme.textTheme.titleMedium,
),
const Spacer(),
IconButton(
iconSize: 40.0,
padding: const EdgeInsets.all(8),
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
tooltip: "${MineController.anonymity.value ? '退出' : '进入'}无痕模式",
onPressed: () {
MineController.onChangeAnonymity(context);
setState(() {});
Obx(
() {
return IconButton(
iconSize: 40.0,
padding: const EdgeInsets.all(8),
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
tooltip: "${MineController.anonymity.value ? '退出' : '进入'}无痕模式",
onPressed: MineController.onChangeAnonymity,
icon: MineController.anonymity.value
? const Icon(MdiIcons.incognito, size: 24)
: const Icon(MdiIcons.incognitoOff, size: 24),
);
},
icon: MineController.anonymity.value
? const Icon(MdiIcons.incognito, size: 24)
: const Icon(MdiIcons.incognitoOff, size: 24),
),
Obx(
() {

View File

@@ -20,31 +20,31 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
late double longPressSpeedDefault;
late List<double> speedList;
late bool enableAutoLongPressSpeed;
List<Map<dynamic, dynamic>> sheetMenu = [
{
'id': 1,
'title': '设置为默认倍速',
'leading': const Icon(
List<({int id, String title, Icon icon})> sheetMenu = [
(
id: 1,
title: '设置为默认倍速',
icon: const Icon(
Icons.speed,
size: 21,
),
},
{
'id': 2,
'title': '设置为默认长按倍速',
'leading': const Icon(
),
(
id: 2,
title: '设置为默认长按倍速',
icon: const Icon(
Icons.speed_sharp,
size: 21,
),
},
{
'id': -1,
'title': '删除该项',
'leading': const Icon(
),
(
id: -1,
title: '删除该项',
icon: const Icon(
Icons.delete_outline,
size: 21,
),
},
),
];
Box get video => GStorage.video;
@@ -62,13 +62,6 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
speedList = GStorage.speedList;
enableAutoLongPressSpeed = GStorage.setting
.get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false);
if (enableAutoLongPressSpeed) {
Map newItem = sheetMenu[1];
newItem['show'] = false;
setState(() {
sheetMenu[1] = newItem;
});
}
}
// 添加自定义倍速
@@ -147,16 +140,18 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
const SizedBox(height: 10),
...sheetMenu.map(
(item) => ListTile(
enabled:
enableAutoLongPressSpeed && item.id == 2 ? false : true,
onTap: () {
Navigator.pop(context);
menuAction(index, item['id']);
Get.back();
menuAction(index, item.id);
},
minLeadingWidth: 0,
iconColor: theme.colorScheme.onSurface,
leading: item['leading'],
leading: item.icon,
title: Text(
item['title'],
style: theme.textTheme.titleSmall,
item.title,
style: const TextStyle(fontSize: 14),
),
),
),
@@ -233,14 +228,8 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
subtitle: '根据默认倍速长按时自动双倍',
setKey: SettingBoxKey.enableAutoLongPressSpeed,
defaultVal: enableAutoLongPressSpeed,
onChanged: (val) {
Map newItem = sheetMenu[1];
val ? newItem['show'] = false : newItem['show'] = true;
setState(() {
sheetMenu[1] = newItem;
enableAutoLongPressSpeed = val;
});
},
onChanged: (val) =>
setState(() => enableAutoLongPressSpeed = val),
),
if (!enableAutoLongPressSpeed)
ListTile(

View File

@@ -1480,7 +1480,7 @@ List<SettingsModel> get privacySettings => [
SettingsModel(
settingsType: SettingsType.normal,
onTap: (setState) {
MineController.onChangeAnonymity(Get.context!);
MineController.onChangeAnonymity();
setState();
},
leading: const Icon(Icons.privacy_tip_outlined),

View File

@@ -74,59 +74,63 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
Widget _blockLimitItem(
ThemeData theme, TextStyle titleStyle, TextStyle subTitleStyle) =>
ListTile(
dense: true,
onTap: () {
_textController.text = _blockLimit.toString();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('最短片段时长', style: titleStyle),
content: TextFormField(
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
controller: _textController,
autofocus: true,
decoration: const InputDecoration(suffixText: 's'),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')),
],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: theme.colorScheme.outline,
),
Builder(
builder: (context) {
return ListTile(
dense: true,
onTap: () {
_textController.text = _blockLimit.toString();
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text('最短片段时长', style: titleStyle),
content: TextFormField(
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
controller: _textController,
autofocus: true,
decoration: const InputDecoration(suffixText: 's'),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')),
],
),
),
TextButton(
onPressed: () {
Get.back();
_blockLimit = max(
0.0, double.tryParse(_textController.text) ?? 0.0);
setting.put(SettingBoxKey.blockLimit, _blockLimit);
setState(() {});
},
child: const Text('确定'),
)
],
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: theme.colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
_blockLimit = max(0.0,
double.tryParse(_textController.text) ?? 0.0);
setting.put(SettingBoxKey.blockLimit, _blockLimit);
(context as Element).markNeedsBuild();
},
child: const Text('确定'),
)
],
);
},
);
},
title: Text('最短片段时长', style: titleStyle),
subtitle: Text(
'忽略短于此时长的片段',
style: subTitleStyle,
),
trailing: Text(
'${_blockLimit}s',
style: const TextStyle(fontSize: 13),
),
);
},
title: Text('最短片段时长', style: titleStyle),
subtitle: Text(
'忽略短于此时长的片段',
style: subTitleStyle,
),
trailing: Text(
'${_blockLimit}s',
style: const TextStyle(fontSize: 13),
),
);
Widget _aboudItem(TextStyle titleStyle, TextStyle subTitleStyle) => ListTile(
@@ -138,230 +142,250 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
Widget _userIdItem(
ThemeData theme, TextStyle titleStyle, TextStyle subTitleStyle) =>
ListTile(
dense: true,
title: Text('用户ID', style: titleStyle),
subtitle: Text(_userId, style: subTitleStyle),
onTap: () {
final key = GlobalKey<FormState>();
_textController.text = _userId;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('用户ID', style: titleStyle),
content: Form(
key: key,
child: TextFormField(
minLines: 1,
maxLines: 4,
autofocus: true,
controller: _textController,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\d]+')),
],
validator: (value) {
if ((value?.length ?? -1) < 30) {
return '用户ID要求至少为30个字符长度的纯字符串';
}
return null;
},
),
),
actions: [
TextButton(
onPressed: () {
Get.back();
_userId = const Uuid().v4().replaceAll('-', '');
setting.put(SettingBoxKey.blockUserID, _userId);
setState(() {});
},
child: const Text('随机'),
),
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: theme.colorScheme.outline,
Builder(
builder: (context) {
return ListTile(
dense: true,
title: Text('用户ID', style: titleStyle),
subtitle: Text(_userId, style: subTitleStyle),
onTap: () {
final key = GlobalKey<FormState>();
_textController.text = _userId;
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text('用户ID', style: titleStyle),
content: Form(
key: key,
child: TextFormField(
minLines: 1,
maxLines: 4,
autofocus: true,
controller: _textController,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z\d]+')),
],
validator: (value) {
if ((value?.length ?? -1) < 30) {
return '用户ID要求至少为30个字符长度的纯字符串';
}
return null;
},
),
),
),
TextButton(
onPressed: () {
if (key.currentState?.validate() == true) {
Get.back();
_userId = _textController.text;
setting.put(SettingBoxKey.blockUserID, _userId);
setState(() {});
}
},
child: const Text('确定'),
)
],
actions: [
TextButton(
onPressed: () {
Get.back();
_userId = const Uuid().v4().replaceAll('-', '');
setting.put(SettingBoxKey.blockUserID, _userId);
(context as Element).markNeedsBuild();
},
child: const Text('随机'),
),
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: theme.colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
if (key.currentState?.validate() == true) {
Get.back();
_userId = _textController.text;
setting.put(SettingBoxKey.blockUserID, _userId);
(context as Element).markNeedsBuild();
}
},
child: const Text('确定'),
)
],
);
},
);
},
);
},
);
void _updateBlockToast() {
_blockToast = !_blockToast;
setting.put(SettingBoxKey.blockToast, _blockToast);
setState(() {});
}
Widget _blockToastItem(TextStyle titleStyle) => Builder(
builder: (context) {
void update() {
_blockToast = !_blockToast;
setting.put(SettingBoxKey.blockToast, _blockToast);
(context as Element).markNeedsBuild();
}
void _updateBlockTrack() {
_blockTrack = !_blockTrack;
setting.put(SettingBoxKey.blockTrack, _blockTrack);
setState(() {});
}
Widget _blockToastItem(TextStyle titleStyle) => ListTile(
dense: true,
onTap: _updateBlockToast,
title: Text(
'显示跳过Toast',
style: titleStyle,
),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((states) {
if (states.isNotEmpty && states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: _blockToast,
onChanged: (val) {
_updateBlockToast();
},
),
));
return ListTile(
dense: true,
onTap: update,
title: Text(
'显示跳过Toast',
style: titleStyle,
),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: _blockToast,
onChanged: (val) => update(),
),
),
);
},
);
Widget _blockTrackItem(TextStyle titleStyle, TextStyle subTitleStyle) =>
ListTile(
dense: true,
onTap: _updateBlockTrack,
title: Text(
'跳过次数统计跟踪',
style: titleStyle,
),
subtitle: Text(
// from origin extension
'此功能追踪您跳过了哪些片段,让用户知道他们提交的片段帮助了多少人。同时点赞会作为依据,确保垃圾信息不会污染数据库。在您每次跳过片段时,我们都会向服务器发送一条消息。希望大家开启此项设置,以便得到更准确的统计数据。:)',
style: subTitleStyle,
),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((states) {
if (states.isNotEmpty && states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: _blockTrack,
onChanged: (val) {
_updateBlockTrack();
},
),
));
Builder(
builder: (context) {
void update() {
_blockTrack = !_blockTrack;
setting.put(SettingBoxKey.blockTrack, _blockTrack);
(context as Element).markNeedsBuild();
}
return ListTile(
dense: true,
onTap: update,
title: Text(
'跳过次数统计跟踪',
style: titleStyle,
),
subtitle: Text(
// from origin extension
'此功能追踪您跳过了哪些片段,让用户知道他们提交的片段帮助了多少人。同时点赞会作为依据,确保垃圾信息不会污染数据库。在您每次跳过片段时,我们都会向服务器发送一条消息。希望大家开启此项设置,以便得到更准确的统计数据。:)',
style: subTitleStyle,
),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: _blockTrack,
onChanged: (val) => update(),
),
));
},
);
Widget _blockServerItem(
ThemeData theme, TextStyle titleStyle, TextStyle subTitleStyle) =>
ListTile(
dense: true,
onTap: () {
_textController.text = _blockServer;
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('服务器地址', style: titleStyle),
content: TextFormField(
keyboardType: TextInputType.url,
controller: _textController,
autofocus: true,
),
actions: [
TextButton(
onPressed: () {
Get.back();
_blockServer = HttpString.sponsorBlockBaseUrl;
setting.put(SettingBoxKey.blockServer, _blockServer);
Request.accountManager.blockServer = _blockServer;
setState(() {});
},
child: const Text('重置'),
),
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: theme.colorScheme.outline,
),
Builder(
builder: (context) {
return ListTile(
dense: true,
onTap: () {
_textController.text = _blockServer;
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text('服务器地址', style: titleStyle),
content: TextFormField(
keyboardType: TextInputType.url,
controller: _textController,
autofocus: true,
),
),
TextButton(
onPressed: () {
Get.back();
_blockServer = _textController.text;
setting.put(SettingBoxKey.blockServer, _blockServer);
Request.accountManager.blockServer = _blockServer;
setState(() {});
},
child: const Text('确定'),
)
],
actions: [
TextButton(
onPressed: () {
Get.back();
_blockServer = HttpString.sponsorBlockBaseUrl;
setting.put(SettingBoxKey.blockServer, _blockServer);
Request.accountManager.blockServer = _blockServer;
(context as Element).markNeedsBuild();
},
child: const Text('重置'),
),
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: theme.colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
_blockServer = _textController.text;
setting.put(SettingBoxKey.blockServer, _blockServer);
Request.accountManager.blockServer = _blockServer;
(context as Element).markNeedsBuild();
},
child: const Text('确定'),
)
],
);
},
);
},
title: Text(
'服务器地址',
style: titleStyle,
),
subtitle: Text(
_blockServer,
style: subTitleStyle,
),
);
},
title: Text(
'服务器地址',
style: titleStyle,
),
subtitle: Text(
_blockServer,
style: subTitleStyle,
),
);
Widget _serverStatusItem(ThemeData theme, TextStyle titleStyle) => ListTile(
dense: true,
onTap: () {
setState(() {
_serverStatus = null;
});
_checkServerStatus();
Widget _serverStatusItem(ThemeData theme, TextStyle titleStyle) => Builder(
builder: (context) {
return ListTile(
dense: true,
onTap: () {
_serverStatus = null;
(context as Element).markNeedsBuild();
_checkServerStatus();
},
title: Text('服务器状态', style: titleStyle),
trailing: Text(
_serverStatus == null
? '——'
: _serverStatus == true
? '正常'
: '错误',
style: TextStyle(
fontSize: 13,
color: _serverStatus == null
? null
: _serverStatus == true
? theme.colorScheme.primary
: theme.colorScheme.error,
),
),
);
},
title: Text('服务器状态', style: titleStyle),
trailing: Text(
_serverStatus == null
? '——'
: _serverStatus == true
? '正常'
: '错误',
style: TextStyle(
fontSize: 13,
color: _serverStatus == null
? null
: _serverStatus == true
? theme.colorScheme.primary
: theme.colorScheme.error,
),
),
);
void onSelectColor(int index) {
void onSelectColor(BuildContext context, int index, Color color,
Pair<SegmentType, SkipType> item) {
showDialog(
context: context,
builder: (context) => AlertDialog(
builder: (_) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 16),
title: Text.rich(
@@ -378,28 +402,28 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index],
color: color,
),
),
style: const TextStyle(fontSize: 13, height: 1),
),
TextSpan(
text: ' ${_blockSettings[index].first.title}',
text: ' ${item.first.title}',
style: const TextStyle(fontSize: 13, height: 1),
),
],
),
),
content: SlideColorPicker(
color: _blockColor[index],
color: color,
callback: (Color? color) {
_blockColor[index] = color ?? _blockSettings[index].first.color;
_blockColor[index] = color ?? item.first.color;
setting.put(
SettingBoxKey.blockColor,
_blockColor
.map((item) => item.value.toRadixString(16).substring(2))
.toList());
setState(() {});
(context as Element).markNeedsBuild();
},
),
),
@@ -447,95 +471,8 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
dividerL,
SliverList.separated(
itemCount: _blockSettings.length,
itemBuilder: (context, index) => ListTile(
dense: true,
enabled: _blockSettings[index].second != SkipType.disable,
onTap: () => onSelectColor(index),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index],
),
),
style: const TextStyle(fontSize: 14, height: 1),
),
TextSpan(
text: ' ${_blockSettings[index].first.title}',
style: const TextStyle(fontSize: 14, height: 1),
),
],
),
),
PopupMenuButton(
initialValue: _blockSettings[index].second,
onSelected: (item) {
_blockSettings[index].second = item;
setting.put(
SettingBoxKey.blockSettings,
_blockSettings
.map((item) => item.second.index)
.toList());
setState(() {});
},
itemBuilder: (context) => SkipType.values
.map((item) => PopupMenuItem<SkipType>(
value: item,
child: Text(item.title),
))
.toList(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_blockSettings[index].second.title,
style: TextStyle(
height: 1,
fontSize: 14,
color: _blockSettings[index].second ==
SkipType.disable
? theme.colorScheme.outline
.withValues(alpha: 0.7)
: theme.colorScheme.secondary,
),
strutStyle: const StrutStyle(height: 1, leading: 0),
),
Icon(
MdiIcons.unfoldMoreHorizontal,
size: MediaQuery.textScalerOf(context).scale(14),
color:
_blockSettings[index].second == SkipType.disable
? theme.colorScheme.outline
.withValues(alpha: 0.7)
: theme.colorScheme.secondary,
),
],
),
),
),
],
),
subtitle: Text(
_blockSettings[index].first.description,
style: TextStyle(
fontSize: 12,
color: _blockSettings[index].second == SkipType.disable
? null
: theme.colorScheme.outline,
),
),
),
itemBuilder: (context, index) =>
_buildItem(theme, index, _blockSettings[index]),
separatorBuilder: (context, index) => divider,
),
dividerL,
@@ -555,4 +492,101 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
),
);
}
Widget _buildItem(
ThemeData theme, int index, Pair<SegmentType, SkipType> item) {
return Builder(
builder: (context) {
Color color = _blockColor[index];
return ListTile(
dense: true,
enabled: item.second != SkipType.disable,
onTap: () => onSelectColor(context, index, color, item),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
),
style: const TextStyle(fontSize: 14, height: 1),
),
TextSpan(
text: ' ${item.first.title}',
style: const TextStyle(fontSize: 14, height: 1),
),
],
),
),
Builder(
builder: (context) {
return PopupMenuButton(
initialValue: item.second,
onSelected: (e) {
item.second = e;
setting.put(SettingBoxKey.blockSettings,
_blockSettings.map((e) => e.second.index).toList());
(context as Element).markNeedsBuild();
},
itemBuilder: (context) => SkipType.values
.map((item) => PopupMenuItem<SkipType>(
value: item,
child: Text(item.title),
))
.toList(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.second.title,
style: TextStyle(
height: 1,
fontSize: 14,
color: item.second == SkipType.disable
? theme.colorScheme.outline
.withValues(alpha: 0.7)
: theme.colorScheme.secondary,
),
strutStyle: const StrutStyle(height: 1, leading: 0),
),
Icon(
MdiIcons.unfoldMoreHorizontal,
size: MediaQuery.textScalerOf(context).scale(14),
color: item.second == SkipType.disable
? theme.colorScheme.outline
.withValues(alpha: 0.7)
: theme.colorScheme.secondary,
),
],
),
),
);
},
),
],
),
subtitle: Text(
item.first.description,
style: TextStyle(
fontSize: 12,
color: item.second == SkipType.disable
? null
: theme.colorScheme.outline,
),
),
);
},
);
}
}

View File

@@ -47,7 +47,6 @@ class ActionItemState extends State<ActionItem>
late final _isThumbsUp = widget.semanticsLabel == '点赞';
late int _lastTime;
late bool _hideCircle = false;
Timer? _timer;
void _startLongPress() {
@@ -100,15 +99,12 @@ class ActionItemState extends State<ActionItem>
}
void listener() {
setState(() {
_hideCircle = controller?.value == 1;
if (_hideCircle) {
controller?.reset();
if (_isThumbsUp) {
widget.onLongPress?.call();
}
if (controller!.value == 1) {
controller!.reset();
if (_isThumbsUp) {
widget.onLongPress?.call();
}
});
}
}
void cancelTimer() {
@@ -157,12 +153,15 @@ class ActionItemState extends State<ActionItem>
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
if (widget.needAnim && !_hideCircle)
CustomPaint(
size: const Size(28, 28),
painter: _ArcPainter(
color: theme.colorScheme.primary,
sweepAngle: _animation!.value,
if (widget.needAnim)
AnimatedBuilder(
animation: _animation!,
builder: (context, child) => CustomPaint(
size: const Size(28, 28),
painter: _ArcPainter(
color: theme.colorScheme.primary,
sweepAngle: _animation!.value,
),
),
)
else

View File

@@ -267,9 +267,7 @@ class _PayCoinsPageState extends State<PayCoinsPage>
physics: const ClampingScrollPhysics(),
itemCount: widget.copyright == 1 ? 2 : 1,
controller: _controller,
onPageChanged: (index) => setState(() {
_scale();
}),
onPageChanged: (index) => setState(_scale),
itemBuilder: (context, index) {
return ListenableBuilder(
listenable: _controller,

View File

@@ -131,23 +131,33 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
runSpacing: 8,
spacing: 16,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: segmentWidget(
theme,
isFirst: true,
index: index,
),
Builder(
builder: (context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: segmentWidget(
context,
theme,
isFirst: true,
index: index,
),
);
},
),
if (list![index].category !=
SegmentType.poi_highlight)
Row(
mainAxisSize: MainAxisSize.min,
children: segmentWidget(
theme,
isFirst: false,
index: index,
),
Builder(
builder: (context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: segmentWidget(
context,
theme,
isFirst: false,
index: index,
),
);
},
),
],
),
@@ -161,72 +171,86 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
mainAxisSize: MainAxisSize.min,
children: [
const Text('分类: '),
PopupMenuButton<SegmentType>(
initialValue: list![index].category,
onSelected: (item) {
list![index].category = item;
List<ActionType> constraintList =
item.toActionType;
if (!constraintList.contains(
list![index].actionType)) {
list![index].actionType =
constraintList.first;
}
switch (item) {
case SegmentType.poi_highlight:
updateSegment(
isFirst: false,
index: index,
value: list![index]
.segment
.first,
);
break;
case SegmentType.exclusive_access:
updateSegment(
isFirst: true,
index: index,
value: 0,
);
break;
case _:
}
setState(() {});
Builder(
builder: (context) {
return PopupMenuButton<SegmentType>(
initialValue:
list![index].category,
onSelected: (item) {
list![index].category = item;
List<ActionType>
constraintList =
item.toActionType;
if (!constraintList.contains(
list![index].actionType)) {
list![index].actionType =
constraintList.first;
}
switch (item) {
case SegmentType
.poi_highlight:
updateSegment(
isFirst: false,
index: index,
value: list![index]
.segment
.first,
);
break;
case SegmentType
.exclusive_access:
updateSegment(
isFirst: true,
index: index,
value: 0,
);
break;
default:
}
(context as Element)
.markNeedsBuild();
},
itemBuilder: (context) =>
SegmentType.values
.map((item) =>
PopupMenuItem<
SegmentType>(
value: item,
child: Text(
item.title),
))
.toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
list![index].category.title,
style: TextStyle(
height: 1,
fontSize: 14,
color: theme.colorScheme
.secondary,
),
strutStyle:
const StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons
.unfoldMoreHorizontal,
size:
MediaQuery.textScalerOf(
context)
.scale(14),
color: theme
.colorScheme.secondary,
),
],
),
);
},
itemBuilder: (context) => SegmentType
.values
.map((item) =>
PopupMenuItem<SegmentType>(
value: item,
child: Text(item.title),
))
.toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
list![index].category.title,
style: TextStyle(
height: 1,
fontSize: 14,
color: theme
.colorScheme.secondary,
),
strutStyle: const StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons.unfoldMoreHorizontal,
size: MediaQuery.textScalerOf(
context)
.scale(14),
color:
theme.colorScheme.secondary,
),
],
),
),
],
),
@@ -234,60 +258,64 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
mainAxisSize: MainAxisSize.min,
children: [
const Text('行为类别: '),
PopupMenuButton<ActionType>(
initialValue: list![index].actionType,
onSelected: (item) {
list![index].actionType = item;
if (item == ActionType.full) {
updateSegment(
isFirst: true,
index: index,
value: 0,
);
}
setState(() {});
},
itemBuilder: (context) => ActionType
.values
.map(
(item) =>
PopupMenuItem<ActionType>(
enabled: list![index]
.category
.toActionType
.contains(item),
value: item,
child: Text(item.title),
Builder(builder: (context) {
return PopupMenuButton<ActionType>(
initialValue:
list![index].actionType,
onSelected: (item) {
list![index].actionType = item;
if (item == ActionType.full) {
updateSegment(
isFirst: true,
index: index,
value: 0,
);
}
(context as Element)
.markNeedsBuild();
},
itemBuilder: (context) => ActionType
.values
.map(
(item) =>
PopupMenuItem<ActionType>(
enabled: list![index]
.category
.toActionType
.contains(item),
value: item,
child: Text(item.title),
),
)
.toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
list![index].actionType.title,
style: TextStyle(
height: 1,
fontSize: 14,
color: theme
.colorScheme.secondary,
),
strutStyle: const StrutStyle(
height: 1,
leading: 0,
),
),
)
.toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
list![index].actionType.title,
style: TextStyle(
height: 1,
fontSize: 14,
Icon(
MdiIcons.unfoldMoreHorizontal,
size: MediaQuery.textScalerOf(
context)
.scale(14),
color: theme
.colorScheme.secondary,
),
strutStyle: const StrutStyle(
height: 1,
leading: 0,
),
),
Icon(
MdiIcons.unfoldMoreHorizontal,
size: MediaQuery.textScalerOf(
context)
.scale(14),
color:
theme.colorScheme.secondary,
),
],
),
),
],
),
);
}),
],
),
],
@@ -419,6 +447,7 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
}
List<Widget> segmentWidget(
BuildContext context,
ThemeData theme, {
required int index,
required bool isFirst,
@@ -436,13 +465,12 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
tooltip: '设为当前',
icon: Icons.my_location,
onPressed: () {
setState(() {
updateSegment(
isFirst: isFirst,
index: index,
value: currentPos,
);
});
updateSegment(
isFirst: isFirst,
index: index,
value: currentPos,
);
(context as Element).markNeedsBuild();
},
),
const SizedBox(width: 5),
@@ -452,13 +480,12 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
tooltip: isFirst ? '视频开头' : '视频结尾',
icon: isFirst ? Icons.first_page : Icons.last_page,
onPressed: () {
setState(() {
updateSegment(
isFirst: isFirst,
index: index,
value: isFirst ? 0 : videoDuration,
);
});
updateSegment(
isFirst: isFirst,
index: index,
value: isFirst ? 0 : videoDuration,
);
(context as Element).markNeedsBuild();
},
),
const SizedBox(width: 5),
@@ -508,13 +535,12 @@ class _PostPanelState extends CommonCollapseSlidePageState<PostPanel> {
duration += split[i] * pow(60, i);
}
if (duration <= videoDuration) {
setState(() {
updateSegment(
isFirst: isFirst,
index: index,
value: duration,
);
});
updateSegment(
isFirst: isFirst,
index: index,
value: duration,
);
(context as Element).markNeedsBuild();
}
} catch (e) {
if (kDebugMode) debugPrint(e.toString());

View File

@@ -22,42 +22,32 @@ class PlayOrPauseButton extends StatefulWidget {
class PlayOrPauseButtonState extends State<PlayOrPauseButton>
with SingleTickerProviderStateMixin {
late final AnimationController animation;
StreamSubscription<bool>? subscription;
late final AnimationController controller;
late final StreamSubscription<bool> subscription;
late Player player;
bool isOpacity = false;
PlPlayerController get plPlayerController => widget.plPlayerController;
@override
void initState() {
super.initState();
player = plPlayerController.videoPlayerController!;
animation = AnimationController(
player = widget.plPlayerController.videoPlayerController!;
controller = AnimationController(
vsync: this,
value: player.state.playing ? 1 : 0,
duration: const Duration(milliseconds: 200),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
subscription ??= player.stream.playing.listen((event) {
if (event) {
animation.forward().whenComplete(() => {isOpacity = true});
subscription = player.stream.playing.listen((playing) {
if (playing) {
controller.forward();
} else {
animation.reverse().whenComplete(() => {isOpacity = false});
controller.reverse();
}
setState(() {});
});
}
@override
void dispose() {
animation.dispose();
subscription?.cancel();
subscription.cancel();
controller.dispose();
super.dispose();
}
@@ -67,6 +57,7 @@ class PlayOrPauseButtonState extends State<PlayOrPauseButton>
width: 42,
height: 34,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
if (player.state.completed) {
await player.seek(Duration.zero);
@@ -77,11 +68,8 @@ class PlayOrPauseButtonState extends State<PlayOrPauseButton>
},
child: Center(
child: AnimatedIcon(
semanticLabel:
plPlayerController.videoPlayerController!.state.playing
? '暂停'
: '播放',
progress: animation,
semanticLabel: player.state.playing ? '暂停' : '播放',
progress: controller,
icon: AnimatedIcons.play_pause,
color: Colors.white,
size: 20,

View File

@@ -45,8 +45,8 @@ class ShutdownTimerService with WidgetsBindingObserver {
return;
}
SmartDialog.showToast("设置 $scheduledExitInMinutes 分钟后定时关闭");
_shutdownTimer = Timer(
Duration(minutes: scheduledExitInMinutes), () => _shutdownDecider());
_shutdownTimer =
Timer(Duration(minutes: scheduledExitInMinutes), _shutdownDecider);
}
void _showTimeUpButPauseDialog() {

View File

@@ -119,8 +119,9 @@ class PageUtils {
onPressed: () {
Get.back();
int choice = int.tryParse(duration) ?? 0;
shutdownTimerService.scheduledExitInMinutes = choice;
shutdownTimerService.startShutdownTimer();
shutdownTimerService
..scheduledExitInMinutes = choice
..startShutdownTimer();
setState(() {});
},
child: const Text('确定'),
@@ -181,59 +182,71 @@ class PageUtils {
),
),
if (!isLive) ...[
ListTile(
dense: true,
onTap: () {
shutdownTimerService.waitForPlayingCompleted =
!shutdownTimerService.waitForPlayingCompleted;
setState(() {});
Builder(
builder: (context) {
return ListTile(
dense: true,
onTap: () {
shutdownTimerService.waitForPlayingCompleted =
!shutdownTimerService.waitForPlayingCompleted;
(context as Element).markNeedsBuild();
},
title: const Text("额外等待视频播放完毕", style: titleStyle),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon:
WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: shutdownTimerService
.waitForPlayingCompleted,
onChanged: (value) {
shutdownTimerService.waitForPlayingCompleted =
value;
(context as Element).markNeedsBuild();
},
),
),
);
},
title: const Text("额外等待视频播放完毕", style: titleStyle),
trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8,
child: Switch(
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.isNotEmpty &&
states.first == WidgetState.selected) {
return const Icon(Icons.done);
}
return null;
}),
value: shutdownTimerService.waitForPlayingCompleted,
onChanged: (value) => setState(() =>
shutdownTimerService.waitForPlayingCompleted =
value),
),
),
),
],
const SizedBox(height: 10),
Row(
children: [
const SizedBox(width: 18),
const Text('倒计时结束:', style: titleStyle),
const Spacer(),
ActionRowLineItem(
onTap: () {
shutdownTimerService.exitApp = false;
setState(() {});
},
text: " 暂停视频 ",
selectStatus: !shutdownTimerService.exitApp,
),
const Spacer(),
ActionRowLineItem(
onTap: () {
shutdownTimerService.exitApp = true;
setState(() {});
},
text: " 退出APP ",
selectStatus: shutdownTimerService.exitApp,
),
const SizedBox(width: 25),
],
Builder(
builder: (context) {
return Row(
children: [
const SizedBox(width: 18),
const Text('倒计时结束:', style: titleStyle),
const Spacer(),
ActionRowLineItem(
onTap: () {
shutdownTimerService.exitApp = false;
(context as Element).markNeedsBuild();
},
text: " 暂停视频 ",
selectStatus: !shutdownTimerService.exitApp,
),
const Spacer(),
ActionRowLineItem(
onTap: () {
shutdownTimerService.exitApp = true;
(context as Element).markNeedsBuild();
},
text: " 退出APP ",
selectStatus: shutdownTimerService.exitApp,
),
const SizedBox(width: 25),
],
);
},
),
],
),