mod: 无障碍语义适配

This commit is contained in:
orz12
2024-02-29 21:00:53 +08:00
parent 19117a041a
commit f8e6ec00f9
65 changed files with 683 additions and 390 deletions

View File

@@ -10,6 +10,7 @@ class PBadge extends StatelessWidget {
final String? size; final String? size;
final String? stack; final String? stack;
final double? fs; final double? fs;
final String? semanticsLabel;
const PBadge({ const PBadge({
super.key, super.key,
@@ -22,6 +23,7 @@ class PBadge extends StatelessWidget {
this.size = 'medium', this.size = 'medium',
this.stack = 'position', this.stack = 'position',
this.fs = 11, this.fs = 11,
this.semanticsLabel,
}); });
@override @override
@@ -68,6 +70,7 @@ class PBadge extends StatelessWidget {
child: Text( child: Text(
text!, text!,
style: TextStyle(fontSize: fs ?? fontSize, color: color), style: TextStyle(fontSize: fs ?? fontSize, color: color),
semanticsLabel: semanticsLabel,
), ),
); );
if (stack == 'position') { if (stack == 'position') {

View File

@@ -1,5 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:PiliPalaX/utils/extension.dart'; import 'package:PiliPalaX/utils/extension.dart';
import 'package:PiliPalaX/utils/global_data.dart'; import 'package:PiliPalaX/utils/global_data.dart';
@@ -20,6 +21,7 @@ class NetworkImgLayer extends StatelessWidget {
// 图片质量 默认1% // 图片质量 默认1%
this.quality, this.quality,
this.origAspectRatio, this.origAspectRatio,
this.semanticsLabel,
}); });
final String? src; final String? src;
@@ -30,6 +32,7 @@ class NetworkImgLayer extends StatelessWidget {
final Duration? fadeInDuration; final Duration? fadeInDuration;
final int? quality; final int? quality;
final double? origAspectRatio; final double? origAspectRatio;
final String? semanticsLabel;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -49,8 +52,7 @@ class NetworkImgLayer extends StatelessWidget {
memCacheWidth = width.cacheSize(context); memCacheWidth = width.cacheSize(context);
// memCacheHeight = height.cacheSize(context); // memCacheHeight = height.cacheSize(context);
} }
Widget res = src != '' && src != null
return src != '' && src != null
? ClipRRect( ? ClipRRect(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
@@ -79,6 +81,13 @@ class NetworkImgLayer extends StatelessWidget {
), ),
) )
: placeholder(context); : placeholder(context);
if (semanticsLabel != null) {
return Semantics(
label: semanticsLabel,
child: res,
);
}
return res;
} }
Widget placeholder(BuildContext context) { Widget placeholder(BuildContext context) {

View File

@@ -41,6 +41,7 @@ class OverlayPop extends StatelessWidget {
borderRadius: borderRadius:
const BorderRadius.all(Radius.circular(20))), const BorderRadius.all(Radius.circular(20))),
child: IconButton( child: IconButton(
tooltip: '关闭',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),

View File

@@ -31,6 +31,7 @@ class StatDanMu extends StatelessWidget {
fontSize: size == 'medium' ? 12 : 11, fontSize: size == 'medium' ? 12 : 11,
color: color, color: color,
), ),
semanticsLabel: '${Utils.numFormat(danmu!)}条弹幕',
) )
], ],
); );

View File

@@ -5,8 +5,9 @@ class StatView extends StatelessWidget {
final String? theme; final String? theme;
final dynamic view; final dynamic view;
final String? size; final String? size;
final String? goto;
const StatView({Key? key, this.theme, this.view, this.size}) const StatView({Key? key, this.theme, this.view, this.size, this.goto})
: super(key: key); : super(key: key);
@override @override
@@ -20,7 +21,9 @@ class StatView extends StatelessWidget {
return Row( return Row(
children: [ children: [
Icon( Icon(
Icons.play_circle_outlined, goto == 'picture'
? Icons.remove_red_eye_outlined
: Icons.play_circle_outlined,
size: 13, size: 13,
color: color, color: color,
), ),
@@ -31,6 +34,8 @@ class StatView extends StatelessWidget {
fontSize: size == 'medium' ? 12 : 11, fontSize: size == 'medium' ? 12 : 11,
color: color, color: color,
), ),
semanticsLabel:
'${Utils.numFormat(view!)}${goto == "picture" ? "浏览" : "播放"}',
), ),
], ],
); );

View File

