diff --git a/assets/images/lv/lv0.png b/assets/images/lv/lv0.png deleted file mode 100644 index f9ed49b62..000000000 Binary files a/assets/images/lv/lv0.png and /dev/null differ diff --git a/assets/images/lv/lv1.png b/assets/images/lv/lv1.png deleted file mode 100644 index ad8b70ce0..000000000 Binary files a/assets/images/lv/lv1.png and /dev/null differ diff --git a/assets/images/lv/lv2.png b/assets/images/lv/lv2.png deleted file mode 100644 index c369f5ada..000000000 Binary files a/assets/images/lv/lv2.png and /dev/null differ diff --git a/assets/images/lv/lv3.png b/assets/images/lv/lv3.png deleted file mode 100644 index 80a0db3fa..000000000 Binary files a/assets/images/lv/lv3.png and /dev/null differ diff --git a/assets/images/lv/lv4.png b/assets/images/lv/lv4.png deleted file mode 100644 index 5441967b2..000000000 Binary files a/assets/images/lv/lv4.png and /dev/null differ diff --git a/assets/images/lv/lv5.png b/assets/images/lv/lv5.png deleted file mode 100644 index dedd1309c..000000000 Binary files a/assets/images/lv/lv5.png and /dev/null differ diff --git a/assets/images/lv/lv6.png b/assets/images/lv/lv6.png deleted file mode 100644 index 399e585a7..000000000 Binary files a/assets/images/lv/lv6.png and /dev/null differ diff --git a/assets/images/lv/lv6_s.png b/assets/images/lv/lv6_s.png deleted file mode 100644 index 4b867500c..000000000 Binary files a/assets/images/lv/lv6_s.png and /dev/null differ diff --git a/lib/common/widgets/svg/level_icon.dart b/lib/common/widgets/svg/level_icon.dart new file mode 100644 index 000000000..e0379816d --- /dev/null +++ b/lib/common/widgets/svg/level_icon.dart @@ -0,0 +1,409 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; + +class UserLevel extends LeafRenderObjectWidget { + const UserLevel( + this.level, { + super.key, + this.height = 11, + this.flash = false, + }); + + final double height; + final int level; + final bool flash; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderLevel(height, level, flash); + } + + @override + void updateRenderObject( + BuildContext context, + RenderLevel renderObject, + ) { + renderObject + ..height = height + ..level = level + ..flash = flash; + } +} + +class RenderLevel extends RenderBox { + RenderLevel(this._height, this._level, this._flash); + + double _height; + set height(double value) { + if (_height == value) return; + _height = value; + markNeedsLayout(); + } + + int _level; + set level(int value) { + if (_level == value) return; + _level = value; + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + + bool _flash; + set flash(bool value) { + if (_flash == value) return; + _flash = value; + markNeedsLayout(); + } + + @override + Size computeDryLayout(covariant BoxConstraints constraints) { + return Size( + (_flash ? LevelCanvas._extendR : LevelCanvas._totalR) * + _height / + LevelCanvas._totalB, + _height, + ); + } + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + void paint(PaintingContext context, Offset offset) { + final paint = Paint()..color = lookupBackgroundColor(_level); + LevelCanvas(context.canvas) + ..save() + ..translate(offset.dx, offset.dy) + ..scale(_height / LevelCanvas._totalB) + ..drawLevelBack(paint, bolt: _flash) + ..drawLevelLv(paint..color = Colors.white) + ..drawLEDigit(_level, paint) + ..restore(); + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.label = '${_flash ? "硬核" : ""}$_level级'; + } + + static Color lookupBackgroundColor(int level) { + return switch (level) { + 0 || 1 => const Color(0xFFC0C0C0), + 2 => const Color(0xFF8BD29B), + 3 => const Color(0xFF7BCDEF), + 4 => const Color(0xFFFEBB8B), + 5 => const Color(0xFFEE672A), + _ => const Color(0xFFFF0000), + }; + } +} + +extension type LevelCanvas(Canvas _) implements Canvas { + // ========== 布局常量 ========== + static const _r = Radius.circular(20); + + static const double _left = 629; + static const double _right = 877; + static const double _colW = 68; // 竖段宽度 + static const double _lColR = _left + _colW; // 697 + static const double _rColL = _right - _colW; // 810 + + // 三条横线的边界 + static const double _rowH = 68; + static const double _rowSp = 146; + static const double _topY = 55; + static const double _topYB = _topY + _rowH; // 123 + static const double _midY = _topY + _rowSp; // 201 + static const double _midYB = _midY + _rowH; // 269 + static const double _botY = _midY + _rowSp; // 347 + static const double _botYB = _botY + _rowH; // 415 + + // 竖段拼接用的中心线 + static const double _midMid = (_midY + _midYB) / 2; // 235 + + static final _boltIcon = + (ParagraphBuilder( + ParagraphStyle( + fontSize: 460, + fontFamily: Icons.bolt_rounded.fontFamily, + height: 1, + fontWeight: FontWeight.w900, + textDirection: TextDirection.ltr, + ), + )..addText(.fromCharCode(Icons.bolt_rounded.codePoint))).build() + ..layout(const ParagraphConstraints(width: double.infinity)); + void drawBolt() => drawParagraph(_boltIcon, const Offset(840, 5)); + + void _draw1(Paint paint) { + drawRRect(const .fromLTRBXY(673, _botY, 833, _botYB, 20, 20), paint); + drawRRect( + .fromLTRBAndCorners( + 673, + _topY, + 787, + _topYB, + topLeft: _r, + bottomLeft: _r, + topRight: _r, + ), + paint, + ); + drawRect(const .fromLTRB(719, _topYB, 787, _botY), paint); + } + + void drawLEDigit(int digit, Paint paint) { + if (digit == 1) return _draw1(paint); + final bits = switch (digit) { + 0 => 0x7E, + 2 => 0x6D, + 3 => 0x79, + 4 => 0x33, + 5 => 0x5B, + 6 => 0x5F, + 7 => 0x70, + 8 => 0x7F, + 9 => 0x7B, + // _ => throw ArgumentError('Unsupported digit: $digit'), + _ => 0x4F, // `E` + }; + + _drawSegments( + bits & 0x40 != 0, + bits & 0x20 != 0, + bits & 0x10 != 0, + bits & 0x08 != 0, + bits & 0x04 != 0, + bits & 0x02 != 0, + bits & 0x01 != 0, + paint, + ); + } + + void _drawSegments( + bool a, + bool b, + bool c, + bool d, + bool e, + bool f, + bool g, + Paint paint, + ) { + // 横段 + if (a) { + _drawRRect( + _left, + _topY, + _right, + _topYB, + _r, + _r, + f ? .zero : _r, + b ? .zero : _r, + paint, + ); + } + if (g) { + _drawRRect( + _left, + _midY, + _right, + _midYB, + f ? .zero : _r, + b ? .zero : _r, + e ? .zero : _r, + c ? .zero : _r, + paint, + ); + } + if (d) { + _drawRRect( + _left, + _botY, + _right, + _botYB, + e ? .zero : _r, + c ? .zero : _r, + _r, + _r, + paint, + ); + } + + // 竖段 + // 左上竖段 f + if (f) { + final top = a ? _topYB : _topY; // 有上横则齐底,否则到顶 + final bottom = g ? _midY : (e ? _midMid : _midYB); + final rTop = a ? Radius.zero : _r; + final rBot = g || e ? Radius.zero : _r; + _drawRRect(_left, top, _lColR, bottom, rTop, rTop, rBot, rBot, paint); + } + + // 右上竖段 b + if (b) { + final top = a ? _topYB : _topY; + final bottom = g ? _midY : (c ? _midMid : _midYB); + final rTop = a ? Radius.zero : _r; + final rBot = g || c ? Radius.zero : _r; + _drawRRect(_rColL, top, _right, bottom, rTop, rTop, rBot, rBot, paint); + } + + // 左下竖段 e + if (e) { + final top = g ? _midYB : (f ? _midMid : _midY); + final bottom = d ? _botY : _botYB; + final rTop = g || f ? Radius.zero : _r; + final rBot = d ? Radius.zero : _r; + _drawRRect(_left, top, _lColR, bottom, rTop, rTop, rBot, rBot, paint); + } + + // 右下竖段 c + if (c) { + final top = g ? _midYB : (b ? _midMid : _midY); + final bottom = d ? _botY : _botYB; + final rTop = g || b ? Radius.zero : _r; + final rBot = d ? Radius.zero : _r; + _drawRRect(_rColL, top, _right, bottom, rTop, rTop, rBot, rBot, paint); + } + } + + /// 绘制圆角矩形,四角全零时退化为矩形 + void _drawRRect( + double l, + double t, + double r, + double b, + Radius tl, + Radius tr, + Radius bl, + Radius br, + Paint paint, + ) { + if (tl == .zero && tr == .zero && bl == .zero && br == .zero) { + drawRect(.fromLTRB(l, t, r, b), paint); + } else { + drawRRect( + .fromLTRBAndCorners( + l, + t, + r, + b, + topLeft: tl, + topRight: tr, + bottomLeft: bl, + bottomRight: br, + ), + paint, + ); + } + } + + static const double _vLeft = 296; + static final vV = Path() + ..moveTo(_vLeft, 282) + ..lineTo(_vLeft, 292) + ..arcToPoint( + const Offset(300, 313), + radius: const .circular(50), + clockwise: false, + ) + ..lineTo(395, 408) + ..arcToPoint( + const Offset(420, 415.5), // 避免拼接裂缝, 实为419.5, 415 + radius: const .circular(50), + clockwise: false, + ) + ..lineTo(420, 340.5) // 419.5, 340 + ..lineTo(363, 282) + ..close(); + + void drawLevelLv(Paint paint) { + const double lvTop = 106; + + drawRRect( + .fromLTRBAndCorners( + 56, + lvTop, + 123, + _botYB, + topLeft: _r, + topRight: _r, + bottomLeft: _r, + ), + paint, + ); + drawRRect( + .fromLTRBAndCorners( + 123, + _botY, + 256, + _botYB, + topRight: _r, + bottomRight: _r, + ), + paint, + ); + + final vL = RRect.fromLTRBAndCorners( + _vLeft, + lvTop, + 363, + 282, + topLeft: _r, + topRight: _r, + ); + + drawRRect(vL, paint); + + const double vSymmetryX = 419.5; + + drawPath(vV, paint); + save(); + translate(vSymmetryX, 0); + scale(-1, 1); + translate(-vSymmetryX, 0); + drawRRect(vL, paint); + drawPath(vV, paint); + restore(); + } + + static const double _totalR = 930; + static const double _extendR = 1250; + static const double _totalB = 466; + + void drawLevelBack(Paint paint, {bool bolt = false}) { + const radius = Radius.circular(27); + final double right = bolt ? _extendR : _totalR; + drawRRect( + RRect.fromLTRBAndCorners( + 0, + 48, + right, + _totalB, + topLeft: radius, + bottomLeft: radius, + bottomRight: radius, + ), + paint, + ); + drawRRect( + RRect.fromLTRBAndCorners( + 576, + 0, + right, + 48, + topLeft: radius, + topRight: radius, + ), + paint, + ); + + if (bolt) drawBolt(); + } +} diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index 4e064ce27..9a887b204 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -217,14 +217,10 @@ class UserInfoCard extends StatelessWidget { ), ), ), - Image.asset( - BiliUtils.levelName( - card.levelInfo!.currentLevel!, - isSeniorMember: card.levelInfo?.identity == 2, - ), + BiliUtils.levelPicture( + card.levelInfo!.currentLevel!, + isSeniorMember: card.levelInfo?.identity == 2, height: 11, - cacheHeight: 11.cacheSize(context), - semanticLabel: '等级${card.levelInfo?.currentLevel}', ), if (card.vip?.status == 1) Container( diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 4a86bceed..7712abb5c 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -315,13 +315,10 @@ class _MediaPageState extends CommonPageState overflow: .ellipsis, ), ), - Image.asset( - BiliUtils.levelName( - levelInfo?.currentLevel ?? 0, - isSeniorMember: userInfo.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + levelInfo?.currentLevel ?? 0, + isSeniorMember: userInfo.isSeniorMember == 1, height: 10, - cacheHeight: 10.cacheSize(context), ), ], ), diff --git a/lib/pages/pgc_review/child/view.dart b/lib/pages/pgc_review/child/view.dart index a227d299b..6718180e1 100644 --- a/lib/pages/pgc_review/child/view.dart +++ b/lib/pages/pgc_review/child/view.dart @@ -14,7 +14,6 @@ import 'package:PiliPlus/pages/pgc_review/child/controller.dart'; import 'package:PiliPlus/pages/pgc_review/post/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; @@ -244,10 +243,9 @@ class _PgcReviewChildPageState extends State fontSize: 13, ), ), - Image.asset( - BiliUtils.levelName(item.author!.level!), + BiliUtils.levelPicture( + item.author!.level!, height: 11, - cacheHeight: 11.cacheSize(context), ), ], ), diff --git a/lib/pages/search_panel/user/widgets/item.dart b/lib/pages/search_panel/user/widgets/item.dart index fdb45feeb..4fc1d7ac9 100644 --- a/lib/pages/search_panel/user/widgets/item.dart +++ b/lib/pages/search_panel/user/widgets/item.dart @@ -1,7 +1,6 @@ import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get_core/src/get_main.dart'; @@ -52,14 +51,10 @@ class SearchUserItem extends StatelessWidget { ), ), const SizedBox(width: 6), - Image.asset( - BiliUtils.levelName( - item.level!, - isSeniorMember: item.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + item.level!, + isSeniorMember: item.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), - semanticLabel: '等级${item.level}', ), ], ), diff --git a/lib/pages/video/member/view.dart b/lib/pages/video/member/view.dart index 0893c101e..4fe42183c 100644 --- a/lib/pages/video/member/view.dart +++ b/lib/pages/video/member/view.dart @@ -20,7 +20,6 @@ import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart'; import 'package:PiliPlus/pages/video/member/controller.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -257,13 +256,10 @@ class _HorizontalMemberPageState extends State { ), ), const SizedBox(width: 8), - Image.asset( - BiliUtils.levelName( - memberInfoModel.level!, - isSeniorMember: memberInfoModel.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + memberInfoModel.level!, + isSeniorMember: memberInfoModel.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), ), ], ), diff --git a/lib/pages/video/note/view.dart b/lib/pages/video/note/view.dart index cdd3c97ca..ca8bdab95 100644 --- a/lib/pages/video/note/view.dart +++ b/lib/pages/video/note/view.dart @@ -10,7 +10,6 @@ import 'package:PiliPlus/pages/video/note/controller.dart'; import 'package:PiliPlus/pages/webview/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; @@ -263,13 +262,10 @@ class _NoteListPageState extends State ), ), const SizedBox(width: 6), - Image.asset( - BiliUtils.levelName( - item.author!.level!, - isSeniorMember: item.author!.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + item.author!.level!, + isSeniorMember: item.author!.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), ), ], ), diff --git a/lib/pages/video/reply/widgets/reply_item_grpc.dart b/lib/pages/video/reply/widgets/reply_item_grpc.dart index c4e1c3c89..7da217e0c 100644 --- a/lib/pages/video/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/reply/widgets/reply_item_grpc.dart @@ -184,13 +184,10 @@ class ReplyItemGrpc extends StatelessWidget { ), ), ), - Image.asset( - BiliUtils.levelName( - member.level, - isSeniorMember: member.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + member.level.toInt(), + isSeniorMember: member.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), ), if (replyItem.mid == upMid) const PBadge( diff --git a/lib/utils/bili_utils.dart b/lib/utils/bili_utils.dart index 96779e859..1555079c1 100644 --- a/lib/utils/bili_utils.dart +++ b/lib/utils/bili_utils.dart @@ -1,3 +1,6 @@ +import 'package:PiliPlus/common/widgets/svg/level_icon.dart'; +import 'package:flutter/material.dart'; + abstract final class BiliUtils { static bool isDefaultFav(int? attr) { if (attr == null) { @@ -21,8 +24,12 @@ abstract final class BiliUtils { return tagid != null && tagid != 0 && tagid != -10 && tagid != -2; } - static String levelName( - Object level, { + // https://s1.hdslb.com/bfs/svg-next/font/2025-10-27/freshspace-zpjpp3aqht.css + static Widget levelPicture( + int level, { bool isSeniorMember = false, - }) => 'assets/images/lv/lv${isSeniorMember ? '6_s' : level}.png'; + double height = 11, + }) { + return UserLevel(level, height: height, flash: isSeniorMember); + } } diff --git a/pubspec.yaml b/pubspec.yaml index c8c3e8ae0..79f61bb12 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -270,7 +270,6 @@ flutter: assets: - path: assets/images/ - - path: assets/images/lv/ - path: assets/images/logo/ - path: assets/images/logo/ico/ platforms: [windows]