mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
committed by
GitHub
parent
84f7f14a29
commit
08a33d9ce5
@@ -38,7 +38,7 @@ typedef IndexedFocusedWidgetBuilder =
|
||||
|
||||
typedef IndexedTagStringBuilder = String Function(int index);
|
||||
|
||||
class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||
class InteractiveviewerGallery extends StatefulWidget {
|
||||
const InteractiveviewerGallery({
|
||||
super.key,
|
||||
required this.sources,
|
||||
|
||||
348
lib/common/widgets/marquee.dart
Normal file
348
lib/common/widgets/marquee.dart
Normal file
@@ -0,0 +1,348 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class MarqueeText extends StatelessWidget {
|
||||
final double maxWidth;
|
||||
final String text;
|
||||
final TextStyle? style;
|
||||
|
||||
const MarqueeText(
|
||||
this.text, {
|
||||
required this.maxWidth,
|
||||
this.style,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: style,
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
)..layout(maxWidth: maxWidth);
|
||||
final child = Text(
|
||||
text,
|
||||
style: style,
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
if (textPainter.didExceedMaxLines) {
|
||||
return SingleWidgetMarquee(
|
||||
child,
|
||||
duration: const Duration(seconds: 5),
|
||||
bounce: true,
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SingleWidgetMarquee extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration? duration;
|
||||
final bool bounce;
|
||||
final double spacing;
|
||||
|
||||
const SingleWidgetMarquee(
|
||||
this.child, {
|
||||
super.key,
|
||||
this.duration,
|
||||
this.bounce = false,
|
||||
this.spacing = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SingleWidgetMarqueeState();
|
||||
}
|
||||
|
||||
class _SingleWidgetMarqueeState extends State<SingleWidgetMarquee>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final _controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
reverseDuration: widget.duration,
|
||||
)..repeat(reverse: widget.bounce);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.bounce
|
||||
? BounceMarquee(
|
||||
animation: _controller,
|
||||
spacing: widget.spacing,
|
||||
child: widget.child,
|
||||
)
|
||||
: NormalMarquee(
|
||||
animation: _controller,
|
||||
spacing: widget.spacing,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Marquee extends SingleChildRenderObjectWidget {
|
||||
final Axis direction;
|
||||
final Clip clipBehavior;
|
||||
final double spacing;
|
||||
final Animation<double> animation;
|
||||
|
||||
const Marquee({
|
||||
super.key,
|
||||
required this.animation,
|
||||
required super.child,
|
||||
this.direction = Axis.horizontal,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.spacing = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
covariant MarqueeRender renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..direction = direction
|
||||
..clipBehavior = clipBehavior
|
||||
..animation = animation
|
||||
..spacing = spacing;
|
||||
}
|
||||
}
|
||||
|
||||
class NormalMarquee extends Marquee {
|
||||
const NormalMarquee({
|
||||
super.key,
|
||||
required super.animation,
|
||||
required super.child,
|
||||
super.direction,
|
||||
super.clipBehavior,
|
||||
super.spacing,
|
||||
});
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) => _NormalMarqueeRender(
|
||||
direction: direction,
|
||||
animation: animation,
|
||||
clipBehavior: clipBehavior,
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
class BounceMarquee extends Marquee {
|
||||
const BounceMarquee({
|
||||
super.key,
|
||||
required super.animation,
|
||||
required super.child,
|
||||
super.direction,
|
||||
super.clipBehavior,
|
||||
super.spacing,
|
||||
});
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) => _BounceMarqueeRender(
|
||||
direction: direction,
|
||||
animation: animation,
|
||||
clipBehavior: clipBehavior,
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
abstract class MarqueeRender extends RenderBox
|
||||
with RenderObjectWithChildMixin<RenderBox> {
|
||||
MarqueeRender({
|
||||
required Axis direction,
|
||||
required Animation<double> animation,
|
||||
required this.clipBehavior,
|
||||
required this.spacing,
|
||||
}) : _direction = direction,
|
||||
_animation = animation,
|
||||
assert(spacing.isFinite && !spacing.isNaN);
|
||||
|
||||
Clip clipBehavior;
|
||||
double spacing;
|
||||
|
||||
Axis _direction;
|
||||
Axis get direction => _direction;
|
||||
set direction(Axis value) {
|
||||
if (_direction == value) return;
|
||||
_direction = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
Animation<double> _animation;
|
||||
Animation<double> get animation => _animation;
|
||||
set animation(Animation<double> value) {
|
||||
if (_animation == value) return;
|
||||
if (_listened) {
|
||||
_animation.removeListener(markNeedsPaint);
|
||||
value.addListener(markNeedsPaint);
|
||||
}
|
||||
_animation = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
_removeListener();
|
||||
super.detach();
|
||||
}
|
||||
|
||||
bool _listened = false;
|
||||
void _addListener() {
|
||||
if (!_listened) {
|
||||
_animation.addListener(markNeedsPaint);
|
||||
_listened = true;
|
||||
}
|
||||
}
|
||||
|
||||
void _removeListener() {
|
||||
if (_listened) {
|
||||
_animation.removeListener(markNeedsPaint);
|
||||
_listened = false;
|
||||
}
|
||||
}
|
||||
|
||||
late double _distance;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final child = this.child;
|
||||
if (child == null) {
|
||||
size = constraints.smallest;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_direction == Axis.horizontal) {
|
||||
child.layout(
|
||||
BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
parentUsesSize: true,
|
||||
);
|
||||
size = constraints.constrain(child.size);
|
||||
_distance = child.size.width - size.width;
|
||||
if (spacing.isNegative) spacing *= -size.width;
|
||||
} else {
|
||||
child.layout(
|
||||
BoxConstraints(maxWidth: constraints.maxWidth),
|
||||
parentUsesSize: true,
|
||||
);
|
||||
size = constraints.constrain(child.size);
|
||||
_distance = child.size.height - size.height;
|
||||
if (spacing.isNegative) spacing *= -size.height;
|
||||
}
|
||||
if (_distance > 0) {
|
||||
_addListener();
|
||||
} else {
|
||||
_removeListener();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
|
||||
void paintCenter(PaintingContext context, Offset offset) {
|
||||
if (_direction == Axis.horizontal) {
|
||||
context.paintChild(child!, Offset(offset.dx - _distance / 2, offset.dy));
|
||||
} else {
|
||||
context.paintChild(child!, Offset(offset.dx, offset.dy - _distance / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _BounceMarqueeRender extends MarqueeRender {
|
||||
_BounceMarqueeRender({
|
||||
required super.direction,
|
||||
required super.animation,
|
||||
required super.clipBehavior,
|
||||
required super.spacing,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child == null) return;
|
||||
|
||||
final tick = _animation.value;
|
||||
|
||||
if (_distance > 0) {
|
||||
final helfSpacing = spacing / 2.0;
|
||||
void paintChild() {
|
||||
if (_direction == Axis.horizontal) {
|
||||
context.paintChild(
|
||||
child!,
|
||||
Offset(
|
||||
offset.dx + helfSpacing - tick * (_distance + spacing),
|
||||
offset.dy,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
context.paintChild(
|
||||
child!,
|
||||
Offset(
|
||||
offset.dx,
|
||||
offset.dy + helfSpacing - tick * (_distance + spacing),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (clipBehavior == Clip.none) {
|
||||
paintChild();
|
||||
} else {
|
||||
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
|
||||
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
|
||||
}
|
||||
} else {
|
||||
paintCenter(context, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _NormalMarqueeRender extends MarqueeRender {
|
||||
_NormalMarqueeRender({
|
||||
required super.direction,
|
||||
required super.animation,
|
||||
required super.clipBehavior,
|
||||
required super.spacing,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final child = this.child;
|
||||
if (child == null) return;
|
||||
|
||||
final tick = _animation.value;
|
||||
|
||||
if (_distance > 0) {
|
||||
void paintChild() {
|
||||
if (_direction == Axis.horizontal) {
|
||||
final w = child.size.width + spacing;
|
||||
final dx = tick * w;
|
||||
context.paintChild(child, Offset(offset.dx - dx, offset.dy));
|
||||
if (dx > _distance) {
|
||||
context.paintChild(child, Offset(offset.dx + w - dx, offset.dy));
|
||||
}
|
||||
} else {
|
||||
final h = child.size.height + spacing;
|
||||
final dy = tick * h;
|
||||
context.paintChild(child, Offset(offset.dx, offset.dy - dy));
|
||||
if (dy > _distance) {
|
||||
context.paintChild(child, Offset(offset.dx, offset.dy + h - dy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clipBehavior == Clip.none) {
|
||||
paintChild();
|
||||
} else {
|
||||
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
|
||||
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
|
||||
}
|
||||
} else {
|
||||
paintCenter(context, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -951,4 +951,12 @@ class Api {
|
||||
|
||||
static const String loginDevices =
|
||||
'${HttpString.passBaseUrl}/x/safecenter/user_login_devices';
|
||||
|
||||
static const String bgmDetail = '/x/copyright-music-publicity/bgm/detail';
|
||||
|
||||
static const String wishUpdate =
|
||||
'/x/copyright-music-publicity/bgm/wish/update';
|
||||
|
||||
static const String bgmRecommend =
|
||||
'/x/copyright-music-publicity/bgm/recommend_list';
|
||||
}
|
||||
|
||||
65
lib/http/music.dart
Normal file
65
lib/http/music.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:PiliPlus/http/api.dart';
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_detail.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_recommend_list.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
import 'package:PiliPlus/utils/wbi_sign.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
class MusicHttp {
|
||||
static Future<LoadingState<MusicDetail>> bgmDetail(String musicId) async {
|
||||
final res = await Request().get(
|
||||
Api.bgmDetail,
|
||||
queryParameters: await WbiSign.makSign({
|
||||
'music_id': musicId,
|
||||
'relation_from': 'bgm_page',
|
||||
}),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return Success(MusicDetail.fromJson(res.data['data']));
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<Null>> wishUpdate(
|
||||
String musicId,
|
||||
bool hasLike,
|
||||
) async {
|
||||
final res = await Request().post(
|
||||
Api.wishUpdate,
|
||||
data: {
|
||||
'music_id': musicId,
|
||||
'state': hasLike ? 2 : 1,
|
||||
'csrf': Accounts.main.csrf,
|
||||
},
|
||||
options: Options(contentType: Headers.formUrlEncodedContentType),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return const Success(null);
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<List<BgmRecommend>?>> bgmRecommend(
|
||||
String musicId,
|
||||
) async {
|
||||
final res = await Request().get(
|
||||
Api.bgmRecommend,
|
||||
queryParameters: {
|
||||
'music_id': musicId,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return Success(
|
||||
(res.data['data']?['list'] as List?)
|
||||
?.map((i) => BgmRecommend.fromJson(i))
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
272
lib/models_new/music/bgm_detail.dart
Normal file
272
lib/models_new/music/bgm_detail.dart
Normal file
@@ -0,0 +1,272 @@
|
||||
import 'package:PiliPlus/models/model_owner.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
|
||||
class MusicDetail {
|
||||
MusicDetail({
|
||||
required this.musicTitle,
|
||||
required this.originArtist,
|
||||
required this.originArtistList,
|
||||
required this.mvAid,
|
||||
required this.mvCid,
|
||||
required this.mvBvid,
|
||||
required this.mvIndexOrder,
|
||||
required this.mvFav,
|
||||
required this.mvLikes,
|
||||
required this.mvShares,
|
||||
required this.mvCover,
|
||||
required this.bgColor,
|
||||
required this.mvLyric,
|
||||
required this.supportListen,
|
||||
required this.wishListen,
|
||||
required this.wishCount,
|
||||
required this.musicShares,
|
||||
required this.musicSource,
|
||||
required this.album,
|
||||
required this.artists,
|
||||
required this.artistsList,
|
||||
required this.listenPv,
|
||||
required this.achievement,
|
||||
required this.musicRank,
|
||||
required this.maxListId,
|
||||
required this.showChosen,
|
||||
required this.hotSongHeat,
|
||||
required this.hotSongRank,
|
||||
required this.creationRank,
|
||||
required this.musicOutUrl,
|
||||
// required this.abTest,
|
||||
required this.musicComment,
|
||||
// required this.musicMaterial,
|
||||
required this.isNextgenActivity,
|
||||
required this.isOriginal,
|
||||
required this.musicHot,
|
||||
required this.musicRelation,
|
||||
required this.musicPublish,
|
||||
// required this.musicAchievementTimeline,
|
||||
required this.flowAttr,
|
||||
});
|
||||
|
||||
final String? musicTitle;
|
||||
final String? originArtist;
|
||||
final String? originArtistList;
|
||||
final int? mvAid;
|
||||
final int? mvCid;
|
||||
final String? mvBvid;
|
||||
final int? mvIndexOrder;
|
||||
final int? mvFav;
|
||||
final int? mvLikes;
|
||||
final int? mvShares;
|
||||
final String? mvCover;
|
||||
final String? bgColor;
|
||||
final String? mvLyric;
|
||||
final bool? supportListen;
|
||||
bool? wishListen;
|
||||
int? wishCount;
|
||||
final int? musicShares;
|
||||
final String? musicSource;
|
||||
final String? album;
|
||||
final List<Artist>? artists;
|
||||
final List<Artist>? artistsList;
|
||||
final int? listenPv;
|
||||
final List<String>? achievement;
|
||||
final String? musicRank;
|
||||
final int? maxListId;
|
||||
final bool? showChosen;
|
||||
final HotSongHeat? hotSongHeat;
|
||||
final Rank? hotSongRank;
|
||||
final Rank? creationRank;
|
||||
final String? musicOutUrl;
|
||||
// final dynamic abTest;
|
||||
final MusicComment? musicComment;
|
||||
// final dynamic musicMaterial;
|
||||
final int? isNextgenActivity;
|
||||
final int? isOriginal;
|
||||
final int? musicHot;
|
||||
final int? musicRelation;
|
||||
final String? musicPublish;
|
||||
// final List<dynamic>? musicAchievementTimeline;
|
||||
final FlowAttr? flowAttr;
|
||||
|
||||
factory MusicDetail.fromJson(Map<String, dynamic> json) {
|
||||
return MusicDetail(
|
||||
musicTitle: json["music_title"],
|
||||
originArtist: json["origin_artist"],
|
||||
originArtistList: json["origin_artist_list"],
|
||||
mvAid: json["mv_aid"],
|
||||
mvCid: json["mv_cid"],
|
||||
mvBvid: json["mv_bvid"],
|
||||
mvIndexOrder: json["mv_index_order"],
|
||||
mvFav: json["mv_fav"],
|
||||
mvLikes: json["mv_likes"],
|
||||
mvShares: json["mv_shares"],
|
||||
mvCover: json["mv_cover"],
|
||||
bgColor: json["bg_color"],
|
||||
mvLyric: json["mv_lyric"],
|
||||
supportListen: json["support_listen"],
|
||||
wishListen: json["wish_listen"],
|
||||
wishCount: json["wish_count"],
|
||||
musicShares: json["music_shares"],
|
||||
musicSource: json["music_source"],
|
||||
album: json["album"],
|
||||
artists: (json["artists"] as List?)
|
||||
?.map((x) => Artist.fromJson(x))
|
||||
.toList(),
|
||||
artistsList: (json["artists_list"] as List?)
|
||||
?.map((x) => Artist.fromJson(x))
|
||||
.toList(),
|
||||
listenPv: json["listen_pv"],
|
||||
achievement: (json["achievement"] as List?)?.fromCast(),
|
||||
musicRank: json["music_rank"],
|
||||
maxListId: json["max_list_id"],
|
||||
showChosen: json["show_chosen"],
|
||||
hotSongHeat: json["hot_song_heat"] == null
|
||||
? null
|
||||
: HotSongHeat.fromJson(json["hot_song_heat"]),
|
||||
hotSongRank: json["hot_song_rank"] == null
|
||||
? null
|
||||
: Rank.fromJson(json["hot_song_rank"]),
|
||||
creationRank: json["creation_rank"] == null
|
||||
? null
|
||||
: Rank.fromJson(json["creation_rank"]),
|
||||
musicOutUrl: json["music_out_url"],
|
||||
musicComment: json["music_comment"] == null
|
||||
? null
|
||||
: MusicComment.fromJson(json["music_comment"]),
|
||||
isNextgenActivity: json["is_nextgen_activity"],
|
||||
isOriginal: json["is_original"],
|
||||
musicHot: json["music_hot"],
|
||||
musicRelation: json["music_relation"],
|
||||
musicPublish: json["music_publish"],
|
||||
flowAttr: json["flow_attr"] == null
|
||||
? null
|
||||
: FlowAttr.fromJson(json["flow_attr"]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Artist extends Owner {
|
||||
String? identity;
|
||||
int? identifyType;
|
||||
|
||||
Artist.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
|
||||
identity = json["identity"];
|
||||
identifyType = json["identify_type"];
|
||||
}
|
||||
}
|
||||
|
||||
class Rank {
|
||||
Rank({
|
||||
required this.lastUpdate,
|
||||
required this.highestRank,
|
||||
required this.onListTimes,
|
||||
required this.listDetail,
|
||||
});
|
||||
|
||||
final int? lastUpdate;
|
||||
final int? highestRank;
|
||||
final int? onListTimes;
|
||||
final List<ListDetail>? listDetail;
|
||||
|
||||
factory Rank.fromJson(Map<String, dynamic> json) {
|
||||
return Rank(
|
||||
lastUpdate: json["last_update"],
|
||||
highestRank: json["highest_rank"],
|
||||
onListTimes: json["on_list_times"],
|
||||
listDetail: (json["list_detail"] as List?)
|
||||
?.map((x) => ListDetail.fromJson(x))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ListDetail {
|
||||
ListDetail({
|
||||
required this.date,
|
||||
required this.rank,
|
||||
});
|
||||
|
||||
final int? date;
|
||||
final int? rank;
|
||||
|
||||
factory ListDetail.fromJson(Map<String, dynamic> json) {
|
||||
return ListDetail(
|
||||
date: json["date"],
|
||||
rank: json["rank"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlowAttr {
|
||||
FlowAttr({
|
||||
required this.noShare,
|
||||
required this.noComment,
|
||||
});
|
||||
|
||||
final bool? noShare;
|
||||
final bool? noComment;
|
||||
|
||||
factory FlowAttr.fromJson(Map<String, dynamic> json) {
|
||||
return FlowAttr(
|
||||
noShare: json["no_share"],
|
||||
noComment: json["no_comment"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HotSongHeat {
|
||||
HotSongHeat({
|
||||
required this.lastHeat,
|
||||
required this.songHeat,
|
||||
});
|
||||
|
||||
final int? lastHeat;
|
||||
final List<SongHeat>? songHeat;
|
||||
|
||||
factory HotSongHeat.fromJson(Map<String, dynamic> json) {
|
||||
return HotSongHeat(
|
||||
lastHeat: json["last_heat"],
|
||||
songHeat: (json["song_heat"] as List?)?.reversed
|
||||
.map((x) => SongHeat.fromJson(x))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SongHeat {
|
||||
SongHeat({
|
||||
required this.date,
|
||||
required this.heat,
|
||||
});
|
||||
|
||||
final int date;
|
||||
final int heat;
|
||||
|
||||
factory SongHeat.fromJson(Map<String, dynamic> json) {
|
||||
return SongHeat(
|
||||
date: json["date"],
|
||||
heat: json["heat"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MusicComment {
|
||||
MusicComment({
|
||||
required this.state,
|
||||
required this.nums,
|
||||
required this.oid,
|
||||
required this.pageType,
|
||||
});
|
||||
|
||||
final int? state;
|
||||
final int? nums;
|
||||
final int? oid;
|
||||
final int? pageType;
|
||||
|
||||
factory MusicComment.fromJson(Map<String, dynamic> json) {
|
||||
return MusicComment(
|
||||
state: json["state"],
|
||||
nums: json["nums"],
|
||||
oid: json["oid"],
|
||||
pageType: json["page_type"],
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/models_new/music/bgm_recommend_list.dart
Normal file
126
lib/models_new/music/bgm_recommend_list.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
class BgmRecommend {
|
||||
BgmRecommend({
|
||||
required this.aid,
|
||||
required this.bvid,
|
||||
required this.indexOrder,
|
||||
required this.cid,
|
||||
required this.cover,
|
||||
required this.title,
|
||||
required this.mid,
|
||||
required this.upNickName,
|
||||
required this.play,
|
||||
required this.vt,
|
||||
required this.isVt,
|
||||
required this.danmu,
|
||||
required this.duration,
|
||||
required this.label,
|
||||
required this.labelList,
|
||||
required this.isTop,
|
||||
required this.showType,
|
||||
required this.clickType,
|
||||
required this.jumpUrl,
|
||||
required this.vtDisplay,
|
||||
required this.aidSource,
|
||||
required this.tid,
|
||||
required this.subTid,
|
||||
required this.subTagName,
|
||||
required this.coverMark,
|
||||
// required this.districtLabel,
|
||||
});
|
||||
|
||||
final int? aid;
|
||||
final String? bvid;
|
||||
final int? indexOrder;
|
||||
final int? cid;
|
||||
final String? cover;
|
||||
final String? title;
|
||||
final int? mid;
|
||||
final String? upNickName;
|
||||
final int? play;
|
||||
final int? vt;
|
||||
final int? isVt;
|
||||
final int? danmu;
|
||||
final int? duration;
|
||||
final String? label;
|
||||
final List<LabelList>? labelList;
|
||||
final bool? isTop;
|
||||
final int? showType;
|
||||
final int? clickType;
|
||||
final String? jumpUrl;
|
||||
final String? vtDisplay;
|
||||
final int? aidSource;
|
||||
final int? tid;
|
||||
final int? subTid;
|
||||
final String? subTagName;
|
||||
final CoverMark? coverMark;
|
||||
// final dynamic districtLabel;
|
||||
|
||||
factory BgmRecommend.fromJson(Map<String, dynamic> json) {
|
||||
return BgmRecommend(
|
||||
aid: json["aid"],
|
||||
bvid: json["bvid"],
|
||||
indexOrder: json["index_order"],
|
||||
cid: json["cid"],
|
||||
cover: json["cover"],
|
||||
title: json["title"],
|
||||
mid: json["mid"],
|
||||
upNickName: json["up_nick_name"],
|
||||
play: json["play"],
|
||||
vt: json["vt"],
|
||||
isVt: json["is_vt"],
|
||||
danmu: json["danmu"],
|
||||
duration: json["duration"],
|
||||
label: json["label"],
|
||||
labelList: (json["label_list"] as List?)
|
||||
?.map((x) => LabelList.fromJson(x))
|
||||
.toList(),
|
||||
isTop: json["is_top"],
|
||||
showType: json["show_type"],
|
||||
clickType: json["click_type"],
|
||||
jumpUrl: json["jump_url"],
|
||||
vtDisplay: json["vt_display"],
|
||||
aidSource: json["aid_source"],
|
||||
tid: json["tid"],
|
||||
subTid: json["sub_tid"],
|
||||
subTagName: json["sub_tag_name"],
|
||||
coverMark: json["cover_mark"] == null
|
||||
? null
|
||||
: CoverMark.fromJson(json["cover_mark"]),
|
||||
// districtLabel: json["district_label"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CoverMark {
|
||||
CoverMark({
|
||||
required this.name,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
final String? name;
|
||||
final String? value;
|
||||
|
||||
factory CoverMark.fromJson(Map<String, dynamic> json) {
|
||||
return CoverMark(
|
||||
name: json["name"],
|
||||
value: json["value"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LabelList {
|
||||
LabelList({
|
||||
required this.name,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
final String? name;
|
||||
final int? value;
|
||||
|
||||
factory LabelList.fromJson(Map<String, dynamic> json) {
|
||||
return LabelList(
|
||||
name: json["name"],
|
||||
value: json["value"],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
class VideoTagItem {
|
||||
int? tagId;
|
||||
String? tagName;
|
||||
String? tagType;
|
||||
String? musicId;
|
||||
String? jumpUrl;
|
||||
|
||||
VideoTagItem({
|
||||
this.tagId,
|
||||
this.tagName,
|
||||
this.tagType,
|
||||
this.musicId,
|
||||
this.jumpUrl,
|
||||
});
|
||||
@@ -14,6 +16,7 @@ class VideoTagItem {
|
||||
factory VideoTagItem.fromJson(Map<String, dynamic> json) => VideoTagItem(
|
||||
tagId: json["tag_id"],
|
||||
tagName: json["tag_name"],
|
||||
tagType: json["tag_type"],
|
||||
musicId: json["music_id"],
|
||||
jumpUrl: json["jump_url"],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show MainListReply, ReplyInfo;
|
||||
import 'package:PiliPlus/http/dynamics.dart';
|
||||
import 'package:PiliPlus/http/fav.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
@@ -168,11 +166,6 @@ class ArticleController extends CommonDynController {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReplyInfo>? getDataList(MainListReply response) {
|
||||
return response.replies;
|
||||
}
|
||||
|
||||
Future<void> onFav() async {
|
||||
final favorite = stats.value?.favorite;
|
||||
bool isFav = favorite?.status == true;
|
||||
|
||||
@@ -21,7 +21,7 @@ abstract class CommonDynController extends ReplyController<MainListReply>
|
||||
late final horizontalPreview = Pref.horizontalPreview;
|
||||
late final List<double> ratio = Pref.dynamicDetailRatio;
|
||||
|
||||
double offsetDy = 1;
|
||||
final double offsetDy = 1;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@@ -89,4 +89,7 @@ abstract class CommonDynController extends ReplyController<MainListReply>
|
||||
cursorNext: cursorNext,
|
||||
offset: paginationReply?.nextOffset,
|
||||
);
|
||||
|
||||
@override
|
||||
List<ReplyInfo>? getDataList(MainListReply response) => response.replies;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show MainListReply, ReplyInfo;
|
||||
import 'package:PiliPlus/http/dynamics.dart';
|
||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
|
||||
@@ -47,9 +45,4 @@ class DynamicDetailController extends CommonDynController {
|
||||
replyType = commentType;
|
||||
queryData();
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReplyInfo>? getDataList(MainListReply response) {
|
||||
return response.replies;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/match.dart';
|
||||
import 'package:PiliPlus/models_new/match/match_info/contest.dart';
|
||||
@@ -17,9 +16,11 @@ class MatchInfoController extends CommonDynController {
|
||||
final Rx<LoadingState<MatchContest?>> infoState =
|
||||
LoadingState<MatchContest?>.loading().obs;
|
||||
|
||||
@override
|
||||
double get offsetDy => 2;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
offsetDy = 2;
|
||||
super.onInit();
|
||||
getMatchInfo();
|
||||
}
|
||||
@@ -31,9 +32,4 @@ class MatchInfoController extends CommonDynController {
|
||||
}
|
||||
infoState.value = res;
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReplyInfo>? getDataList(MainListReply response) {
|
||||
return response.replies;
|
||||
}
|
||||
}
|
||||
|
||||
41
lib/pages/music/controller.dart
Normal file
41
lib/pages/music/controller.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/music.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_detail.dart';
|
||||
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MusicDetailController extends CommonDynController {
|
||||
@override
|
||||
late final int oid;
|
||||
@override
|
||||
late final int replyType;
|
||||
|
||||
@override
|
||||
dynamic get sourceId => oid.toString();
|
||||
|
||||
final infoState = LoadingState<MusicDetail>.loading().obs;
|
||||
|
||||
late final String musicId;
|
||||
|
||||
bool get showDynActionBar => Pref.showDynActionBar;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
musicId = Get.parameters['musicId']!;
|
||||
getMusicDetail();
|
||||
}
|
||||
|
||||
Future<void> getMusicDetail() async {
|
||||
final res = await MusicHttp.bgmDetail(musicId);
|
||||
if (res.isSuccess) {
|
||||
final comment = res.data.musicComment!;
|
||||
oid = comment.oid!;
|
||||
replyType = comment.pageType ?? 47;
|
||||
count.value = comment.nums ?? -1;
|
||||
queryData();
|
||||
}
|
||||
infoState.value = res;
|
||||
}
|
||||
}
|
||||
30
lib/pages/music/video/controller.dart
Normal file
30
lib/pages/music/video/controller.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/music.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_detail.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_recommend_list.dart';
|
||||
import 'package:PiliPlus/pages/common/common_list_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MusicRecommendController
|
||||
extends CommonListController<List<BgmRecommend>?, BgmRecommend> {
|
||||
late final String musicId;
|
||||
late final MusicDetail musicDetail;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
final Map<String, dynamic> args = Get.arguments;
|
||||
musicId = args['id'];
|
||||
musicDetail = args['detail'];
|
||||
queryData();
|
||||
}
|
||||
|
||||
@override
|
||||
void checkIsEnd(int length) {
|
||||
isEnd = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState<List<BgmRecommend>?>> customGetData() =>
|
||||
MusicHttp.bgmRecommend(musicId);
|
||||
}
|
||||
127
lib/pages/music/video/view.dart
Normal file
127
lib/pages/music/video/view.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_recommend_list.dart';
|
||||
import 'package:PiliPlus/pages/music/video/controller.dart';
|
||||
import 'package:PiliPlus/pages/music/widget/music_video_card_h.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MusicRecommandPage extends StatefulWidget {
|
||||
const MusicRecommandPage({super.key});
|
||||
|
||||
@override
|
||||
State<MusicRecommandPage> createState() => _MusicRecommandPageState();
|
||||
}
|
||||
|
||||
class _MusicRecommandPageState extends State<MusicRecommandPage>
|
||||
with GridMixin, SingleTickerProviderStateMixin {
|
||||
late final _controller = Get.put(
|
||||
MusicRecommendController(),
|
||||
tag: Utils.generateRandomString(8),
|
||||
);
|
||||
|
||||
late final _animation = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 5),
|
||||
reverseDuration: const Duration(seconds: 5),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final padding = MediaQuery.viewPaddingOf(context);
|
||||
return Material(
|
||||
color: theme.colorScheme.surface,
|
||||
child: refreshIndicator(
|
||||
onRefresh: _controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: _controller.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
_buildAppBar(theme, padding),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 7,
|
||||
left: padding.left,
|
||||
right: padding.right,
|
||||
bottom: padding.bottom + 100,
|
||||
),
|
||||
sliver: Obx(
|
||||
() => _buildBody(_controller.loadingState.value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(LoadingState<List<BgmRecommend>?> loadingState) {
|
||||
return switch (loadingState) {
|
||||
Loading() => gridSkeleton,
|
||||
Success(:var response) =>
|
||||
response?.isNotEmpty == true
|
||||
? SliverGrid.builder(
|
||||
gridDelegate: gridDelegate,
|
||||
itemBuilder: (context, index) => MusicVideoCardH(
|
||||
videoItem: response[index],
|
||||
animation: _animation,
|
||||
),
|
||||
itemCount: response!.length,
|
||||
)
|
||||
: HttpError(onReload: _controller.onReload),
|
||||
Error(:var errMsg) => HttpError(
|
||||
errMsg: errMsg,
|
||||
onReload: _controller.onReload,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildAppBar(ThemeData theme, EdgeInsets padding) {
|
||||
final info = _controller.musicDetail;
|
||||
return SliverAppBar(
|
||||
pinned: true,
|
||||
title: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: 40,
|
||||
height: 40,
|
||||
src: info.mvCover,
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
info.musicTitle!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
Obx(() {
|
||||
final count = _controller.loadingState.value.dataOrNull?.length;
|
||||
return count == null
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
'共$count条视频',
|
||||
style: theme.textTheme.labelMedium,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animation.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
667
lib/pages/music/view.dart
Normal file
667
lib/pages/music/view.dart
Normal file
@@ -0,0 +1,667 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/marquee.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/music.dart';
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||
import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_detail.dart';
|
||||
import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart';
|
||||
import 'package:PiliPlus/pages/music/controller.dart';
|
||||
import 'package:PiliPlus/pages/music/video/view.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
import 'package:PiliPlus/utils/date_util.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/grid.dart';
|
||||
import 'package:PiliPlus/utils/num_util.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart' hide ContextExtensionss;
|
||||
|
||||
class MusicDetailPage extends StatefulWidget {
|
||||
const MusicDetailPage({super.key});
|
||||
|
||||
@override
|
||||
State<MusicDetailPage> createState() => _MusicDetailPageState();
|
||||
}
|
||||
|
||||
class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
@override
|
||||
final MusicDetailController controller = Get.put(
|
||||
MusicDetailController(),
|
||||
tag: Utils.generateRandomString(8),
|
||||
);
|
||||
|
||||
@override
|
||||
dynamic get arguments => null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final maxWidth = size.width;
|
||||
isPortrait = size.height >= maxWidth;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: _buildAppBar(isPortrait, maxWidth),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(left: padding.left, right: padding.right),
|
||||
child: isPortrait
|
||||
? refreshIndicator(
|
||||
onRefresh: controller.onRefresh,
|
||||
child: _buildBody(theme, isPortrait, maxWidth),
|
||||
)
|
||||
: _buildBody(theme, isPortrait, maxWidth),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar(bool isPortrait, double maxWidth) => AppBar(
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: Obx(
|
||||
() {
|
||||
final info = controller.infoState.value;
|
||||
late final showTitle = controller.showTitle.value;
|
||||
return info.isSuccess
|
||||
? AnimatedOpacity(
|
||||
opacity: showTitle ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: IgnorePointer(
|
||||
ignoring: !showTitle,
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: info.data.mvCover,
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
Text(info.data.musicTitle!),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(height: 40);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: isPortrait
|
||||
? null
|
||||
: [
|
||||
ratioWidget(maxWidth),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildBody(
|
||||
ThemeData theme,
|
||||
bool isPortrait,
|
||||
double maxWidth,
|
||||
) => Obx(() {
|
||||
switch (controller.infoState.value) {
|
||||
case Success(:final response):
|
||||
double padding = max(maxWidth / 2 - Grid.smallCardWidth, 0);
|
||||
final Widget child;
|
||||
if (isPortrait) {
|
||||
child = Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding),
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _buildCard(theme, response, maxWidth),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: _buildChart(theme, response, maxWidth),
|
||||
),
|
||||
buildReplyHeader(theme),
|
||||
Obx(() => replyList(theme, controller.loadingState.value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
padding = padding / 4;
|
||||
final flex = controller.ratio[0].toInt();
|
||||
final flex1 = controller.ratio[1].toInt();
|
||||
final leftWidth =
|
||||
(maxWidth - this.padding.horizontal) * (flex / (flex + flex1)) -
|
||||
padding;
|
||||
child = Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: flex,
|
||||
child: CustomScrollView(
|
||||
controller: controller.scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: padding,
|
||||
),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: _buildCard(theme, response, leftWidth),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: padding,
|
||||
bottom: this.padding.bottom + 100,
|
||||
),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: _buildChart(theme, response, leftWidth),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: flex1,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: padding),
|
||||
child: Scaffold(
|
||||
key: scaffoldKey,
|
||||
backgroundColor: Colors.transparent,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: refreshIndicator(
|
||||
onRefresh: controller.onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: controller
|
||||
.scrollController, // debug: The provided ScrollController is attached to more than one ScrollPosition.
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
buildReplyHeader(theme),
|
||||
Obx(
|
||||
() =>
|
||||
replyList(theme, controller.loadingState.value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
child,
|
||||
_buildBottom(theme, response),
|
||||
],
|
||||
);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
});
|
||||
|
||||
Widget _buildBottom(ThemeData theme, MusicDetail item) {
|
||||
final outline = theme.colorScheme.outline;
|
||||
|
||||
Widget textIconButton({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
int? count,
|
||||
bool status = false,
|
||||
required VoidCallback onPressed,
|
||||
IconData? activitedIcon,
|
||||
}) {
|
||||
final color = status ? theme.colorScheme.primary : outline;
|
||||
return TextButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
status ? activitedIcon : icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
foregroundColor: outline,
|
||||
),
|
||||
label: Text(
|
||||
count != null ? NumUtil.numFormat(count) : text,
|
||||
style: TextStyle(color: color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SlideTransition(
|
||||
position: controller.fabAnim,
|
||||
child: controller.showDynActionBar
|
||||
? Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: 14,
|
||||
bottom: padding.bottom + 14,
|
||||
),
|
||||
child: replyButton,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 14, bottom: 14),
|
||||
child: replyButton,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.08,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: padding.bottom),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
// TODO
|
||||
// Expanded(
|
||||
// child: textIconButton(
|
||||
// icon: FontAwesomeIcons.shareFromSquare,
|
||||
// text: '转发',
|
||||
// count: item.musicShares,
|
||||
// onPressed: () {
|
||||
// final data = controller.infoState.value.dataOrNull;
|
||||
// if (data != null) {
|
||||
// showModalBottomSheet(
|
||||
// context: context,
|
||||
// isScrollControlled: true,
|
||||
// useSafeArea: true,
|
||||
// builder: (context) => RepostPanel(
|
||||
// rid: controller.oid,
|
||||
// dynType: null,
|
||||
// pic: data.mvCover,
|
||||
// title: data.musicTitle,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
icon: CustomIcon.share_node,
|
||||
text: '分享',
|
||||
onPressed: () => Utils.shareText(
|
||||
'https://music.bilibili.com/h5/music-detail?music_id=${controller.musicId}',
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) => textIconButton(
|
||||
icon: FontAwesomeIcons.thumbsUp,
|
||||
activitedIcon: FontAwesomeIcons.solidThumbsUp,
|
||||
text: '点赞',
|
||||
count: item.wishCount,
|
||||
status: item.wishListen ?? false,
|
||||
onPressed: () async {
|
||||
if (!Accounts.main.isLogin) {
|
||||
SmartDialog.showToast('请先登录');
|
||||
return;
|
||||
}
|
||||
final hasLike = item.wishListen ?? false;
|
||||
final res = await MusicHttp.wishUpdate(
|
||||
controller.musicId,
|
||||
hasLike,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
if (hasLike) {
|
||||
item.wishCount--;
|
||||
} else {
|
||||
item.wishCount++;
|
||||
}
|
||||
item.wishListen = !hasLike;
|
||||
if (context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildArtist(Artist artist, TextStyle? style) {
|
||||
Widget child = Text('${artist.identity}: ${artist.name}', style: style);
|
||||
if (!artist.face.isNullOrEmpty) {
|
||||
child = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: artist.face,
|
||||
width: 15,
|
||||
height: 15,
|
||||
type: ImageType.avatar,
|
||||
),
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
child = InkWell(
|
||||
borderRadius: StyleString.mdRadius,
|
||||
onTap: artist.mid == null || artist.mid == 0
|
||||
? () => Utils.copyText(artist.name!)
|
||||
: () => Get.toNamed(
|
||||
'/member',
|
||||
parameters: {'mid': artist.mid!.toString()},
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildRank(
|
||||
int? rank,
|
||||
String name,
|
||||
TextTheme theme, [
|
||||
VoidCallback? onTap,
|
||||
]) {
|
||||
final child = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(NumUtil.numFormat(rank), style: theme.bodyLarge),
|
||||
Text(name, style: theme.bodySmall),
|
||||
],
|
||||
);
|
||||
return onTap == null
|
||||
? child
|
||||
: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Padding(padding: const EdgeInsets.all(4), child: child),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCard(ThemeData theme, MusicDetail item, double maxWidth) {
|
||||
final textTheme = theme.textTheme;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => PageUtils.imageView(
|
||||
imgList: [SourceModel(url: item.mvCover!)],
|
||||
),
|
||||
child: NetworkImgLayer(
|
||||
src: item.mvCover,
|
||||
width: 80,
|
||||
height: 80,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 2,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: StyleString.mdRadius,
|
||||
// TODO: android intent ACTION_MEDIA_SEARCH
|
||||
onTap: () => Utils.copyText(
|
||||
item.musicTitle!,
|
||||
),
|
||||
child: MarqueeText(
|
||||
item.musicTitle!,
|
||||
maxWidth: maxWidth - 136, // 80 + 16 + 32 + 8
|
||||
style: textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 2,
|
||||
alignment: WrapAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (!item.artistsList.isNullOrEmpty)
|
||||
for (var artist in item.artistsList!)
|
||||
_buildArtist(artist, textTheme.bodySmall),
|
||||
if (!item.musicPublish.isNullOrEmpty)
|
||||
Text(
|
||||
'发行日期:${item.musicPublish}',
|
||||
style: textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
if (!item.musicRank.isNullOrEmpty)
|
||||
PBadge(
|
||||
text: item.musicRank,
|
||||
type: PBadgeType.secondary,
|
||||
isStack: false,
|
||||
fontSize: 11,
|
||||
),
|
||||
if (item.mvCid != null && item.mvCid != 0)
|
||||
InkWell(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(4),
|
||||
),
|
||||
onTap: () => PageUtils.toVideoPage(
|
||||
bvid: item.mvBvid,
|
||||
cid: item.mvCid!,
|
||||
aid: item.mvAid,
|
||||
),
|
||||
child: ColoredBox(
|
||||
color: theme.colorScheme.secondaryContainer
|
||||
.withValues(alpha: 0.5),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2,
|
||||
horizontal: 3,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.play_circle_outline,
|
||||
size: 11,
|
||||
color: theme
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
Text(
|
||||
'看MV',
|
||||
style: TextStyle(
|
||||
color: theme
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
height: 1,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
strutStyle: const StrutStyle(
|
||||
leading: 0,
|
||||
height: 1,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SelectableText(
|
||||
[
|
||||
if (!(item.originArtist ?? item.originArtistList)
|
||||
.isNullOrEmpty)
|
||||
'原唱:${item.originArtist ?? item.originArtistList}',
|
||||
if (!item.album.isNullOrEmpty) '专辑:${item.album}',
|
||||
if (!item.musicSource.isNullOrEmpty) '出处:${item.musicSource}',
|
||||
].join('\n'),
|
||||
),
|
||||
const Divider(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Text('热歌榜排名'),
|
||||
_buildRank(item.hotSongHeat?.lastHeat, '热度', textTheme),
|
||||
_buildRank(item.listenPv, '总播放量', textTheme),
|
||||
_buildRank(
|
||||
item.musicRelation,
|
||||
'使用稿件量',
|
||||
textTheme,
|
||||
() => Get.to(
|
||||
const MusicRecommandPage(),
|
||||
arguments: {'id': controller.musicId, 'detail': item},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildChart(ThemeData theme, MusicDetail item, double maxWidth) {
|
||||
final heat = item.hotSongHeat?.songHeat;
|
||||
if (heat == null || heat.isEmpty) return null;
|
||||
final colorScheme = theme.colorScheme;
|
||||
int maxHeat = heat.first.heat;
|
||||
int minHeat = heat.first.heat;
|
||||
for (int i = 1; i < heat.length; i++) {
|
||||
final h = heat[i].heat;
|
||||
if (h > maxHeat) maxHeat = h;
|
||||
if (h < minHeat) minHeat = h;
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text('近${heat.length}日热度趋势', style: theme.textTheme.titleMedium),
|
||||
SizedBox(
|
||||
width: maxWidth,
|
||||
height: maxWidth * 0.5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsGeometry.only(top: 4, right: 22),
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
lineTouchData: const LineTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
leftTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
reservedSize: 55,
|
||||
showTitles: true,
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30 * sqrt2,
|
||||
getTitlesWidget: (index, meta) {
|
||||
return SideTitleWidget(
|
||||
angle: -pi / 4,
|
||||
space: 8 * sqrt2,
|
||||
meta: meta,
|
||||
child: Text(
|
||||
DateUtil.shortFormat.format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
heat[index.toInt()].date * 1000,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border.all(color: colorScheme.onSurface),
|
||||
),
|
||||
minX: 0,
|
||||
maxX: (heat.length - 1).toDouble(),
|
||||
minY: minHeat.toDouble(),
|
||||
maxY: maxHeat.toDouble(),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: List.generate(
|
||||
heat.length,
|
||||
(index) => FlSpot(
|
||||
index.toDouble(),
|
||||
heat[index].heat.toDouble(),
|
||||
),
|
||||
),
|
||||
color: colorScheme.primary,
|
||||
barWidth: 1,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
colorScheme.primary.withValues(alpha: 0.5),
|
||||
colorScheme.onPrimary.withValues(alpha: 0.5),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
147
lib/pages/music/widget/music_video_card_h.dart
Normal file
147
lib/pages/music/widget/music_video_card_h.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/marquee.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||
import 'package:PiliPlus/http/search.dart';
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/stat_type.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_recommend_list.dart';
|
||||
import 'package:PiliPlus/utils/duration_util.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MusicVideoCardH extends StatelessWidget {
|
||||
final BgmRecommend videoItem;
|
||||
final Animation<double> animation;
|
||||
|
||||
const MusicVideoCardH({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
int? cid =
|
||||
videoItem.cid ?? await SearchHttp.ab2c(bvid: videoItem.bvid);
|
||||
if (cid != null) {
|
||||
PageUtils.toVideoPage(
|
||||
bvid: videoItem.bvid,
|
||||
cid: cid,
|
||||
cover: videoItem.cover,
|
||||
title: videoItem.title,
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () => imageSaveDialog(
|
||||
title: videoItem.title,
|
||||
cover: videoItem.cover,
|
||||
bvid: videoItem.bvid,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: videoItem.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
PBadge(
|
||||
text: DurationUtil.formatDuration(videoItem.duration),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: PBadgeType.gray,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
content(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget content(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
videoItem.title!,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
StatWidget(
|
||||
type: StatType.play,
|
||||
value: videoItem.play,
|
||||
),
|
||||
StatWidget(
|
||||
type: StatType.danmaku,
|
||||
value: videoItem.danmu,
|
||||
),
|
||||
],
|
||||
),
|
||||
BounceMarquee(
|
||||
animation: animation,
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (videoItem.labelList case final labelList?)
|
||||
for (final label in labelList)
|
||||
PBadge(
|
||||
text: label.name,
|
||||
isStack: false,
|
||||
size: PBadgeSize.small,
|
||||
type: PBadgeType.secondary,
|
||||
),
|
||||
Text(
|
||||
videoItem.upNickName!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -933,11 +933,25 @@ class _UgcIntroPanelState extends TripleState<UgcIntroPanel>
|
||||
.map(
|
||||
(item) => SearchText(
|
||||
fontSize: 13,
|
||||
text: item.tagName!,
|
||||
onTap: (tagName) => Get.toNamed(
|
||||
'/searchResult',
|
||||
parameters: {'keyword': tagName},
|
||||
),
|
||||
text: switch (item.tagType) {
|
||||
'bgm' => item.tagName!.replaceFirst('发现', '\u{1f3b5}BGM:'),
|
||||
'topic' => '#${item.tagName}',
|
||||
_ => item.tagName!,
|
||||
},
|
||||
onTap: switch (item.tagType) {
|
||||
'bgm' => (_) => Get.toNamed(
|
||||
'/musicDetail',
|
||||
parameters: {'musicId': item.musicId!},
|
||||
),
|
||||
'topic' => (_) => Get.toNamed(
|
||||
'/dynTopic',
|
||||
parameters: {'id': item.tagId!.toString()},
|
||||
),
|
||||
_ => (tagName) => Get.toNamed(
|
||||
'/searchResult',
|
||||
parameters: {'keyword': tagName},
|
||||
),
|
||||
},
|
||||
onLongPress: Utils.copyText,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/marquee.dart';
|
||||
import 'package:PiliPlus/models/common/super_resolution_type.dart';
|
||||
import 'package:PiliPlus/models/common/video/audio_quality.dart';
|
||||
import 'package:PiliPlus/models/common/video/cdn_type.dart';
|
||||
@@ -42,7 +43,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
@@ -1960,9 +1960,9 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
builder: (context, constraints) {
|
||||
return Obx(
|
||||
() {
|
||||
String title;
|
||||
final videoDetail =
|
||||
introController.videoDetail.value;
|
||||
final String title;
|
||||
if (videoDetail.videos == 1) {
|
||||
title = videoDetail.title!;
|
||||
} else {
|
||||
@@ -1975,59 +1975,14 @@ class HeaderControlState extends TripleState<HeaderControl> {
|
||||
?.pagePart ??
|
||||
videoDetail.title!;
|
||||
}
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
return MarqueeText(
|
||||
title,
|
||||
maxWidth: constraints.maxWidth,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
)..layout(maxWidth: constraints.maxWidth);
|
||||
if (textPainter.didExceedMaxLines) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 25,
|
||||
),
|
||||
child: Marquee(
|
||||
text: title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
scrollAxis: Axis.horizontal,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
blankSpace: 200,
|
||||
velocity: 40,
|
||||
startAfter: const Duration(seconds: 1),
|
||||
showFadingOnlyWhenScrolling: true,
|
||||
fadingEdgeStartFraction: 0,
|
||||
fadingEdgeEndFraction: 0.1,
|
||||
numberOfRounds: 1,
|
||||
startPadding: 0,
|
||||
accelerationDuration: const Duration(
|
||||
seconds: 1,
|
||||
),
|
||||
accelerationCurve: Curves.linear,
|
||||
decelerationDuration: const Duration(
|
||||
milliseconds: 500,
|
||||
),
|
||||
decelerationCurve: Curves.easeOut,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -36,6 +36,7 @@ import 'package:PiliPlus/pages/msg_feed_top/like_detail/view.dart';
|
||||
import 'package:PiliPlus/pages/msg_feed_top/like_me/view.dart';
|
||||
import 'package:PiliPlus/pages/msg_feed_top/reply_me/view.dart';
|
||||
import 'package:PiliPlus/pages/msg_feed_top/sys_msg/view.dart';
|
||||
import 'package:PiliPlus/pages/music/view.dart';
|
||||
import 'package:PiliPlus/pages/search/view.dart';
|
||||
import 'package:PiliPlus/pages/search_result/view.dart';
|
||||
import 'package:PiliPlus/pages/search_trending/view.dart';
|
||||
@@ -207,6 +208,7 @@ class Routes {
|
||||
page: () => const LiveDmBlockPage(),
|
||||
),
|
||||
CustomGetPage(name: '/createVote', page: () => const CreateVotePage()),
|
||||
CustomGetPage(name: '/musicDetail', page: () => const MusicDetailPage()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -609,9 +609,7 @@ class PiliScheme {
|
||||
launchURL();
|
||||
}
|
||||
return hasMatch;
|
||||
}
|
||||
|
||||
if (host.contains('live.bilibili.com')) {
|
||||
} else if (host.contains('live.bilibili.com')) {
|
||||
String? roomId = uriDigitRegExp.firstMatch(path)?.group(1);
|
||||
if (roomId != null) {
|
||||
PageUtils.toLiveRoom(int.parse(roomId), off: off);
|
||||
@@ -619,9 +617,7 @@ class PiliScheme {
|
||||
}
|
||||
launchURL();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (host.contains('space.bilibili.com')) {
|
||||
} else if (host.contains('space.bilibili.com')) {
|
||||
String? sid =
|
||||
uri.queryParameters['sid'] ??
|
||||
RegExp(r'lists/(\d+)').firstMatch(path)?.group(1);
|
||||
@@ -636,9 +632,7 @@ class PiliScheme {
|
||||
}
|
||||
launchURL();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (host.contains('search.bilibili.com')) {
|
||||
} else if (host.contains('search.bilibili.com')) {
|
||||
String? keyword = uri.queryParameters['keyword'];
|
||||
if (keyword != null) {
|
||||
PageUtils.toDupNamed(
|
||||
@@ -650,9 +644,23 @@ class PiliScheme {
|
||||
}
|
||||
launchURL();
|
||||
return false;
|
||||
} else if (host.contains('music.bilibili.com')) {
|
||||
// music.bilibili.com/pc/music-detail?music_id=MA***
|
||||
// music.bilibili.com/h5-music-detail?music_id=MA***
|
||||
if (path.contains('music-detail')) {
|
||||
final musicId = uri.queryParameters['music_id'];
|
||||
if (musicId != null && musicId.startsWith('MA')) {
|
||||
PageUtils.toDupNamed(
|
||||
'/musicDetail',
|
||||
parameters: {'musicId': musicId},
|
||||
);
|
||||
return true;
|
||||
}
|
||||
launchURL();
|
||||
}
|
||||
}
|
||||
|
||||
List<String> pathSegments = uri.pathSegments;
|
||||
final pathSegments = uri.pathSegments;
|
||||
if (pathSegments.isEmpty) {
|
||||
launchURL();
|
||||
return false;
|
||||
|
||||
@@ -1123,14 +1123,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
marquee:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: marquee
|
||||
sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -166,7 +166,7 @@ dependencies:
|
||||
#瀑布流
|
||||
waterfall_flow: ^3.1.0
|
||||
#跑马灯
|
||||
marquee: ^2.3.0
|
||||
# marquee: ^2.3.0
|
||||
#富文本
|
||||
# extended_text: ^14.1.0
|
||||
# chat_bottom_container: ^0.2.0
|
||||
|
||||
Reference in New Issue
Block a user