@@ -38,93 +38,96 @@ class VideoCardH extends StatelessWidget {
final int aid = videoItem.aid; final int aid = videoItem.aid;
final String bvid = videoItem.bvid; final String bvid = videoItem.bvid;
final String heroTag = Utils.makeHeroTag(aid); final String heroTag = Utils.makeHeroTag(aid);
return GestureDetector( return Semantics(
onLongPress: () { label: Utils.videoItemSemantics(videoItem),
if (longPress != null) { excludeSemantics: true,
longPress!(); child: GestureDetector(
} onLongPress: () {
}, if (longPress != null) {
// onLongPressEnd: (details) { longPress!();
// if (longPressEnd != null) { }
// longPressEnd!(); },
// } // onLongPressEnd: (details) {
// }, // if (longPressEnd != null) {
child: InkWell( // longPressEnd!();
onTap: () async { // }
try { // },
final int cid = child: InkWell(
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); onTap: () async {
Get.toNamed('/video?bvid=$bvid&cid=$cid', try {
arguments: {'videoItem': videoItem, 'heroTag': heroTag}); final int cid = videoItem.cid ??
} catch (err) { await SearchHttp.ab2c(aid: aid, bvid: bvid);
SmartDialog.showToast(err.toString()); Get.toNamed('/video?bvid=$bvid&cid=$cid',
} arguments: {'videoItem': videoItem, 'heroTag': heroTag});
}, } catch (err) {
child: Padding( SmartDialog.showToast(err.toString());
padding: const EdgeInsets.fromLTRB( }
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
final double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.textScalerOf(context).scale(1.0)) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic as String,
width: maxWidth,
height: maxHeight,
),
),
PBadge(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
// if (videoItem.rcmdReason != null &&
// videoItem.rcmdReason.content != '')
// pBadge(videoItem.rcmdReason.content, context,
// 6.0, 6.0, null, null),
],
);
},
),
),
VideoContent(
videoItem: videoItem,
source: source,
showOwner: showOwner,
showView: showView,
showDanmaku: showDanmaku,
showPubdate: showPubdate,
)
],
),
);
}, },
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
final double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.textScalerOf(context).scale(1.0)) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic as String,
width: maxWidth,
height: maxHeight,
),
),
PBadge(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
// if (videoItem.rcmdReason != null &&
// videoItem.rcmdReason.content != '')
// pBadge(videoItem.rcmdReason.content, context,
// 6.0, 6.0, null, null),
],
);
},
),
),
VideoContent(
videoItem: videoItem,
source: source,
showOwner: showOwner,
showView: showView,
showDanmaku: showDanmaku,
showPubdate: showPubdate,
)
],
),
);
},
),
),
), ),
), ));
),
);
} }
} }
@@ -185,6 +188,7 @@ class VideoContent extends StatelessWidget {
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface, : Theme.of(context).colorScheme.onSurface,
), ),
semanticsLabel: i['text'] as String,
), ),
] ]
], ],
@@ -235,7 +239,7 @@ class VideoContent extends StatelessWidget {
if (showDanmaku) if (showDanmaku)
StatDanMu( StatDanMu(
theme: 'gray', theme: 'gray',
danmu: videoItem.stat.danmaku as int, danmu: videoItem.stat.danmu as int,
), ),
const Spacer(), const Spacer(),
if (source == 'normal') if (source == 'normal')

View File

@@ -124,57 +124,62 @@ class VideoCardV extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id); String heroTag = Utils.makeHeroTag(videoItem.id);
return Card( return Semantics(
elevation: 0, label: Utils.videoItemSemantics(videoItem),
clipBehavior: Clip.hardEdge, excludeSemantics: true,
margin: EdgeInsets.zero, child: Card(
child: GestureDetector( elevation: 0,
onLongPress: () { clipBehavior: Clip.hardEdge,
if (longPress != null) { margin: EdgeInsets.zero,
longPress!(); child: GestureDetector(
} onLongPress: () {
}, if (longPress != null) {
// onLongPressEnd: (details) { longPress!();
// if (longPressEnd != null) { }
// longPressEnd!(); },
// } // onLongPressEnd: (details) {
// }, // if (longPressEnd != null) {
child: InkWell( // longPressEnd!();
onTap: () async => onPushDetail(heroTag), // }
child: Column( // },
children: [ child: InkWell(
AspectRatio( onTap: () async => onPushDetail(heroTag),
aspectRatio: StyleString.aspectRatio, child: Column(
child: LayoutBuilder(builder: (context, boxConstraints) { children: [
double maxWidth = boxConstraints.maxWidth; AspectRatio(
double maxHeight = boxConstraints.maxHeight; aspectRatio: StyleString.aspectRatio,
return Stack( child: LayoutBuilder(builder: (context, boxConstraints) {
children: [ double maxWidth = boxConstraints.maxWidth;
Hero( double maxHeight = boxConstraints.maxHeight;
tag: heroTag, return Stack(
child: NetworkImgLayer( children: [
src: videoItem.pic, Hero(
width: maxWidth, tag: heroTag,
height: maxHeight, child: NetworkImgLayer(
), src: videoItem.pic,
), width: maxWidth,
if (videoItem.duration > 0) height: maxHeight,
PBadge( ),
bottom: 6, ),
right: 7, if (videoItem.duration > 0)
size: 'small', PBadge(
type: 'gray', bottom: 6,
text: Utils.timeFormat(videoItem.duration), right: 7,
) size: 'small',
], type: 'gray',
); text: Utils.timeFormat(videoItem.duration),
}), // semanticsLabel:
// '时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}',
)
],
);
}),
),
VideoContent(videoItem: videoItem)
],
), ),
VideoContent(videoItem: videoItem) ),
], )),
),
),
),
); );
} }
} }
@@ -195,6 +200,7 @@ class VideoContent extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: Text(videoItem.title, child: Text(videoItem.title,
// semanticsLabel: "${videoItem.title}",
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
@@ -248,6 +254,7 @@ class VideoContent extends StatelessWidget {
flex: 1, flex: 1,
child: Text( child: Text(
videoItem.owner.name, videoItem.owner.name,
// semanticsLabel: "Up主${videoItem.owner.name}",
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
@@ -290,12 +297,14 @@ class VideoStat extends StatelessWidget {
StatView( StatView(
theme: 'gray', theme: 'gray',
view: videoItem.stat.view, view: videoItem.stat.view,
goto: videoItem.goto,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
StatDanMu( if (videoItem.goto != 'picture')
theme: 'gray', StatDanMu(
danmu: videoItem.stat.danmu, theme: 'gray',
), danmu: videoItem.stat.danmu,
),
if (videoItem is RecVideoItemModel) ...<Widget>[ if (videoItem is RecVideoItemModel) ...<Widget>[
const Spacer(), const Spacer(),
RichText( RichText(

View File

@@ -148,6 +148,7 @@ class MyApp extends StatelessWidget {
// 图片缓存 // 图片缓存
// PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20; // PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20;
return GetMaterialApp( return GetMaterialApp(
// showSemanticsDebugger: true,
title: 'PiliPalaX', title: 'PiliPalaX',
theme: ThemeData( theme: ThemeData(
// fontFamily: 'HarmonyOS', // fontFamily: 'HarmonyOS',

View File

@@ -826,15 +826,15 @@ class Like {
class Stat { class Stat {
Stat({ Stat({
this.danmaku, this.danmu,
this.play, this.play,
}); });
String? danmaku; String? danmu;
String? play; String? play;
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
danmaku = json['danmaku']; danmu = json['danmaku'];
play = json['play']; play = json['play'];
} }
} }

View File

@@ -134,15 +134,15 @@ class VListItemModel {
class Stat { class Stat {
Stat({ Stat({
this.view, this.view,
this.danmaku, this.danmu,
}); });
int? view; int? view;
int? danmaku; int? danmu;
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
view = json["play"]; view = json["play"];
danmaku = json['video_review']; danmu = json['video_review'];
} }
} }

View File

@@ -90,7 +90,7 @@ class Stat {
Stat({ Stat({
this.aid, this.aid,
this.view, this.view,
this.danmaku, this.danmu,
this.reply, this.reply,
this.favorite, this.favorite,
this.coin, this.coin,
@@ -105,7 +105,7 @@ class Stat {
int? aid; int? aid;
int? view; int? view;
int? danmaku; int? danmu;
int? reply; int? reply;
int? favorite; int? favorite;
int? coin; int? coin;
@@ -120,7 +120,7 @@ class Stat {
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
aid = json["aid"]; aid = json["aid"];
view = json["view"]; view = json["view"];
danmaku = json['danmaku']; danmu = json['danmaku'];
reply = json["reply"]; reply = json["reply"];
favorite = json["favorite"]; favorite = json["favorite"];
coin = json['coin']; coin = json['coin'];

View File

@@ -98,7 +98,7 @@ class SearchVideoItemModel {
class Stat { class Stat {
Stat({ Stat({
this.view, this.view,
this.danmaku, this.danmu,
this.favorite, this.favorite,
this.reply, this.reply,
this.like, this.like,
@@ -107,7 +107,7 @@ class Stat {
// 播放量 // 播放量
int? view; int? view;
// 弹幕数 // 弹幕数
int? danmaku; int? danmu;
// 收藏数 // 收藏数
int? favorite; int? favorite;
// 评论数 // 评论数
@@ -117,7 +117,7 @@ class Stat {
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
view = json['play']; view = json['play'];
danmaku = json['danmaku']; danmu = json['danmaku'];
favorite = json['favorite']; favorite = json['favorite'];
reply = json['review']; reply = json['review'];
like = json['like']; like = json['like'];

View File

@@ -107,14 +107,14 @@ class FavDetailItemData {
class Stat { class Stat {
Stat({ Stat({
this.view, this.view,
this.danmaku, this.danmu,
}); });
int? view; int? view;
int? danmaku; int? danmu;
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
view = json['play']; view = json['play'];
danmaku = json['danmaku']; danmu = json['danmaku'];
} }
} }

View File

@@ -427,7 +427,7 @@ class Part {
class Stat { class Stat {
int? aid; int? aid;
int? view; int? view;
int? danmaku; int? danmu;
int? reply; int? reply;
int? favorite; int? favorite;
int? coin; int? coin;
@@ -442,7 +442,7 @@ class Stat {
Stat({ Stat({
this.aid, this.aid,
this.view, this.view,
this.danmaku, this.danmu,
this.reply, this.reply,
this.favorite, this.favorite,
this.coin, this.coin,
@@ -462,7 +462,7 @@ class Stat {
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
aid = json["aid"]; aid = json["aid"];
view = json["view"]; view = json["view"];
danmaku = json["danmaku"]; danmu = json["danmaku"];
reply = json["reply"]; reply = json["reply"];
favorite = json["favorite"]; favorite = json["favorite"];
coin = json["coin"]; coin = json["coin"];
@@ -480,7 +480,7 @@ class Stat {
data["aid"] = aid; data["aid"] = aid;
data["view"] = view; data["view"] = view;
data["danmaku"] = danmaku; data["danmaku"] = danmu;
data["reply"] = reply; data["reply"] = reply;
data["favorite"] = favorite; data["favorite"] = favorite;
data["coin"] = coin; data["coin"] = coin;

View File

@@ -50,9 +50,10 @@ class _AboutPageState extends State<AboutPage> {
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150), constraints: const BoxConstraints(maxHeight: 150),
child: Image.asset( child: ExcludeSemantics(
child: Image.asset(
'assets/images/logo/logo_android_2.png', 'assets/images/logo/logo_android_2.png',
), )),
), ),
ListTile( ListTile(
title: Text('PiliPalaX', title: Text('PiliPalaX',
@@ -65,6 +66,7 @@ class _AboutPageState extends State<AboutPage> {
'使用Flutter开发的哔哩哔哩第三方客户端', '使用Flutter开发的哔哩哔哩第三方客户端',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.outline), style: TextStyle(color: Theme.of(context).colorScheme.outline),
semanticsLabel: '与你一起,发现不一样的世界',
), ),
), ),
Obx( Obx(
@@ -156,7 +158,7 @@ class _AboutPageState extends State<AboutPage> {
var cleanStatus = await CacheManage().clearCacheAll(); var cleanStatus = await CacheManage().clearCacheAll();
if (cleanStatus) { if (cleanStatus) {
getCacheSize(); getCacheSize();
SmartDialog.showToast('清除成功'); SmartDialog.showToast('清除成功');
} }
}, },
title: const Text('清除缓存'), title: const Text('清除缓存'),
@@ -207,7 +209,7 @@ class AboutController extends GetxController {
String buildNumber = currentInfo.buildNumber; String buildNumber = currentInfo.buildNumber;
//if is android //if is android
if (Platform.isAndroid) { if (Platform.isAndroid) {
buildNumber = buildNumber.substring(0,buildNumber.length - 1); buildNumber = buildNumber.substring(0, buildNumber.length - 1);
} }
currentVersion.value = "${currentInfo.version}+$buildNumber"; currentVersion.value = "${currentInfo.version}+$buildNumber";
} }
@@ -265,6 +267,7 @@ class AboutController extends GetxController {
), ),
); );
} }
// 问题反馈 // 问题反馈
feedback() { feedback() {
launchUrl( launchUrl(

View File

@@ -15,6 +15,7 @@ import 'package:PiliPalaX/pages/video/detail/introduction/widgets/action_row_ite
import 'package:PiliPalaX/pages/video/detail/introduction/widgets/fav_panel.dart'; import 'package:PiliPalaX/pages/video/detail/introduction/widgets/fav_panel.dart';
import 'package:PiliPalaX/utils/feed_back.dart'; import 'package:PiliPalaX/utils/feed_back.dart';
import '../../../utils/utils.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/intro_detail.dart'; import 'widgets/intro_detail.dart';
@@ -192,6 +193,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
src: !widget.loadingStatus src: !widget.loadingStatus
? widget.bangumiDetail!.cover! ? widget.bangumiDetail!.cover!
: bangumiItem!.cover!, : bangumiItem!.cover!,
semanticsLabel: '封面',
), ),
if (bangumiItem != null && if (bangumiItem != null &&
bangumiItem!.rating != null) bangumiItem!.rating != null)
@@ -235,6 +237,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
width: 34, width: 34,
height: 34, height: 34,
child: IconButton( child: IconButton(
tooltip: '收藏',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all( padding: MaterialStateProperty.all(
EdgeInsets.zero), EdgeInsets.zero),
@@ -394,18 +397,19 @@ class _BangumiInfoState extends State<BangumiInfo> {
crossAxisCount: 5, crossAxisCount: 5,
childAspectRatio: 1.25, childAspectRatio: 1.25,
children: <Widget>[ children: <Widget>[
Obx( Obx(() => ActionItem(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp), icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: onTap:
handleState(bangumiIntroController.actionLikeVideo), handleState(bangumiIntroController.actionLikeVideo),
selectStatus: bangumiIntroController.hasLike.value, selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: false, loadingStatus: false,
semanticsLabel: '点赞',
text: !widget.loadingStatus text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['likes']!.toString() ? Utils.numFormat(
: bangumiItem!.stat!['likes']!.toString()), widget.bangumiDetail!.stat!['likes']!)
), : Utils.numFormat(bangumiItem!.stat!['likes']!),
)),
Obx( Obx(
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.b),
@@ -414,9 +418,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
handleState(bangumiIntroController.actionCoinVideo), handleState(bangumiIntroController.actionCoinVideo),
selectStatus: bangumiIntroController.hasCoin.value, selectStatus: bangumiIntroController.hasCoin.value,
loadingStatus: false, loadingStatus: false,
semanticsLabel: '投币',
text: !widget.loadingStatus text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['coins']!.toString() ? Utils.numFormat(widget.bangumiDetail!.stat!['coins']!)
: bangumiItem!.stat!['coins']!.toString()), : Utils.numFormat(bangumiItem!.stat!['coins']!)),
), ),
Obx( Obx(
() => ActionItem( () => ActionItem(
@@ -425,9 +430,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
onTap: () => showFavBottomSheet(), onTap: () => showFavBottomSheet(),
selectStatus: bangumiIntroController.hasFav.value, selectStatus: bangumiIntroController.hasFav.value,
loadingStatus: false, loadingStatus: false,
semanticsLabel: '收藏',
text: !widget.loadingStatus text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['favorite']!.toString() ? Utils.numFormat(widget.bangumiDetail!.stat!['favorite']!)
: bangumiItem!.stat!['favorite']!.toString()), : Utils.numFormat(bangumiItem!.stat!['favorite']!)),
), ),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.comment), icon: const Icon(FontAwesomeIcons.comment),
@@ -435,18 +441,20 @@ class _BangumiInfoState extends State<BangumiInfo> {
onTap: () => videoDetailCtr.tabCtr.animateTo(1), onTap: () => videoDetailCtr.tabCtr.animateTo(1),
selectStatus: false, selectStatus: false,
loadingStatus: false, loadingStatus: false,
semanticsLabel: '评论',
text: !widget.loadingStatus text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['reply']!.toString() ? Utils.numFormat(widget.bangumiDetail!.stat!['reply']!)
: bangumiItem!.stat!['reply']!.toString(), : Utils.numFormat(bangumiItem!.stat!['reply']!),
), ),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare), icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => bangumiIntroController.actionShareVideo(), onTap: () => bangumiIntroController.actionShareVideo(),
selectStatus: false, selectStatus: false,
loadingStatus: false, loadingStatus: false,
semanticsLabel: '转发',
text: !widget.loadingStatus text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['share']!.toString() ? Utils.numFormat(widget.bangumiDetail!.stat!['share']!)
: bangumiItem!.stat!['share']!.toString()), : Utils.numFormat(bangumiItem!.stat!['share']!)),
], ],
), ),
), ),

View File

@@ -98,6 +98,7 @@ class _BangumiPageState extends State<BangumiPage>
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
IconButton( IconButton(
tooltip: '刷新',
onPressed: () { onPressed: () {
setState(() { setState(() {
_futureBuilderFutureFollow = _futureBuilderFutureFollow =

View File

@@ -95,6 +95,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
IconButton( IconButton(
tooltip: '关闭',
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),

View File

@@ -25,12 +25,15 @@ class _ActionPanelState extends State<ActionPanel> {
late ModuleStatModel stat; late ModuleStatModel stat;
bool isProcessing = false; bool isProcessing = false;
void Function()? handleState(Future Function() action) { void Function()? handleState(Future Function() action) {
return isProcessing ? null : () async { return isProcessing
setState(() => isProcessing = true); ? null
await action(); : () async {
setState(() => isProcessing = false); setState(() => isProcessing = true);
}; await action();
setState(() => isProcessing = false);
};
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -83,12 +86,13 @@ class _ActionPanelState extends State<ActionPanel> {
icon: const Icon( icon: const Icon(
FontAwesomeIcons.shareFromSquare, FontAwesomeIcons.shareFromSquare,
size: 16, size: 16,
semanticLabel: "转发",
), ),
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline, foregroundColor: Theme.of(context).colorScheme.outline,
), ),
label: Text(stat.forward!.count ?? '转发'), label: Text(stat.forward!.count ?? ''),
), ),
), ),
Expanded( Expanded(
@@ -99,12 +103,13 @@ class _ActionPanelState extends State<ActionPanel> {
icon: const Icon( icon: const Icon(
FontAwesomeIcons.comment, FontAwesomeIcons.comment,
size: 16, size: 16,
semanticLabel: "评论",
), ),
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline, foregroundColor: Theme.of(context).colorScheme.outline,
), ),
label: Text(stat.comment!.count ?? '评论'), label: Text(stat.comment!.count ?? ''),
), ),
), ),
Expanded( Expanded(
@@ -117,6 +122,7 @@ class _ActionPanelState extends State<ActionPanel> {
: FontAwesomeIcons.thumbsUp, : FontAwesomeIcons.thumbsUp,
size: 16, size: 16,
color: stat.like!.status! ? primary : color, color: stat.like!.status! ? primary : color,
semanticLabel: stat.like!.status! ? "已赞": "点赞",
), ),
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
@@ -128,7 +134,7 @@ class _ActionPanelState extends State<ActionPanel> {
return ScaleTransition(scale: animation, child: child); return ScaleTransition(scale: animation, child: child);
}, },
child: Text( child: Text(
stat.like!.count ?? '点赞', stat.like!.count ?? '',
key: ValueKey<String>(stat.like!.count ?? '点赞'), key: ValueKey<String>(stat.like!.count ?? '点赞'),
style: TextStyle( style: TextStyle(
color: stat.like!.status! ? primary : color, color: stat.like!.status! ? primary : color,

View File

@@ -48,6 +48,7 @@ class AuthorPanel extends StatelessWidget {
children: [ children: [
Text( Text(
item.modules.moduleAuthor.name, item.modules.moduleAuthor.name,
// semanticsLabel: "Up主${item.modules.moduleAuthor.name}",
style: TextStyle( style: TextStyle(
color: item.modules.moduleAuthor!.vip != null && color: item.modules.moduleAuthor!.vip != null &&
item.modules.moduleAuthor!.vip['status'] > 0 item.modules.moduleAuthor!.vip['status'] > 0
@@ -81,6 +82,7 @@ class AuthorPanel extends StatelessWidget {
width: 32, width: 32,
height: 32, height: 32,
child: IconButton( child: IconButton(
tooltip: '更多',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),

View File

@@ -87,6 +87,7 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
width: width, width: width,
height: width / StyleString.aspectRatio, height: width / StyleString.aspectRatio,
src: content.cover, src: content.cover,
semanticsLabel: content.title,
), ),
), ),
if (content.badge != null && type == 'pgc') if (content.badge != null && type == 'pgc')
@@ -133,7 +134,7 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
const SizedBox(width: 10), const SizedBox(width: 10),
Text(content.stat.play + '次围观'), Text(content.stat.play + '次围观'),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(content.stat.danmaku + '条弹幕') Text(content.stat.danmu + '条弹幕')
], ],
), ),
), ),

View File

@@ -78,10 +78,11 @@ class _EmotePanelState extends State<EmotePanel>
overflow: TextOverflow.clip, overflow: TextOverflow.clip,
maxLines: 1, maxLines: 1,
) )
: Image.network( : NetworkImgLayer(
e.emote![index].url!, src: e.emote![index].url!,
width: size * 38, width: size * 38,
height: size * 38, height: size * 38,
semanticsLabel: e.emote![index].text!,
), ),
), ),
), ),

View File

@@ -49,6 +49,7 @@ class _FavPageState extends State<FavPage> {
onPressed: () => Get.toNamed( onPressed: () => Get.toNamed(
'/favSearch?searchType=1&mediaId=${_favController.favFolderData.value.list!.first.id}'), '/favSearch?searchType=1&mediaId=${_favController.favFolderData.value.list!.first.id}'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
tooltip: '搜索',
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
], ],

View File

@@ -96,6 +96,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
), ),
actions: [ actions: [
IconButton( IconButton(
tooltip: '搜索',
onPressed: () => onPressed: () =>
Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'), Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),

View File

@@ -209,6 +209,7 @@ class VideoContent extends StatelessWidget {
right: 0, right: 0,
bottom: -4, bottom: -4,
child: IconButton( child: IconButton(
tooltip: '取消收藏',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),

View File

@@ -50,6 +50,7 @@ class _FavSearchPageState extends State<FavSearchPage> {
titleSpacing: 0, titleSpacing: 0,
actions: [ actions: [
IconButton( IconButton(
tooltip: '搜索',
onPressed: () => _favSearchCtr.submit(), onPressed: () => _favSearchCtr.submit(),
icon: const Icon(Icons.search_outlined, size: 22)), icon: const Icon(Icons.search_outlined, size: 22)),
const SizedBox(width: 10) const SizedBox(width: 10)
@@ -65,6 +66,7 @@ class _FavSearchPageState extends State<FavSearchPage> {
hintText: _favSearchCtr.hintText, hintText: _favSearchCtr.hintText,
border: InputBorder.none, border: InputBorder.none,
suffixIcon: IconButton( suffixIcon: IconButton(
tooltip: '清空',
icon: Icon( icon: Icon(
Icons.clear, Icons.clear,
size: 22, size: 22,

View File

@@ -41,6 +41,7 @@ class _FollowPageState extends State<FollowPage> {
IconButton( IconButton(
onPressed: () => Get.toNamed('/followSearch?mid=$mid'), onPressed: () => Get.toNamed('/followSearch?mid=$mid'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
tooltip: '搜索'
), ),
PopupMenuButton( PopupMenuButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),

View File

@@ -50,6 +50,7 @@ class _FollowSearchPageState extends State<FollowSearchPage> {
titleSpacing: 0, titleSpacing: 0,
actions: [ actions: [
IconButton( IconButton(
tooltip: '搜索',
onPressed: reRequest, onPressed: reRequest,
icon: const Icon(CupertinoIcons.search, size: 22), icon: const Icon(CupertinoIcons.search, size: 22),
), ),
@@ -65,6 +66,7 @@ class _FollowSearchPageState extends State<FollowSearchPage> {
hintText: _followSearchController.hintText, hintText: _followSearchController.hintText,
border: InputBorder.none, border: InputBorder.none,
suffixIcon: IconButton( suffixIcon: IconButton(
tooltip: '清空',
icon: Icon( icon: Icon(
Icons.clear, Icons.clear,
size: 22, size: 22,

View File

@@ -76,6 +76,7 @@ class _HistoryPageState extends State<HistoryPage> {
), ),
actions: [ actions: [
IconButton( IconButton(
tooltip: '搜索',
onPressed: () => Get.toNamed('/historySearch'), onPressed: () => Get.toNamed('/historySearch'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
), ),
@@ -129,6 +130,7 @@ class _HistoryPageState extends State<HistoryPage> {
titleSpacing: 0, titleSpacing: 0,
centerTitle: false, centerTitle: false,
leading: IconButton( leading: IconButton(
tooltip: '取消',
onPressed: () { onPressed: () {
_historyController.enableMultiple.value = false; _historyController.enableMultiple.value = false;
for (var item in _historyController.historyList) { for (var item in _historyController.historyList) {

View File

@@ -230,6 +230,7 @@ class HistoryItem extends StatelessWidget {
const Duration(milliseconds: 250), const Duration(milliseconds: 250),
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: IconButton( child: IconButton(
tooltip: '取消选择',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all( padding: MaterialStateProperty.all(
EdgeInsets.zero), EdgeInsets.zero),

View File

@@ -50,6 +50,7 @@ class _HistorySearchPageState extends State<HistorySearchPage> {
titleSpacing: 0, titleSpacing: 0,
actions: [ actions: [
IconButton( IconButton(
tooltip: '搜索',
onPressed: () => _historySearchCtr.submit(), onPressed: () => _historySearchCtr.submit(),
icon: const Icon(Icons.search_outlined, size: 22)), icon: const Icon(Icons.search_outlined, size: 22)),
const SizedBox(width: 10) const SizedBox(width: 10)
@@ -65,6 +66,7 @@ class _HistorySearchPageState extends State<HistorySearchPage> {
hintText: _historySearchCtr.hintText, hintText: _historySearchCtr.hintText,
border: InputBorder.none, border: InputBorder.none,
suffixIcon: IconButton( suffixIcon: IconButton(
tooltip: '清空',
icon: Icon( icon: Icon(
Icons.clear, Icons.clear,
size: 22, size: 22,

View File

@@ -220,41 +220,46 @@ class UserInfoWidget extends StatelessWidget {
const SizedBox(width: 4), const SizedBox(width: 4),
ClipRect( ClipRect(
child: IconButton( child: IconButton(
tooltip: '消息',
onPressed: () => Get.toNamed('/whisper'), onPressed: () => Get.toNamed('/whisper'),
icon: const Icon(Icons.notifications_none), icon: const Icon(
Icons.notifications_none,
),
), ),
) )
], ],
const SizedBox(width: 8), const SizedBox(width: 8),
Obx( Semantics(
() => userLogin.value label: "我的",
? Stack( child: Obx(
children: [ () => userLogin.value
NetworkImgLayer( ? Stack(
type: 'avatar', children: [
width: 34, NetworkImgLayer(
height: 34, type: 'avatar',
src: userFace, width: 34,
), height: 34,
Positioned.fill( src: userFace,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => callback?.call(),
splashColor: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.3),
borderRadius: const BorderRadius.all(
Radius.circular(50),
),
), ),
), Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => callback?.call(),
splashColor: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.3),
borderRadius: const BorderRadius.all(
Radius.circular(50),
),
),
),
)
],
) )
], : DefaultUser(callback: () => callback!()),
) )),
: DefaultUser(callback: () => callback!()),
),
], ],
); );
} }
@@ -270,6 +275,7 @@ class DefaultUser extends StatelessWidget {
width: 38, width: 38,
height: 38, height: 38,
child: IconButton( child: IconButton(
tooltip: '默认用户头像',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.resolveWith((states) { backgroundColor: MaterialStateProperty.resolveWith((states) {
@@ -409,6 +415,7 @@ class SearchBar extends StatelessWidget {
Icon( Icon(
Icons.search_outlined, Icons.search_outlined,
color: colorScheme.onSecondaryContainer, color: colorScheme.onSecondaryContainer,
semanticLabel: '搜索',
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(

View File

@@ -43,6 +43,7 @@ class HomeAppBar extends StatelessWidget {
Hero( Hero(
tag: 'searchTag', tag: 'searchTag',
child: IconButton( child: IconButton(
tooltip: '搜索',
onPressed: () { onPressed: () {
Get.toNamed('/search'); Get.toNamed('/search');
}, },
@@ -72,11 +73,13 @@ class HomeAppBar extends StatelessWidget {
width: 32, width: 32,
height: 32, height: 32,
src: userInfo.face, src: userInfo.face,
semanticsLabel: '我的',
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
] else ...[ ] else ...[
IconButton( IconButton(
tooltip: '登录',
onPressed: () => showModalBottomSheet( onPressed: () => showModalBottomSheet(
context: context, context: context,
builder: (_) => const SizedBox( builder: (_) => const SizedBox(

View File

@@ -136,6 +136,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
actions: [ actions: [
const SizedBox(width: 4), const SizedBox(width: 4),
IconButton( IconButton(
tooltip: '用内置浏览器打开',
onPressed: () { onPressed: () {
Get.toNamed('/webview', parameters: { Get.toNamed('/webview', parameters: {
'url': url.startsWith('http') ? url : 'https:$url', 'url': url.startsWith('http') ? url : 'https:$url',
@@ -148,6 +149,36 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
PopupMenuButton( PopupMenuButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
onTap: () => {
_htmlRenderCtr.reqHtml(id),
},
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.refresh, size: 19),
SizedBox(width: 10),
Text('刷新'),
],
),
),
PopupMenuItem(
onTap: () => {
Get.toNamed('/webview', parameters: {
'url': url.startsWith('http') ? url : 'https:$url',
'type': 'url',
'pageTitle': title,
}),
},
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.open_in_new, size: 19),
SizedBox(width: 10),
Text('内置浏览器打开'),
],
),
),
PopupMenuItem( PopupMenuItem(
onTap: () => { onTap: () => {
Clipboard.setData(ClipboardData(text: url)), Clipboard.setData(ClipboardData(text: url)),

View File

@@ -89,6 +89,7 @@ class _BottomControlState extends State<BottomControl> {
width: 34, width: 34,
height: 34, height: 34,
child: IconButton( child: IconButton(
tooltip: '画中画',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
@@ -114,6 +115,7 @@ class _BottomControlState extends State<BottomControl> {
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
ComBtn( ComBtn(
tooltip: '全屏切换',
icon: const Icon( icon: const Icon(
Icons.fullscreen, Icons.fullscreen,
size: 20, size: 20,

View File

@@ -25,6 +25,7 @@ class _LoginPageState extends State<LoginPage> {
leading: Obx( leading: Obx(
() => _loginPageCtr.currentIndex.value == 0 () => _loginPageCtr.currentIndex.value == 0
? IconButton( ? IconButton(
tooltip: '关闭',
onPressed: () async { onPressed: () async {
_loginPageCtr.mobTextFieldNode.unfocus(); _loginPageCtr.mobTextFieldNode.unfocus();
await Future.delayed(const Duration(milliseconds: 200)); await Future.delayed(const Duration(milliseconds: 200));
@@ -33,6 +34,7 @@ class _LoginPageState extends State<LoginPage> {
icon: const Icon(Icons.close_outlined), icon: const Icon(Icons.close_outlined),
) )
: IconButton( : IconButton(
tooltip: '返回',
onPressed: () => _loginPageCtr.previousPage(), onPressed: () => _loginPageCtr.previousPage(),
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
), ),
@@ -174,6 +176,7 @@ class _LoginPageState extends State<LoginPage> {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
IconButton( IconButton(
tooltip: '切换至验证码登录',
style: ButtonStyle( style: ButtonStyle(
backgroundColor: backgroundColor:
MaterialStateProperty.resolveWith( MaterialStateProperty.resolveWith(
@@ -265,6 +268,7 @@ class _LoginPageState extends State<LoginPage> {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
IconButton( IconButton(
tooltip: '切换至密码登录',
style: ButtonStyle( style: ButtonStyle(
backgroundColor: backgroundColor:
MaterialStateProperty.resolveWith( MaterialStateProperty.resolveWith(

View File

@@ -149,6 +149,7 @@ class _MediaPageState extends State<MediaPage>
), ),
), ),
trailing: IconButton( trailing: IconButton(
tooltip: '刷新',
onPressed: () { onPressed: () {
setState(() { setState(() {
_futureBuilderFuture = mediaController.queryFavFolder(); _futureBuilderFuture = mediaController.queryFavFolder();
@@ -189,6 +190,7 @@ class _MediaPageState extends State<MediaPage>
right: 14, bottom: 35), right: 14, bottom: 35),
child: Center( child: Center(
child: IconButton( child: IconButton(
tooltip: '查看更多',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all( padding: MaterialStateProperty.all(
EdgeInsets.zero), EdgeInsets.zero),

View File

@@ -104,6 +104,7 @@ class _MemberPageState extends State<MemberPage>
), ),
actions: [ actions: [
IconButton( IconButton(
tooltip: '搜索',
onPressed: () => Get.toNamed( onPressed: () => Get.toNamed(
'/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name!}'), '/memberSearch?mid=$mid&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined), icon: const Icon(Icons.search_outlined),
@@ -310,17 +311,20 @@ class _MemberPageState extends State<MemberPage>
FontAwesomeIcons.venus, FontAwesomeIcons.venus,
size: 14, size: 14,
color: Colors.pink, color: Colors.pink,
semanticLabel: _memberController.memberInfo.value.sex,
), ),
if (_memberController.memberInfo.value.sex == '') if (_memberController.memberInfo.value.sex == '')
const Icon( const Icon(
FontAwesomeIcons.mars, FontAwesomeIcons.mars,
size: 14, size: 14,
color: Colors.blue, color: Colors.blue,
semanticLabel: _memberController.memberInfo.value.sex,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Image.asset( Image.asset(
'assets/images/lv/lv${_memberController.memberInfo.value.level}.png', 'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',
height: 11, height: 11,
semanticLabel: '等级${_memberController.memberInfo.value.level}',
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
if (_memberController if (_memberController
@@ -333,6 +337,7 @@ class _MemberPageState extends State<MemberPage>
_memberController.memberInfo.value.vip! _memberController.memberInfo.value.vip!
.label!['img_label_uri_hans'], .label!['img_label_uri_hans'],
height: 20, height: 20,
semanticLabel: _memberController.memberInfo.value.vip!.label!['text'],
), ),
] else if (_memberController ] else if (_memberController
.memberInfo.value.vip!.status == .memberInfo.value.vip!.status ==
@@ -344,6 +349,7 @@ class _MemberPageState extends State<MemberPage>
_memberController.memberInfo.value.vip! _memberController.memberInfo.value.vip!
.label!['img_label_uri_hans_static'], .label!['img_label_uri_hans_static'],
height: 20, height: 20,
semanticLabel: _memberController.memberInfo.value.vip!.label!['text'],
), ),
] ]
], ],

View File

@@ -147,28 +147,30 @@ class ProfilePanel extends StatelessWidget {
], ],
), ),
), ),
Column( InkWell(
children: [ onTap: null,
Text( child: Column(
!loadingStatus children: [
? ctr.userStat!['likes'] != null Text(
? Utils.numFormat( !loadingStatus
ctr.userStat!['likes'], ? ctr.userStat!['likes'] != null
) ? Utils.numFormat(
: '-' ctr.userStat!['likes'],
: '-', )
style: const TextStyle( : '-'
fontWeight: FontWeight.bold)), : '-',
Text( style: const TextStyle(
'获赞', fontWeight: FontWeight.bold)),
style: TextStyle( Text(
fontSize: Theme.of(context) '获赞',
.textTheme style: TextStyle(
.labelMedium! fontSize: Theme.of(context)
.fontSize), .textTheme
) .labelMedium!
], .fontSize),
), )
],
)),
], ],
), ),
), ),
@@ -221,8 +223,7 @@ class ProfilePanel extends StatelessWidget {
TextButton( TextButton(
onPressed: () { onPressed: () {
Get.toNamed('/webview', parameters: { Get.toNamed('/webview', parameters: {
'url': 'url': 'https://account.bilibili.com/account/home',
'https://account.bilibili.com/account/home',
'pageTitle': '编辑资料(建议浏览器打开)', 'pageTitle': '编辑资料(建议浏览器打开)',
'type': 'url' 'type': 'url'
}); });

View File

@@ -43,6 +43,7 @@ class MemberSeasonsPanel extends StatelessWidget {
width: 35, width: 35,
height: 35, height: 35,
child: IconButton( child: IconButton(
tooltip: '前往',
onPressed: () => Get.toNamed( onPressed: () => Get.toNamed(
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'), '/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
style: ButtonStyle( style: ButtonStyle(

View File

@@ -52,6 +52,7 @@ class _MemberSearchPageState extends State<MemberSearchPage>
titleSpacing: 0, titleSpacing: 0,
actions: [ actions: [
IconButton( IconButton(
tooltip: '搜索',
onPressed: () => _memberSearchCtr.submit(), onPressed: () => _memberSearchCtr.submit(),
icon: const Icon(CupertinoIcons.search, size: 22)), icon: const Icon(CupertinoIcons.search, size: 22)),
const SizedBox(width: 10) const SizedBox(width: 10)
@@ -67,6 +68,7 @@ class _MemberSearchPageState extends State<MemberSearchPage>
hintText: _memberSearchCtr.hintText, hintText: _memberSearchCtr.hintText,
border: InputBorder.none, border: InputBorder.none,
suffixIcon: IconButton( suffixIcon: IconButton(
tooltip: '清空',
icon: Icon( icon: Icon(
Icons.clear, Icons.clear,
size: 22, size: 22,

View File

@@ -44,46 +44,54 @@ class _MinePageState extends State<MinePage> {
toolbarHeight: kTextTabBarHeight + 20, toolbarHeight: kTextTabBarHeight + 20,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
centerTitle: false, centerTitle: false,
title: //logo title: ExcludeSemantics(
Row( child: Row(
children: [ children: [
Image.asset( Image.asset(
'assets/images/logo/logo_android_2.png', 'assets/images/logo/logo_android_2.png',
width: 40, width: 40,
), ),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
'PiliPalaX', 'PiliPalaX',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
], ],
),
), ),
actions: [ actions: [
IconButton( IconButton(
tooltip: "${MineController.anonymity ? '退出' : '进入'}无痕模式",
onPressed: () { onPressed: () {
MineController.onChangeAnonymity(context); MineController.onChangeAnonymity(context);
setState(() {}); setState(() {});
}, },
icon: Icon( icon: Icon(
MineController.anonymity MineController.anonymity
? Icons.visibility_off ? CupertinoIcons.checkmark_shield
: Icons.visibility, : CupertinoIcons.shield_slash,
size: 22, size: 22,
), ),
), ),
IconButton( IconButton(
onPressed: () => mineController.onChangeTheme(), tooltip:
'切换至${mineController.themeType.value == ThemeType.dark ? '浅色' : '深色'}主题',
onPressed: () {
mineController.onChangeTheme();
setState(() {});
},
icon: Icon( icon: Icon(
mineController.themeType.value == ThemeType.dark mineController.themeType.value == ThemeType.dark
? Icons.light_mode ? CupertinoIcons.moon
: Icons.mode_night, : CupertinoIcons.sun_min,
size: 22, size: 22,
), ),
), ),
IconButton( IconButton(
tooltip: '设置',
onPressed: () => Get.toNamed('/setting', preventDuplicates: false), onPressed: () => Get.toNamed('/setting', preventDuplicates: false),
icon: const Icon( icon: const Icon(
Icons.settings, CupertinoIcons.gear,
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@@ -140,6 +148,7 @@ class _MinePageState extends State<MinePage> {
child: _mineController.userInfo.value.face != null child: _mineController.userInfo.value.face != null
? NetworkImgLayer( ? NetworkImgLayer(
src: _mineController.userInfo.value.face, src: _mineController.userInfo.value.face,
semanticsLabel: '头像',
width: 85, width: 85,
height: 85) height: 85)
: Image.asset('assets/images/noface.jpeg'), : Image.asset('assets/images/noface.jpeg'),
@@ -159,6 +168,8 @@ class _MinePageState extends State<MinePage> {
Image.asset( Image.asset(
'assets/images/lv/lv${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}.png', 'assets/images/lv/lv${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}.png',
height: 10, height: 10,
semanticLabel:
'等级:${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}',
), ),
], ],
), ),
@@ -207,6 +218,8 @@ class _MinePageState extends State<MinePage> {
color: Theme.of(context).colorScheme.onPrimary, color: Theme.of(context).colorScheme.onPrimary,
fontSize: 12, fontSize: 12,
), ),
semanticsLabel:
'当前经验${levelInfo.currentExp!},升级需要${levelInfo.nextExp!}',
), ),
), ),
), ),

View File

@@ -64,7 +64,10 @@ class SSearchController extends GetxController {
void submit() { void submit() {
// ignore: unrelated_type_equality_checks // ignore: unrelated_type_equality_checks
if (searchKeyWord == '') { if (searchKeyWord == '') {
return; if (hintText == ''){
return;
}
searchKeyWord.value = hintText;
} }
List arr = historyCacheList.where((e) => e != searchKeyWord.value).toList(); List arr = historyCacheList.where((e) => e != searchKeyWord.value).toList();
arr.insert(0, searchKeyWord.value); arr.insert(0, searchKeyWord.value);

View File

@@ -53,6 +53,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
titleSpacing: 0, titleSpacing: 0,
actions: [ actions: [
IconButton( IconButton(
tooltip: '搜索',
onPressed: () => _searchController.submit(), onPressed: () => _searchController.submit(),
icon: const Icon(CupertinoIcons.search, size: 22), icon: const Icon(CupertinoIcons.search, size: 22),
), ),
@@ -69,6 +70,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
hintText: _searchController.hintText, hintText: _searchController.hintText,
border: InputBorder.none, border: InputBorder.none,
suffixIcon: IconButton( suffixIcon: IconButton(
tooltip: '清空',
icon: Icon( icon: Icon(
Icons.clear, Icons.clear,
size: 22, size: 22,

View File

@@ -87,6 +87,7 @@ class SearchVideoPanel extends StatelessWidget {
width: 32, width: 32,
height: 32, height: 32,
child: IconButton( child: IconButton(
tooltip: '筛选',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),

View File

@@ -87,7 +87,7 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
errMsg: snapshot.data['msg'], errMsg: snapshot.data['msg'],
btnText: snapshot.data['code'] == -404 || btnText: snapshot.data['code'] == -404 ||
snapshot.data['code'] == 62002 snapshot.data['code'] == 62002
? '返回上一页' ? '上一页'
: null, : null,
fn: () => Get.back(), fn: () => Get.back(),
); );
@@ -285,8 +285,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
StatDanMu( StatDanMu(
theme: 'gray', theme: 'gray',
danmu: !loadingStatus danmu: !loadingStatus
? widget.videoDetail!.stat!.danmaku ? widget.videoDetail!.stat!.danmu
: videoItem['stat'].danmaku, : videoItem['stat'].danmu,
size: 'medium', size: 'medium',
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@@ -335,17 +335,19 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Positioned( Positioned(
right: 10, right: 10,
top: 6, top: 6,
child: GestureDetector( child: Semantics(
onTap: () async { label: 'AI总结',
final res = child: GestureDetector(
await videoIntroController.aiConclusion(); onTap: () async {
if (res['status']) { final res =
showAiBottomSheet(); await videoIntroController.aiConclusion();
} if (res['status']) {
}, showAiBottomSheet();
child: }
Image.asset('assets/images/ai.png', height: 22), },
), child: Image.asset('assets/images/ai.png',
height: 22),
)),
) )
], ],
), ),
@@ -406,11 +408,15 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
fadeOutDuration: Duration.zero, fadeOutDuration: Duration.zero,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(owner.name, Text(
style: const TextStyle(fontSize: 13)), owner.name,
style: const TextStyle(fontSize: 13),
// semanticsLabel: "Up主${owner.name}",
),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
follower, follower,
semanticsLabel: "粉丝数:$follower",
style: TextStyle( style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize, fontSize: t.textTheme.labelSmall!.fontSize,
color: outline, color: outline,
@@ -498,8 +504,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
onTap: handleState(videoIntroController.actionLikeVideo), onTap: handleState(videoIntroController.actionLikeVideo),
selectStatus: videoIntroController.hasLike.value, selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
semanticsLabel: '点赞',
text: !loadingStatus text: !loadingStatus
? widget.videoDetail!.stat!.like!.toString() ? Utils.numFormat(widget.videoDetail!.stat!.like!)
: '-'), : '-'),
), ),
// ActionItem( // ActionItem(
@@ -515,8 +522,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
onTap: handleState(videoIntroController.actionCoinVideo), onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value, selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
semanticsLabel: '投币',
text: !loadingStatus text: !loadingStatus
? widget.videoDetail!.stat!.coin!.toString() ? Utils.numFormat(widget.videoDetail!.stat!.coin!)
: '-'), : '-'),
), ),
Obx( Obx(
@@ -527,8 +535,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
onLongPress: () => showFavBottomSheet(type: 'longPress'), onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value, selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
semanticsLabel: '收藏',
text: !loadingStatus text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString() ? Utils.numFormat(widget.videoDetail!.stat!.favorite!)
: '-'), : '-'),
), ),
ActionItem( ActionItem(
@@ -536,15 +545,19 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
onTap: () => videoDetailCtr.tabCtr.animateTo(1), onTap: () => videoDetailCtr.tabCtr.animateTo(1),
selectStatus: false, selectStatus: false,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
semanticsLabel: '评论',
text: !loadingStatus text: !loadingStatus
? widget.videoDetail!.stat!.reply!.toString() ? Utils.numFormat(widget.videoDetail!.stat!.reply!)
: '评论'), : '评论'),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare), icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(), onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false, selectStatus: false,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
text: '分享'), semanticsLabel: '分享',
text: !loadingStatus
? Utils.numFormat(widget.videoDetail!.stat!.share!)
: '分享'),
], ],
), ),
); );

View File

@@ -10,6 +10,7 @@ class ActionItem extends StatelessWidget {
final bool? loadingStatus; final bool? loadingStatus;
final String? text; final String? text;
final bool selectStatus; final bool selectStatus;
final String semanticsLabel;
const ActionItem({ const ActionItem({
Key? key, Key? key,
@@ -20,11 +21,15 @@ class ActionItem extends StatelessWidget {
this.loadingStatus, this.loadingStatus,
this.text, this.text,
this.selectStatus = false, this.selectStatus = false,
required this.semanticsLabel,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return Semantics(
label: (text ?? "") + (selectStatus ? "" :"") + semanticsLabel,
child:
InkWell(
onTap: () => { onTap: () => {
feedBack(), feedBack(),
onTap!(), onTap!(),
@@ -37,11 +42,15 @@ class ActionItem extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(height: 4), const SizedBox(height: 4),
selectStatus Icon(
? Icon(selectIcon!.icon!, selectStatus
size: 18, color: Theme.of(context).colorScheme.primary) ? selectIcon!.icon!
: Icon(icon!.icon!, : icon!.icon!,
size: 18, color: Theme.of(context).colorScheme.outline), size: 18,
color: selectStatus
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 6), const SizedBox(height: 6),
AnimatedOpacity( AnimatedOpacity(
opacity: loadingStatus! ? 0 : 1, opacity: loadingStatus! ? 0 : 1,
@@ -59,11 +68,12 @@ class ActionItem extends StatelessWidget {
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline, : Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize), fontSize: Theme.of(context).textTheme.labelSmall!.fontSize),
semanticsLabel: "",
), ),
), ),
), ),
], ],
), ),
); ));
} }
} }

View File

@@ -33,6 +33,7 @@ class _FavPanelState extends State<FavPanel> {
centerTitle: false, centerTitle: false,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
tooltip: '关闭',
onPressed: () => Get.back(), onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)), icon: const Icon(Icons.close_outlined)),
title: title:

View File

@@ -61,6 +61,7 @@ class _GroupPanelState extends State<GroupPanel> {
centerTitle: false, centerTitle: false,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
tooltip: '关闭',
onPressed: () => Get.back(), onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)), icon: const Icon(Icons.close_outlined)),
title: title:

View File

@@ -61,7 +61,7 @@ class IntroDetail extends StatelessWidget {
const SizedBox(width: 10), const SizedBox(width: 10),
StatDanMu( StatDanMu(
theme: 'gray', theme: 'gray',
danmu: videoDetail!.stat!.danmaku, danmu: videoDetail!.stat!.danmu,
size: 'medium', size: 'medium',
), ),
const SizedBox(width: 10), const SizedBox(width: 10),

View File

@@ -115,6 +115,7 @@ class _PagesPanelState extends State<PagesPanel> {
.titleMedium, .titleMedium,
), ),
IconButton( IconButton(
tooltip: '关闭',
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),

View File

@@ -210,28 +210,30 @@ class ReplyItem extends StatelessWidget {
// title // title
Container( Container(
margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4), margin: const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),
child: Text.rich( child: Semantics(
style: const TextStyle(height: 1.75), label: replyItem?.content?.message ?? "",
maxLines: child: Text.rich(
replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999, style: const TextStyle(height: 1.75),
overflow: TextOverflow.ellipsis, maxLines:
TextSpan( replyItem!.content!.isText! && replyLevel == '1' ? 3 : 999,
children: [ overflow: TextOverflow.ellipsis,
if (replyItem!.isTop!) TextSpan(
const WidgetSpan( children: [
alignment: PlaceholderAlignment.top, if (replyItem!.isTop!)
child: PBadge( const WidgetSpan(
text: 'TOP', alignment: PlaceholderAlignment.top,
size: 'small', child: PBadge(
stack: 'normal', text: 'TOP',
type: 'line', size: 'small',
fs: 9, stack: 'normal',
), type: 'line',
), fs: 9,
buildContent(context, replyItem!, replyReply, null), ),
], ),
), buildContent(context, replyItem!, replyReply, null),
), ],
),
)),
), ),
// 操作区域 // 操作区域
bottonAction(context, replyItem!.replyControl), bottonAction(context, replyItem!.replyControl),

View File

@@ -76,6 +76,7 @@ class _ZanButtonState extends State<ZanButton> {
: FontAwesomeIcons.thumbsUp, : FontAwesomeIcons.thumbsUp,
size: 16, size: 16,
color: widget.replyItem!.action == 1 ? primary : color, color: widget.replyItem!.action == 1 ? primary : color,
semanticLabel: widget.replyItem!.action == 1 ? '已赞' : '点赞',
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
AnimatedSwitcher( AnimatedSwitcher(

View File

@@ -5,6 +5,7 @@ class ToolbarIconButton extends StatelessWidget {
final Icon icon; final Icon icon;
final String toolbarType; final String toolbarType;
final bool selected; final bool selected;
final String tooltip;
const ToolbarIconButton({ const ToolbarIconButton({
super.key, super.key,
@@ -12,6 +13,7 @@ class ToolbarIconButton extends StatelessWidget {
required this.icon, required this.icon,
required this.toolbarType, required this.toolbarType,
required this.selected, required this.selected,
required this.tooltip,
}); });
@override @override
@@ -20,6 +22,7 @@ class ToolbarIconButton extends StatelessWidget {
width: 36, width: 36,
height: 36, height: 36,
child: IconButton( child: IconButton(
tooltip: tooltip,
onPressed: onPressed, onPressed: onPressed,
icon: icon, icon: icon,
highlightColor: Theme.of(context).colorScheme.secondaryContainer, highlightColor: Theme.of(context).colorScheme.secondaryContainer,

View File

@@ -192,6 +192,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
ToolbarIconButton( ToolbarIconButton(
tooltip: '输入',
onPressed: () { onPressed: () {
if (toolbarType == 'emote') { if (toolbarType == 'emote') {
setState(() { setState(() {
@@ -206,6 +207,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
), ),
const SizedBox(width: 20), const SizedBox(width: 20),
ToolbarIconButton( ToolbarIconButton(
tooltip: '表情',
onPressed: () { onPressed: () {
if (toolbarType == 'input') { if (toolbarType == 'input') {
setState(() { setState(() {

View File

@@ -85,6 +85,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
children: <Widget>[ children: <Widget>[
const Text('评论详情'), const Text('评论详情'),
IconButton( IconButton(
tooltip: '关闭',
icon: const Icon(Icons.close, size: 20), icon: const Icon(Icons.close, size: 20),
onPressed: () { onPressed: () {
_videoReplyReplyController.currentPage = 0; _videoReplyReplyController.currentPage = 0;

View File

@@ -1027,6 +1027,7 @@ class _HeaderControlState extends State<HeaderControl> {
children: [ children: [
// SizedBox(width: MediaQuery.of(context).padding.left,), // SizedBox(width: MediaQuery.of(context).padding.left,),
ComBtn( ComBtn(
tooltip: '上一页',
icon: const Icon( icon: const Icon(
FontAwesomeIcons.arrowLeft, FontAwesomeIcons.arrowLeft,
size: 15, size: 15,
@@ -1048,8 +1049,9 @@ class _HeaderControlState extends State<HeaderControl> {
}, },
), ),
SizedBox(width: buttonSpace), SizedBox(width: buttonSpace),
if ((videoIntroController.videoDetail.value.title != null) && (isFullScreen || if ((videoIntroController.videoDetail.value.title != null) &&
(!isFullScreen && isLandscape && !horizontalScreen))) ...[ (isFullScreen ||
(!isFullScreen && isLandscape && !horizontalScreen))) ...[
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -1090,6 +1092,7 @@ class _HeaderControlState extends State<HeaderControl> {
) )
] else ...[ ] else ...[
ComBtn( ComBtn(
tooltip: '返回主页',
icon: const Icon( icon: const Icon(
FontAwesomeIcons.house, FontAwesomeIcons.house,
size: 15, size: 15,
@@ -1118,12 +1121,13 @@ class _HeaderControlState extends State<HeaderControl> {
width: 34, width: 34,
height: 34, height: 34,
child: IconButton( child: IconButton(
tooltip: '发弹幕',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
onPressed: () => showShootDanmakuSheet(), onPressed: () => showShootDanmakuSheet(),
icon: const Icon( icon: const Icon(
Icons.add_card_outlined, Icons.add_comment_outlined,
size: 19, size: 19,
color: Colors.white, color: Colors.white,
), ),
@@ -1135,6 +1139,7 @@ class _HeaderControlState extends State<HeaderControl> {
height: 34, height: 34,
child: Obx( child: Obx(
() => IconButton( () => IconButton(
tooltip: "${_.isOpenDanmu.value ? '关闭' : '开启'}弹幕",
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
@@ -1143,8 +1148,8 @@ class _HeaderControlState extends State<HeaderControl> {
}, },
icon: Icon( icon: Icon(
_.isOpenDanmu.value _.isOpenDanmu.value
? Icons.subtitles_outlined ? Icons.comment_outlined
: Icons.subtitles_off_outlined, : Icons.comments_disabled_outlined,
size: 19, size: 19,
color: Colors.white, color: Colors.white,
), ),
@@ -1157,6 +1162,7 @@ class _HeaderControlState extends State<HeaderControl> {
width: 34, width: 34,
height: 34, height: 34,
child: IconButton( child: IconButton(
tooltip: '画中画',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
@@ -1182,6 +1188,7 @@ class _HeaderControlState extends State<HeaderControl> {
SizedBox(width: buttonSpace), SizedBox(width: buttonSpace),
], ],
ComBtn( ComBtn(
tooltip: '更多设置',
icon: const Icon( icon: const Icon(
Icons.more_vert_outlined, Icons.more_vert_outlined,
size: 18, size: 18,

View File

@@ -27,6 +27,7 @@ class _WebviewPageState extends State<WebviewPage> {
actions: [ actions: [
const SizedBox(width: 4), const SizedBox(width: 4),
IconButton( IconButton(
tooltip: '刷新',
onPressed: () { onPressed: () {
_webviewController.controller.reload(); _webviewController.controller.reload();
}, },
@@ -34,6 +35,7 @@ class _WebviewPageState extends State<WebviewPage> {
color: Theme.of(context).colorScheme.primary), color: Theme.of(context).colorScheme.primary),
), ),
IconButton( IconButton(
tooltip: '用外部浏览器打开',
onPressed: () { onPressed: () {
launchUrl(Uri.parse(_webviewController.url)); launchUrl(Uri.parse(_webviewController.url));
}, },

View File

@@ -90,6 +90,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
width: 34, width: 34,
height: 34, height: 34,
child: IconButton( child: IconButton(
tooltip: '返回',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.resolveWith( backgroundColor: MaterialStateProperty.resolveWith(
@@ -160,7 +161,8 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
reverse: true, reverse: true,
itemBuilder: (_, int i) { itemBuilder: (_, int i) {
return ChatItem( return ChatItem(
item: messageList[i], e_infos: _whisperDetailController.eInfos); item: messageList[i],
e_infos: _whisperDetailController.eInfos);
}, },
); );
}), }),
@@ -197,6 +199,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
// ), // ),
// ), // ),
IconButton( IconButton(
tooltip: '表情',
onPressed: () { onPressed: () {
// if (toolbarType == 'input') { // if (toolbarType == 'input') {
// setState(() { // setState(() {
@@ -220,22 +223,25 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
.withOpacity(0.08), .withOpacity(0.08),
borderRadius: BorderRadius.circular(40.0), borderRadius: BorderRadius.circular(40.0),
), ),
child: TextField( child: Semantics(
readOnly: true, label: '私信输入框(开发中)',
style: Theme.of(context).textTheme.titleMedium, child: TextField(
controller: _replyContentController, readOnly: true,
autofocus: false, style: Theme.of(context).textTheme.titleMedium,
focusNode: replyContentFocusNode, controller: _replyContentController,
decoration: const InputDecoration( autofocus: false,
border: InputBorder.none, // 移除默认边框 focusNode: replyContentFocusNode,
hintText: '开发中 ...', // 提示文本 decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric( border: InputBorder.none, // 移除默认边框
horizontal: 16.0, vertical: 12.0), // 内边距 hintText: '开发中 ...', // 提示文本
), contentPadding: EdgeInsets.symmetric(
), horizontal: 16.0, vertical: 12.0), // 内边距
),
)),
), ),
), ),
IconButton( IconButton(
tooltip: '发送',
// onPressed: _whisperDetailController.sendMsg, // onPressed: _whisperDetailController.sendMsg,
onPressed: null, onPressed: null,
icon: Icon( icon: Icon(

View File

@@ -1,11 +1,16 @@
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:nil/nil.dart'; import 'package:nil/nil.dart';
import 'package:PiliPalaX/plugin/pl_player/index.dart'; import 'package:PiliPalaX/plugin/pl_player/index.dart';
import 'package:PiliPalaX/plugin/pl_player/widgets/play_pause_btn.dart'; import 'package:PiliPalaX/plugin/pl_player/widgets/play_pause_btn.dart';
import 'package:PiliPalaX/utils/feed_back.dart'; import 'package:PiliPalaX/utils/feed_back.dart';
import '../../../common/widgets/audio_video_progress_bar.dart';
import '../../../utils/utils.dart';
class BottomControl extends StatelessWidget implements PreferredSizeWidget { class BottomControl extends StatelessWidget implements PreferredSizeWidget {
final PlPlayerController? controller; final PlPlayerController? controller;
final Function? triggerFullScreen; final Function? triggerFullScreen;
@@ -23,7 +28,9 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
color: Colors.white, color: Colors.white,
fontSize: 12, fontSize: 12,
); );
//阅读器限制
Timer? _accessibilityDebounce;
double _lastAnnouncedValue = -1;
return Container( return Container(
color: Colors.transparent, color: Colors.transparent,
height: 90, height: 90,
@@ -41,31 +48,49 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
} }
return Padding( return Padding(
padding: const EdgeInsets.only(left: 7, right: 7, bottom: 6), padding: const EdgeInsets.only(left: 7, right: 7, bottom: 6),
child: ProgressBar( child: Semantics(
progress: Duration(seconds: value), // label: '${(value / max * 100).round()}%',
buffered: Duration(seconds: buffer), value: '${(value / max * 100).round()}%',
total: Duration(seconds: max), // enabled: false,
progressBarColor: colorTheme, child: ProgressBar(
baseBarColor: Colors.white.withOpacity(0.2), progress: Duration(seconds: value),
bufferedBarColor: colorTheme.withOpacity(0.4), buffered: Duration(seconds: buffer),
timeLabelLocation: TimeLabelLocation.none, total: Duration(seconds: max),
thumbColor: colorTheme, progressBarColor: colorTheme,
barHeight: 3.5, baseBarColor: Colors.white.withOpacity(0.2),
thumbRadius: 7, bufferedBarColor: colorTheme.withOpacity(0.4),
onDragStart: (duration) { timeLabelLocation: TimeLabelLocation.none,
feedBack(); thumbColor: colorTheme,
_.onChangedSliderStart(); barHeight: 3.5,
}, thumbRadius: 7,
onDragUpdate: (duration) { onDragStart: (duration) {
_.onUpdatedSliderProgress(duration.timeStamp); feedBack();
}, _.onChangedSliderStart();
onSeek: (duration) { },
_.onChangedSliderEnd(); onDragUpdate: (duration) {
_.onChangedSlider(duration.inSeconds.toDouble()); double newProgress = duration.timeStamp.inSeconds / max;
_.seekTo(Duration(seconds: duration.inSeconds), if ((newProgress - _lastAnnouncedValue).abs() > 0.02) {
type: 'slider'); _accessibilityDebounce?.cancel();
}, _accessibilityDebounce =
), Timer(const Duration(milliseconds: 200), () {
SemanticsService.announce(
"${(newProgress * 100).round()}%",
TextDirection.ltr);
_lastAnnouncedValue = newProgress;
});
}
_.onUpdatedSliderProgress(duration.timeStamp);
},
onSeek: (duration) {
_.onChangedSliderEnd();
_.onChangedSlider(duration.inSeconds.toDouble());
_.seekTo(Duration(seconds: duration.inSeconds),
type: 'slider');
SemanticsService.announce(
"${(duration.inSeconds / max * 100).round()}%",
TextDirection.ltr);
},
)),
); );
}, },
), ),
@@ -80,25 +105,26 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
// 播放时间 // 播放时间
Obx(() { Obx(() {
return Text( return Text(
_.durationSeconds.value >= 3600 Utils.timeFormat(_.positionSeconds.value),
? printDurationWithHours(
Duration(seconds: _.positionSeconds.value))
: printDuration(
Duration(seconds: _.positionSeconds.value)),
style: textStyle, style: textStyle,
semanticsLabel:
'已播放${Utils.durationReadFormat(Utils.timeFormat(_.positionSeconds.value))}',
); );
}), }),
const SizedBox(width: 2), const SizedBox(width: 2),
const Text('/', style: textStyle), const ExcludeSemantics(
child: Text(
'/',
style: textStyle,
),
),
const SizedBox(width: 2), const SizedBox(width: 2),
Obx( Obx(
() => Text( () => Text(
_.durationSeconds.value >= 3600 Utils.timeFormat(_.durationSeconds.value),
? printDurationWithHours(
Duration(seconds: _.durationSeconds.value))
: printDuration(
Duration(seconds: _.durationSeconds.value)),
style: textStyle, style: textStyle,
semanticsLabel:
'${Utils.durationReadFormat(Utils.timeFormat(_.durationSeconds.value))}',
), ),
), ),
const Spacer(), const Spacer(),
@@ -127,6 +153,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
width: 45, width: 45,
height: 30, height: 30,
child: IconButton( child: IconButton(
tooltip: '字幕',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
@@ -151,6 +178,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
() => Text( () => Text(
'${_.playbackSpeed}X', '${_.playbackSpeed}X',
style: const TextStyle(color: Colors.white, fontSize: 13), style: const TextStyle(color: Colors.white, fontSize: 13),
semanticsLabel: '${_.playbackSpeed}倍速',
), ),
), ),
), ),
@@ -159,16 +187,18 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
SizedBox( SizedBox(
width: 45, width: 45,
height: 30, height: 30,
child: ComBtn( child: Obx(() => ComBtn(
icon: Obx(() => Icon( tooltip: _.isFullScreen.value ? '退出全屏' : '全屏',
icon: Icon(
_.isFullScreen.value _.isFullScreen.value
? Icons.fullscreen_exit ? Icons.fullscreen_exit
: Icons.fullscreen, : Icons.fullscreen,
size: 19, size: 19,
color: Colors.white, color: Colors.white,
)), ),
fuc: () => triggerFullScreen!(status: !_.isFullScreen.value), fuc: () =>
), triggerFullScreen!(status: !_.isFullScreen.value),
)),
), ),
], ],
), ),

View File

@@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
class ComBtn extends StatelessWidget { class ComBtn extends StatelessWidget {
final Widget? icon; final Widget? icon;
final Function? fuc; final Function? fuc;
final String tooltip;
const ComBtn({ const ComBtn({
this.icon, this.icon,
this.fuc, this.fuc,
required this.tooltip,
super.key, super.key,
}); });
@@ -16,6 +18,7 @@ class ComBtn extends StatelessWidget {
width: 34, width: 34,
height: 34, height: 34,
child: IconButton( child: IconButton(
tooltip: tooltip,
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),

View File

@@ -67,6 +67,9 @@ class PlayOrPauseButtonState extends State<PlayOrPauseButton>
width: 34, width: 34,
height: 34, height: 34,
child: IconButton( child: IconButton(
tooltip: widget.controller!.videoPlayerController!.state.playing
? '暂停'
: '播放',
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),

View File

@@ -45,6 +45,76 @@ class Utils {
} }
} }
static String durationReadFormat(String duration) {
List<String> durationParts = duration.split(':');
if (durationParts.length == 3) {
if (durationParts[0] != '00') {
return '${int.parse(durationParts[0])}小时${durationParts[1]}分钟${durationParts[2]}';
}
durationParts.removeAt(0);
}
if (durationParts.length == 2) {
if (durationParts[0] != '00') {
return '${int.parse(durationParts[0])}分钟${durationParts[1]}';
}
durationParts.removeAt(0);
}
return '${int.parse(durationParts[0])}';
}
static String videoItemSemantics(dynamic videoItem) {
String semanticsLabel = "";
bool emptyStatCheck(dynamic stat) {
return stat == null ||
stat == '' ||
stat == 0 ||
stat == '0' ||
stat == '-';
}
if (videoItem.runtimeType.toString() == "RecVideoItemAppModel") {
if (videoItem.goto == 'picture') {
semanticsLabel += '动态,';
} else if (videoItem.goto == 'bangumi') {
semanticsLabel += '番剧,';
}
}
semanticsLabel += '${videoItem.title}';
if (!emptyStatCheck(videoItem.stat.view)) {
semanticsLabel += ',${Utils.numFormat(videoItem.stat.view)}';
semanticsLabel +=
(videoItem.runtimeType.toString() == "RecVideoItemAppModel" &&
videoItem.goto == 'picture')
? '浏览'
: '播放';
}
if (!emptyStatCheck(videoItem.stat.danmu)) {
semanticsLabel += ',${Utils.numFormat(videoItem.stat.danmu)}弹幕';
}
if (videoItem.rcmdReason != null && videoItem.rcmdReason.content != '') {
semanticsLabel += ',${videoItem.rcmdReason.content}';
}
if (!emptyStatCheck(videoItem.duration)) {
semanticsLabel +=
',时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}';
}
if (videoItem.runtimeType.toString() != "RecVideoItemAppModel" &&
videoItem.pubdate != null) {
semanticsLabel +=
',${Utils.dateFormat(videoItem.pubdate!, formatType: 'day')}';
}
if (videoItem.owner.name != '') {
semanticsLabel += ',Up主${videoItem.owner.name}';
}
if (videoItem.runtimeType.toString() == "RecVideoItemAppModel" ||
videoItem.runtimeType.toString() == "RecVideoItemModel" &&
videoItem.isFollowed == 1) {
semanticsLabel += ',已关注';
}
return semanticsLabel;
}
static String timeFormat(dynamic time) { static String timeFormat(dynamic time) {
// 1小时内 // 1小时内
if (time is String && time.contains(':')) { if (time is String && time.contains(':')) {
@@ -214,7 +284,8 @@ class Utils {
closestNumber = number; closestNumber = number;
} }
} }
} catch (_) {} finally { } catch (_) {
} finally {
closestNumber ??= numbers.last; closestNumber ??= numbers.last;
} }
return closestNumber; return closestNumber;
@@ -347,9 +418,8 @@ class Utils {
} }
static List<int> generateRandomBytes(int minLength, int maxLength) { static List<int> generateRandomBytes(int minLength, int maxLength) {
return List<int>.generate( return List<int>.generate(random.nextInt(maxLength - minLength + 1),
random.nextInt(maxLength-minLength+1), (_) => random.nextInt(0x60) + 0x20 (_) => random.nextInt(0x60) + 0x20);
);
} }
static String base64EncodeRandomString(int minLength, int maxLength) { static String base64EncodeRandomString(int minLength, int maxLength) {

View File

@@ -97,14 +97,6 @@ packages:
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "0.1.18" version: "0.1.18"
audio_video_progress_bar:
dependency: "direct main"
description:
name: audio_video_progress_bar
sha256: "3384875247cdbea748bd9ae8330631cd06a6cabfcda4945d45c9b406da92bc66"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.0.1"
auto_orientation: auto_orientation:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -98,8 +98,6 @@ dependencies:
screen_brightness: ^0.2.2+1 screen_brightness: ^0.2.2+1
wakelock_plus: ^1.1.1 wakelock_plus: ^1.1.1
universal_platform: ^1.0.0+1 universal_platform: ^1.0.0+1
# 进度条
audio_video_progress_bar: ^2.0.1
auto_orientation: ^2.3.1 auto_orientation: ^2.3.1
protobuf: ^3.0.0 protobuf: ^3.0.0
animations: ^2.0.8 animations: ^2.0.8