mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-22 03:31:09 +08:00
mod: 无障碍语义适配
This commit is contained in:
@@ -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') {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -31,6 +31,7 @@ class StatDanMu extends StatelessWidget {
|
||||
fontSize: size == 'medium' ? 12 : 11,
|
||||
color: color,
|
||||
),
|
||||
semanticsLabel: '${Utils.numFormat(danmu!)}条弹幕',
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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" ? "浏览" : "播放"}',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user