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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,93 +38,96 @@ class VideoCardH extends StatelessWidget {
final int aid = videoItem.aid;
final String bvid = videoItem.bvid;
final String heroTag = Utils.makeHeroTag(aid);
return GestureDetector(
onLongPress: () {
if (longPress != null) {
longPress!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
try {
final int cid =
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
} catch (err) {
SmartDialog.showToast(err.toString());
}
},
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,
)
],
),
);
return Semantics(
label: Utils.videoItemSemantics(videoItem),
excludeSemantics: true,
child: GestureDetector(
onLongPress: () {
if (longPress != null) {
longPress!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
try {
final int cid = videoItem.cid ??
await SearchHttp.ab2c(aid: aid, bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
} catch (err) {
SmartDialog.showToast(err.toString());
}
},
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.onSurface,
),
semanticsLabel: i['text'] as String,
),
]
],
@@ -235,7 +239,7 @@ class VideoContent extends StatelessWidget {
if (showDanmaku)
StatDanMu(
theme: 'gray',
danmu: videoItem.stat.danmaku as int,
danmu: videoItem.stat.danmu as int,
),
const Spacer(),
if (source == 'normal')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,9 +50,10 @@ class _AboutPageState extends State<AboutPage> {
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 150),
child: Image.asset(
child: ExcludeSemantics(
child: Image.asset(
'assets/images/logo/logo_android_2.png',
),
)),
),
ListTile(
title: Text('PiliPalaX',
@@ -65,6 +66,7 @@ class _AboutPageState extends State<AboutPage> {
'使用Flutter开发的哔哩哔哩第三方客户端',
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.outline),
semanticsLabel: '与你一起,发现不一样的世界',
),
),
Obx(
@@ -156,7 +158,7 @@ class _AboutPageState extends State<AboutPage> {
var cleanStatus = await CacheManage().clearCacheAll();
if (cleanStatus) {
getCacheSize();
SmartDialog.showToast('清除成功');
SmartDialog.showToast('清除成功');
}
},
title: const Text('清除缓存'),
@@ -207,7 +209,7 @@ class AboutController extends GetxController {
String buildNumber = currentInfo.buildNumber;
//if is android
if (Platform.isAndroid) {
buildNumber = buildNumber.substring(0,buildNumber.length - 1);
buildNumber = buildNumber.substring(0, buildNumber.length - 1);
}
currentVersion.value = "${currentInfo.version}+$buildNumber";
}
@@ -265,6 +267,7 @@ class AboutController extends GetxController {
),
);
}
// 问题反馈
feedback() {
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/utils/feed_back.dart';
import '../../../utils/utils.dart';
import 'controller.dart';
import 'widgets/intro_detail.dart';
@@ -192,6 +193,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
src: !widget.loadingStatus
? widget.bangumiDetail!.cover!
: bangumiItem!.cover!,
semanticsLabel: '封面',
),
if (bangumiItem != null &&
bangumiItem!.rating != null)
@@ -235,6 +237,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
width: 34,
height: 34,
child: IconButton(
tooltip: '收藏',
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
@@ -394,18 +397,19 @@ class _BangumiInfoState extends State<BangumiInfo> {
crossAxisCount: 5,
childAspectRatio: 1.25,
children: <Widget>[
Obx(
() => ActionItem(
Obx(() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap:
handleState(bangumiIntroController.actionLikeVideo),
selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: false,
semanticsLabel: '点赞',
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['likes']!.toString()
: bangumiItem!.stat!['likes']!.toString()),
),
? Utils.numFormat(
widget.bangumiDetail!.stat!['likes']!)
: Utils.numFormat(bangumiItem!.stat!['likes']!),
)),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
@@ -414,9 +418,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
handleState(bangumiIntroController.actionCoinVideo),
selectStatus: bangumiIntroController.hasCoin.value,
loadingStatus: false,
semanticsLabel: '投币',
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['coins']!.toString()
: bangumiItem!.stat!['coins']!.toString()),
? Utils.numFormat(widget.bangumiDetail!.stat!['coins']!)
: Utils.numFormat(bangumiItem!.stat!['coins']!)),
),
Obx(
() => ActionItem(
@@ -425,9 +430,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
onTap: () => showFavBottomSheet(),
selectStatus: bangumiIntroController.hasFav.value,
loadingStatus: false,
semanticsLabel: '收藏',
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['favorite']!.toString()
: bangumiItem!.stat!['favorite']!.toString()),
? Utils.numFormat(widget.bangumiDetail!.stat!['favorite']!)
: Utils.numFormat(bangumiItem!.stat!['favorite']!)),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.comment),
@@ -435,18 +441,20 @@ class _BangumiInfoState extends State<BangumiInfo> {
onTap: () => videoDetailCtr.tabCtr.animateTo(1),
selectStatus: false,
loadingStatus: false,
semanticsLabel: '评论',
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['reply']!.toString()
: bangumiItem!.stat!['reply']!.toString(),
? Utils.numFormat(widget.bangumiDetail!.stat!['reply']!)
: Utils.numFormat(bangumiItem!.stat!['reply']!),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => bangumiIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: false,
semanticsLabel: '转发',
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['share']!.toString()
: bangumiItem!.stat!['share']!.toString()),
? Utils.numFormat(widget.bangumiDetail!.stat!['share']!)
: Utils.numFormat(bangumiItem!.stat!['share']!)),
],
),
),

View File

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

View File

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

View File

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

View File

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

View File

@@ -87,6 +87,7 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
width: width,
height: width / StyleString.aspectRatio,
src: content.cover,
semanticsLabel: content.title,
),
),
if (content.badge != null && type == 'pgc')
@@ -133,7 +134,7 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
const SizedBox(width: 10),
Text(content.stat.play + '次围观'),
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,
maxLines: 1,
)
: Image.network(
e.emote![index].url!,
: NetworkImgLayer(
src: e.emote![index].url!,
width: size * 38,
height: size * 38,
semanticsLabel: e.emote![index].text!,
),
),
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -220,41 +220,46 @@ class UserInfoWidget extends StatelessWidget {
const SizedBox(width: 4),
ClipRect(
child: IconButton(
tooltip: '消息',
onPressed: () => Get.toNamed('/whisper'),
icon: const Icon(Icons.notifications_none),
icon: const Icon(
Icons.notifications_none,
),
),
)
],
const SizedBox(width: 8),
Obx(
() => userLogin.value
? Stack(
children: [
NetworkImgLayer(
type: 'avatar',
width: 34,
height: 34,
src: userFace,
),
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),
),
Semantics(
label: "我的",
child: Obx(
() => userLogin.value
? Stack(
children: [
NetworkImgLayer(
type: 'avatar',
width: 34,
height: 34,
src: userFace,
),
),
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,
height: 38,
child: IconButton(
tooltip: '默认用户头像',
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.resolveWith((states) {
@@ -409,6 +415,7 @@ class SearchBar extends StatelessWidget {
Icon(
Icons.search_outlined,
color: colorScheme.onSecondaryContainer,
semanticLabel: '搜索',
),
const SizedBox(width: 10),
Expanded(

View File

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

View File

@@ -136,6 +136,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
actions: [
const SizedBox(width: 4),
IconButton(
tooltip: '用内置浏览器打开',
onPressed: () {
Get.toNamed('/webview', parameters: {
'url': url.startsWith('http') ? url : 'https:$url',
@@ -148,6 +149,36 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
PopupMenuButton(
icon: const Icon(Icons.more_vert),
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(
onTap: () => {
Clipboard.setData(ClipboardData(text: url)),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,6 +90,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
width: 34,
height: 34,
child: IconButton(
tooltip: '返回',
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.resolveWith(
@@ -160,7 +161,8 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
reverse: true,
itemBuilder: (_, int i) {
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(
tooltip: '表情',
onPressed: () {
// if (toolbarType == 'input') {
// setState(() {
@@ -220,22 +223,25 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
.withOpacity(0.08),
borderRadius: BorderRadius.circular(40.0),
),
child: TextField(
readOnly: true,
style: Theme.of(context).textTheme.titleMedium,
controller: _replyContentController,
autofocus: false,
focusNode: replyContentFocusNode,
decoration: const InputDecoration(
border: InputBorder.none, // 移除默认边框
hintText: '开发中 ...', // 提示文本
contentPadding: EdgeInsets.symmetric(
horizontal: 16.0, vertical: 12.0), // 内边距
),
),
child: Semantics(
label: '私信输入框(开发中)',
child: TextField(
readOnly: true,
style: Theme.of(context).textTheme.titleMedium,
controller: _replyContentController,
autofocus: false,
focusNode: replyContentFocusNode,
decoration: const InputDecoration(
border: InputBorder.none, // 移除默认边框
hintText: '开发中 ...', // 提示文本
contentPadding: EdgeInsets.symmetric(
horizontal: 16.0, vertical: 12.0), // 内边距
),
)),
),
),
IconButton(
tooltip: '发送',
// onPressed: _whisperDetailController.sendMsg,
onPressed: null,
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/rendering.dart';
import 'package:get/get.dart';
import 'package:nil/nil.dart';
import 'package:PiliPalaX/plugin/pl_player/index.dart';
import 'package:PiliPalaX/plugin/pl_player/widgets/play_pause_btn.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 {
final PlPlayerController? controller;
final Function? triggerFullScreen;
@@ -23,7 +28,9 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
color: Colors.white,
fontSize: 12,
);
//阅读器限制
Timer? _accessibilityDebounce;
double _lastAnnouncedValue = -1;
return Container(
color: Colors.transparent,
height: 90,
@@ -41,31 +48,49 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
}
return Padding(
padding: const EdgeInsets.only(left: 7, right: 7, bottom: 6),
child: ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),
total: Duration(seconds: max),
progressBarColor: colorTheme,
baseBarColor: Colors.white.withOpacity(0.2),
bufferedBarColor: colorTheme.withOpacity(0.4),
timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme,
barHeight: 3.5,
thumbRadius: 7,
onDragStart: (duration) {
feedBack();
_.onChangedSliderStart();
},
onDragUpdate: (duration) {
_.onUpdatedSliderProgress(duration.timeStamp);
},
onSeek: (duration) {
_.onChangedSliderEnd();
_.onChangedSlider(duration.inSeconds.toDouble());
_.seekTo(Duration(seconds: duration.inSeconds),
type: 'slider');
},
),
child: Semantics(
// label: '${(value / max * 100).round()}%',
value: '${(value / max * 100).round()}%',
// enabled: false,
child: ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),
total: Duration(seconds: max),
progressBarColor: colorTheme,
baseBarColor: Colors.white.withOpacity(0.2),
bufferedBarColor: colorTheme.withOpacity(0.4),
timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme,
barHeight: 3.5,
thumbRadius: 7,
onDragStart: (duration) {
feedBack();
_.onChangedSliderStart();
},
onDragUpdate: (duration) {
double newProgress = duration.timeStamp.inSeconds / max;
if ((newProgress - _lastAnnouncedValue).abs() > 0.02) {
_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(() {
return Text(
_.durationSeconds.value >= 3600
? printDurationWithHours(
Duration(seconds: _.positionSeconds.value))
: printDuration(
Duration(seconds: _.positionSeconds.value)),
Utils.timeFormat(_.positionSeconds.value),
style: textStyle,
semanticsLabel:
'已播放${Utils.durationReadFormat(Utils.timeFormat(_.positionSeconds.value))}',
);
}),
const SizedBox(width: 2),
const Text('/', style: textStyle),
const ExcludeSemantics(
child: Text(
'/',
style: textStyle,
),
),
const SizedBox(width: 2),
Obx(
() => Text(
_.durationSeconds.value >= 3600
? printDurationWithHours(
Duration(seconds: _.durationSeconds.value))
: printDuration(
Duration(seconds: _.durationSeconds.value)),
Utils.timeFormat(_.durationSeconds.value),
style: textStyle,
semanticsLabel:
'${Utils.durationReadFormat(Utils.timeFormat(_.durationSeconds.value))}',
),
),
const Spacer(),
@@ -127,6 +153,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
width: 45,
height: 30,
child: IconButton(
tooltip: '字幕',
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
@@ -151,6 +178,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
() => Text(
'${_.playbackSpeed}X',
style: const TextStyle(color: Colors.white, fontSize: 13),
semanticsLabel: '${_.playbackSpeed}倍速',
),
),
),
@@ -159,16 +187,18 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
SizedBox(
width: 45,
height: 30,
child: ComBtn(
icon: Obx(() => Icon(
child: Obx(() => ComBtn(
tooltip: _.isFullScreen.value ? '退出全屏' : '全屏',
icon: Icon(
_.isFullScreen.value
? Icons.fullscreen_exit
: Icons.fullscreen,
size: 19,
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 {
final Widget? icon;
final Function? fuc;
final String tooltip;
const ComBtn({
this.icon,
this.fuc,
required this.tooltip,
super.key,
});
@@ -16,6 +18,7 @@ class ComBtn extends StatelessWidget {
width: 34,
height: 34,
child: IconButton(
tooltip: tooltip,
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),

View File

@@ -67,6 +67,9 @@ class PlayOrPauseButtonState extends State<PlayOrPauseButton>
width: 34,
height: 34,
child: IconButton(
tooltip: widget.controller!.videoPlayerController!.state.playing
? '暂停'
: '播放',
style: ButtonStyle(
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) {
// 1小时内
if (time is String && time.contains(':')) {
@@ -214,7 +284,8 @@ class Utils {
closestNumber = number;
}
}
} catch (_) {} finally {
} catch (_) {
} finally {
closestNumber ??= numbers.last;
}
return closestNumber;
@@ -347,9 +418,8 @@ class Utils {
}
static List<int> generateRandomBytes(int minLength, int maxLength) {
return List<int>.generate(
random.nextInt(maxLength-minLength+1), (_) => random.nextInt(0x60) + 0x20
);
return List<int>.generate(random.nextInt(maxLength - minLength + 1),
(_) => random.nextInt(0x60) + 0x20);
}
static String base64EncodeRandomString(int minLength, int maxLength) {