mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-21 08:38:37 +00:00
feat: audio page (#1518)
* feat: audio page Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * opt ui Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * impl intro, share, fav Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> * load prev/next Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> --------- Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
603
lib/pages/audio/controller.dart
Normal file
603
lib/pages/audio/controller.dart
Normal file
@@ -0,0 +1,603 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/grpc/audio.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart'
|
||||
show
|
||||
DetailItem,
|
||||
PlayURLResp,
|
||||
PlaylistResp,
|
||||
PlaylistSource,
|
||||
PlayInfo,
|
||||
ThumbUpReq_ThumbType;
|
||||
import 'package:PiliPlus/grpc/bilibili/pagination.pb.dart';
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:PiliPlus/http/ua_type.dart';
|
||||
import 'package:PiliPlus/pages/common/common_intro_controller.dart'
|
||||
show FavMixin;
|
||||
import 'package:PiliPlus/pages/dynamics_repost/view.dart';
|
||||
import 'package:PiliPlus/pages/main_reply/view.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_mixin.dart';
|
||||
import 'package:PiliPlus/pages/video/pay_coins/view.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/global_data.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:fixnum/fixnum.dart' show Int64;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
class AudioController extends GetxController
|
||||
with GetTickerProviderStateMixin, TripleMixin, FavMixin {
|
||||
late Int64 id;
|
||||
late Int64 oid;
|
||||
late List<Int64> subId;
|
||||
late int itemType;
|
||||
late final PlaylistSource from;
|
||||
|
||||
final Rx<DetailItem?> audioItem = Rx<DetailItem?>(null);
|
||||
|
||||
Player? player;
|
||||
late int cacheAudioQa;
|
||||
|
||||
late bool isDragging = false;
|
||||
final Rx<Duration> position = Duration.zero.obs;
|
||||
final Rx<Duration> duration = Duration.zero.obs;
|
||||
|
||||
late final AnimationController animController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
|
||||
Set<StreamSubscription>? _subscriptions;
|
||||
|
||||
int? index;
|
||||
List<DetailItem>? playlist;
|
||||
|
||||
late double speed = 1.0;
|
||||
|
||||
late final Rx<PlayRepeat> playMode = Pref.audioPlayMode.obs;
|
||||
|
||||
late final isLogin = Accounts.main.isLogin;
|
||||
|
||||
Duration? _start;
|
||||
VideoDetailController? _videoDetailController;
|
||||
|
||||
String? _prev;
|
||||
String? _next;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
final args = Get.arguments;
|
||||
oid = Int64(args['oid']);
|
||||
final id = args['id'];
|
||||
this.id = id != null ? Int64(id) : oid;
|
||||
subId = (args['subId'] as List<int>?)?.map(Int64.new).toList() ?? [oid];
|
||||
itemType = args['itemType'];
|
||||
from = args['from'];
|
||||
_start = args['start'];
|
||||
if (args['heroTag'] case String heroTag) {
|
||||
try {
|
||||
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
_queryPlayList(isInit: true);
|
||||
|
||||
final String? audioUrl = args['audioUrl'];
|
||||
final hasAudioUrl = audioUrl != null;
|
||||
if (hasAudioUrl) {
|
||||
_onOpenMedia(
|
||||
audioUrl,
|
||||
ua: UaType.pc.ua,
|
||||
referer: HttpString.baseUrl,
|
||||
);
|
||||
}
|
||||
Utils.isWiFi.then((isWiFi) {
|
||||
cacheAudioQa = isWiFi ? Pref.defaultAudioQa : Pref.defaultAudioQaCellular;
|
||||
if (!hasAudioUrl) {
|
||||
_queryPlayUrl();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _queryPlayList({
|
||||
bool isInit = false,
|
||||
bool isLoadPrev = false,
|
||||
bool isLoadNext = false,
|
||||
}) async {
|
||||
final res = await AudioGrpc.audioPlayList(
|
||||
id: id,
|
||||
oid: isInit ? oid : null,
|
||||
subId: isInit ? subId : null,
|
||||
itemType: isInit ? itemType : null,
|
||||
from: isInit ? from : null,
|
||||
pagination: isLoadPrev
|
||||
? Pagination(next: _prev)
|
||||
: isLoadNext
|
||||
? Pagination(next: _next)
|
||||
: null,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
final PlaylistResp data = res.data;
|
||||
if (isInit) {
|
||||
late final paginationReply = data.paginationReply;
|
||||
_prev = data.reachStart ? null : paginationReply.prev;
|
||||
_next = data.reachEnd ? null : paginationReply.next;
|
||||
final index = data.list.indexWhere((e) => e.item.oid == oid);
|
||||
if (index != -1) {
|
||||
this.index = index;
|
||||
final item = data.list[index];
|
||||
audioItem.value = item;
|
||||
hasLike.value = item.stat.hasLike_7;
|
||||
coinNum.value = item.stat.hasCoin_8 ? 2 : 0;
|
||||
hasFav.value = item.stat.hasFav;
|
||||
playlist = data.list;
|
||||
}
|
||||
} else if (isLoadPrev) {
|
||||
_prev = data.reachStart ? null : data.paginationReply.prev;
|
||||
if (data.list.isNotEmpty) {
|
||||
index += data.list.length;
|
||||
playlist?.insertAll(0, data.list);
|
||||
}
|
||||
} else if (isLoadNext) {
|
||||
_next = data.reachEnd ? null : data.paginationReply.next;
|
||||
if (data.list.isNotEmpty) {
|
||||
playlist?.addAll(data.list);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _queryPlayUrl() async {
|
||||
final res = await AudioGrpc.audioPlayUrl(
|
||||
itemType: itemType,
|
||||
oid: oid,
|
||||
subId: subId,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
_onPlay(res.data);
|
||||
return true;
|
||||
} else {
|
||||
res.toast();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onPlay(PlayURLResp data) async {
|
||||
final PlayInfo? playInfo = data.playerInfo.values.firstOrNull;
|
||||
if (playInfo != null) {
|
||||
if (playInfo.hasPlayDash()) {
|
||||
final playDash = playInfo.playDash;
|
||||
final audios = playDash.audio;
|
||||
if (audios.isEmpty) {
|
||||
return;
|
||||
}
|
||||
position.value = Duration.zero;
|
||||
final audio = audios.findClosestTarget(
|
||||
(e) => e.id <= cacheAudioQa,
|
||||
(a, b) => a.id > b.id ? a : b,
|
||||
);
|
||||
_onOpenMedia(audio.baseUrl);
|
||||
} else if (playInfo.hasPlayUrl()) {
|
||||
final playUrl = playInfo.playUrl;
|
||||
final durls = playUrl.durl;
|
||||
if (durls.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final durl = durls.first;
|
||||
position.value = Duration.zero;
|
||||
_onOpenMedia(durl.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onOpenMedia(
|
||||
String url, {
|
||||
String? referer,
|
||||
String ua = Constants.userAgentApp,
|
||||
}) {
|
||||
_initPlayerIfNeeded();
|
||||
player!.open(
|
||||
Media(
|
||||
url,
|
||||
start: _start,
|
||||
httpHeaders: {
|
||||
'user-agent': ua,
|
||||
'referer': ?referer,
|
||||
},
|
||||
),
|
||||
);
|
||||
_start = null;
|
||||
}
|
||||
|
||||
void _initPlayerIfNeeded() {
|
||||
player ??= Player();
|
||||
_subscriptions ??= {
|
||||
player!.stream.position.listen((position) {
|
||||
if (isDragging) return;
|
||||
if (position.inSeconds != this.position.value.inSeconds) {
|
||||
this.position.value = position;
|
||||
_videoDetailController?.playedTime = position;
|
||||
}
|
||||
}),
|
||||
player!.stream.duration.listen((duration) {
|
||||
this.duration.value = duration;
|
||||
}),
|
||||
player!.stream.playing.listen((playing) {
|
||||
if (playing) {
|
||||
animController.forward();
|
||||
} else {
|
||||
animController.reverse();
|
||||
}
|
||||
}),
|
||||
player!.stream.completed.listen((completed) {
|
||||
_videoDetailController?.playedTime = duration.value;
|
||||
if (completed) {
|
||||
switch (playMode.value) {
|
||||
case PlayRepeat.pause:
|
||||
break;
|
||||
case PlayRepeat.listOrder:
|
||||
playNext();
|
||||
break;
|
||||
case PlayRepeat.singleCycle:
|
||||
player?.play();
|
||||
break;
|
||||
case PlayRepeat.listCycle:
|
||||
if (!playNext()) {
|
||||
if (index != null && index != 0 && playlist != null) {
|
||||
playIndex(0);
|
||||
} else {
|
||||
player?.play();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PlayRepeat.autoPlayRelated:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> actionLikeVideo() async {
|
||||
if (!isLogin) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
final newVal = !hasLike.value;
|
||||
final res = await AudioGrpc.audioThumbUp(
|
||||
oid: oid,
|
||||
subId: subId,
|
||||
itemType: itemType,
|
||||
type: newVal
|
||||
? ThumbUpReq_ThumbType.LIKE
|
||||
: ThumbUpReq_ThumbType.CANCEL_LIKE,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
hasLike.value = newVal;
|
||||
SmartDialog.showToast(res.data.message);
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> actionTriple() async {
|
||||
if (!isLogin) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
final res = await AudioGrpc.audioTripleLike(
|
||||
oid: oid,
|
||||
subId: subId,
|
||||
itemType: itemType,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
hasLike.value = true;
|
||||
coinNum.value = 2;
|
||||
hasFav.value = true;
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
|
||||
void actionCoinVideo() {
|
||||
final audioItem = this.audioItem.value;
|
||||
if (audioItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLogin) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
|
||||
final int copyright = audioItem.arc.copyright;
|
||||
if ((copyright != 1 && coinNum.value >= 1) || coinNum.value >= 2) {
|
||||
SmartDialog.showToast('达到投币上限啦~');
|
||||
return;
|
||||
}
|
||||
|
||||
if (GlobalData().coins != null && GlobalData().coins! < 1) {
|
||||
SmartDialog.showToast('硬币不足');
|
||||
return;
|
||||
}
|
||||
|
||||
PayCoinsPage.toPayCoinsPage(
|
||||
onPayCoin: _onPayCoin,
|
||||
hasCoin: coinNum.value == 1,
|
||||
copyright: copyright,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPayCoin(int coin, bool coinWithLike) async {
|
||||
final res = await AudioGrpc.audioCoinAdd(
|
||||
oid: oid,
|
||||
subId: subId,
|
||||
itemType: itemType,
|
||||
num: coin,
|
||||
thumbUp: coinWithLike,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
if (coinWithLike) {
|
||||
hasLike.value = true;
|
||||
}
|
||||
coinNum.value += coin;
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void showFavBottomSheet(BuildContext context, {bool isLongPress = false}) {
|
||||
if (!isLogin) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
if (enableQuickFav) {
|
||||
if (!isLongPress) {
|
||||
actionFavVideo(isQuick: true);
|
||||
} else {
|
||||
PageUtils.showFavBottomSheet(context: context, ctr: this);
|
||||
}
|
||||
} else if (!isLongPress) {
|
||||
PageUtils.showFavBottomSheet(context: context, ctr: this);
|
||||
}
|
||||
}
|
||||
|
||||
void showReply() {
|
||||
MainReplyPage.toMainReplyPage(
|
||||
oid: oid.toInt(),
|
||||
replyType: itemType == 1 ? 1 : 14,
|
||||
);
|
||||
}
|
||||
|
||||
void actionShareVideo(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
final audioUrl = itemType == 1
|
||||
? '${HttpString.baseUrl}/video/${IdUtils.av2bv(oid.toInt())}'
|
||||
: '${HttpString.baseUrl}/audio/au$oid';
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'复制链接',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
Utils.copyText(audioUrl);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'其它app打开',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
PageUtils.launchURL(audioUrl);
|
||||
},
|
||||
),
|
||||
if (Utils.isMobile)
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'分享视频',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
if (audioItem.value case final audioItem?) {
|
||||
Utils.shareText(
|
||||
'${audioItem.arc.title} '
|
||||
'UP主: ${audioItem.owner.name}'
|
||||
' - $audioUrl',
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'分享至动态',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
if (audioItem.value case final audioItem?) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (context) => RepostPanel(
|
||||
rid: oid.toInt(),
|
||||
dynType: itemType == 1 ? 8 : 256,
|
||||
pic: audioItem.arc.cover,
|
||||
title: audioItem.arc.title,
|
||||
uname: audioItem.owner.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (itemType == 1)
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'分享至消息',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
if (audioItem.value case final audioItem?) {
|
||||
try {
|
||||
PageUtils.pmShare(
|
||||
context,
|
||||
content: {
|
||||
"id": oid.toString(),
|
||||
"title": audioItem.arc.title,
|
||||
"headline": audioItem.arc.title,
|
||||
"source": 5,
|
||||
"thumb": audioItem.arc.cover,
|
||||
"author": audioItem.owner.name,
|
||||
"author_id": audioItem.owner.mid.toString(),
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void playOrPause() {
|
||||
if (player case final player?) {
|
||||
if ((duration.value - position.value).inMilliseconds < 50) {
|
||||
player.seek(Duration.zero).whenComplete(player.play);
|
||||
} else {
|
||||
player.playOrPause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool playPrev() {
|
||||
if (index != null && playlist != null && player != null) {
|
||||
final prev = index! - 1;
|
||||
if (prev >= 0) {
|
||||
playIndex(prev);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool playNext() {
|
||||
if (index != null && playlist != null && player != null) {
|
||||
final next = index! + 1;
|
||||
if (next < playlist!.length) {
|
||||
playIndex(next);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void playIndex(int index) {
|
||||
if (index == this.index) return;
|
||||
this.index = index;
|
||||
final audioItem = playlist![index];
|
||||
final item = audioItem.item;
|
||||
oid = item.oid;
|
||||
subId = item.subId;
|
||||
itemType = item.itemType;
|
||||
_queryPlayUrl().then((res) {
|
||||
if (res) {
|
||||
this.audioItem.value = audioItem;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setSpeed(double speed) {
|
||||
if (player case final player?) {
|
||||
this.speed = speed;
|
||||
player.setRate(speed);
|
||||
}
|
||||
}
|
||||
|
||||
// Timer? _timer;
|
||||
|
||||
// void _cancelTimer() {
|
||||
// _timer?.cancel();
|
||||
// _timer = null;
|
||||
// }
|
||||
|
||||
// void showTimerDialog() {
|
||||
// // TODO
|
||||
// }
|
||||
|
||||
@override
|
||||
(Object, int) get getFavRidType => (oid, itemType == 1 ? 2 : 12);
|
||||
|
||||
@override
|
||||
void updateFavCount(int count) {
|
||||
audioItem
|
||||
..value?.stat.favourite += count
|
||||
..refresh();
|
||||
}
|
||||
|
||||
Future<void> loadPrev(BuildContext context) async {
|
||||
if (_prev == null) return;
|
||||
final length = playlist!.length;
|
||||
await _queryPlayList(isLoadPrev: true);
|
||||
if (length != playlist!.length && context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadNext(BuildContext context) async {
|
||||
if (_next == null) return;
|
||||
final length = playlist!.length;
|
||||
await _queryPlayList(isLoadNext: true);
|
||||
if (length != playlist!.length && context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// _cancelTimer();
|
||||
_subscriptions?.forEach((e) => e.cancel());
|
||||
_subscriptions = null;
|
||||
player?.dispose();
|
||||
player = null;
|
||||
animController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
860
lib/pages/audio/view.dart
Normal file
860
lib/pages/audio/view.dart
Normal file
@@ -0,0 +1,860 @@
|
||||
import 'dart:math' show min;
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart';
|
||||
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart';
|
||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||
import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/pages/audio/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
|
||||
import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:PiliPlus/utils/date_utils.dart';
|
||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/num_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_key.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart' hide DraggableScrollableSheet;
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AudioPage extends StatefulWidget {
|
||||
const AudioPage({super.key});
|
||||
|
||||
@override
|
||||
State<AudioPage> createState() => _AudioPageState();
|
||||
|
||||
static void toAudioPage({
|
||||
int? id,
|
||||
required int oid,
|
||||
List<int>? subId,
|
||||
required int itemType,
|
||||
required PlaylistSource from,
|
||||
String? heroTag,
|
||||
Duration? start,
|
||||
String? audioUrl,
|
||||
}) => Get.toNamed(
|
||||
'/audio',
|
||||
arguments: {
|
||||
'id': ?id,
|
||||
'oid': oid,
|
||||
'subId': ?subId,
|
||||
'from': from,
|
||||
'itemType': itemType,
|
||||
'heroTag': ?heroTag,
|
||||
'start': ?start,
|
||||
'audioUrl': ?audioUrl,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _AudioPageState extends State<AudioPage> {
|
||||
final _controller = Get.put(
|
||||
AudioController(),
|
||||
tag: Utils.generateRandomString(8),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
final isPortrait = MediaQuery.sizeOf(context).isPortrait;
|
||||
final padding = MediaQuery.viewPaddingOf(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _showMore,
|
||||
icon: const Icon(Icons.more_vert),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 20 + padding.left,
|
||||
right: 20 + padding.right,
|
||||
bottom: 30 + padding.bottom,
|
||||
),
|
||||
child: isPortrait
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildInfo(colorScheme, isPortrait)),
|
||||
const SizedBox(height: 25),
|
||||
_buildProgressBar(colorScheme),
|
||||
_buildDuration(colorScheme),
|
||||
_buildControls(),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildInfo(colorScheme, isPortrait)],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() {
|
||||
final audioItem = _controller.audioItem.value;
|
||||
if (audioItem != null) {
|
||||
return _buildActions(audioItem);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
const SizedBox(height: 25),
|
||||
_buildProgressBar(colorScheme),
|
||||
_buildDuration(colorScheme),
|
||||
_buildControls(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPlaylist() {
|
||||
if (_controller.playlist case final playlist?) {
|
||||
final initialScrollOffset = 45.0 * _controller.index!;
|
||||
final scrollController = ScrollController(
|
||||
initialScrollOffset: initialScrollOffset,
|
||||
);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(640, context.mediaQueryShortestSide),
|
||||
),
|
||||
builder: (context) {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return FractionallySizedBox(
|
||||
heightFactor: !context.mediaQuerySize.isPortrait && Utils.isMobile
|
||||
? 1.0
|
||||
: 0.7,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: Get.back,
|
||||
borderRadius: StyleString.bottomSheetRadius,
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outline,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: refreshIndicator(
|
||||
onRefresh: () => _controller.loadPrev(context),
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom:
|
||||
MediaQuery.paddingOf(context).bottom + 100,
|
||||
),
|
||||
sliver: SliverList.builder(
|
||||
itemCount: playlist.length,
|
||||
itemBuilder: (_, index) {
|
||||
if (index == playlist.length - 1) {
|
||||
_controller.loadNext(context);
|
||||
}
|
||||
final isCurr = index == _controller.index;
|
||||
final item = playlist[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
minTileHeight: 45,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
if (!isCurr) {
|
||||
_controller.playIndex(index);
|
||||
}
|
||||
},
|
||||
title: Text.rich(
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: isCurr
|
||||
? TextStyle(
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
)
|
||||
: const TextStyle(
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
),
|
||||
strutStyle: const StrutStyle(
|
||||
height: 1,
|
||||
leading: 0,
|
||||
fontSize: 14,
|
||||
),
|
||||
TextSpan(
|
||||
children: [
|
||||
if (isCurr) ...[
|
||||
WidgetSpan(
|
||||
alignment:
|
||||
PlaceholderAlignment.bottom,
|
||||
child: Image.asset(
|
||||
'assets/images/live.gif',
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ' '),
|
||||
],
|
||||
TextSpan(
|
||||
text: item.arc.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: isCurr
|
||||
? null
|
||||
: iconButton(
|
||||
context: context,
|
||||
icon: Icons.clear,
|
||||
onPressed: () {
|
||||
if (index < _controller.index!) {
|
||||
_controller.index -= 1;
|
||||
}
|
||||
_controller.playlist!.removeAt(
|
||||
index,
|
||||
);
|
||||
(context as Element)
|
||||
.markNeedsBuild();
|
||||
},
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: colorScheme.outline,
|
||||
size: 28,
|
||||
iconSize: 18,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: colorScheme.outline.withValues(alpha: 0.1),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: Get.back,
|
||||
child: SizedBox(
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'关闭',
|
||||
style: TextStyle(color: colorScheme.outline),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
).whenComplete(scrollController.dispose);
|
||||
}
|
||||
}
|
||||
|
||||
void _showPlaySettings() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(640, context.mediaQueryShortestSide),
|
||||
),
|
||||
builder: (context) {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: Get.back,
|
||||
borderRadius: StyleString.bottomSheetRadius,
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outline,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 20,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) => Column(
|
||||
spacing: 12,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('播放倍速(${_controller.speed})'),
|
||||
Slider(
|
||||
padding: EdgeInsets.zero,
|
||||
min: 0.5,
|
||||
max: 2.0,
|
||||
divisions: 15,
|
||||
value: _controller.speed,
|
||||
onChanged: (value) {
|
||||
_controller.speed = value.toPrecision(1);
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
onChangeEnd: (_) =>
|
||||
_controller.setSpeed(_controller.speed),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text('播放模式'),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: PlayRepeat.values
|
||||
.sublist(0, 4)
|
||||
.map(
|
||||
(e) => _playModeWidget(
|
||||
colorScheme: colorScheme,
|
||||
playMode: e,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _playModeWidget({
|
||||
required ColorScheme colorScheme,
|
||||
required PlayRepeat playMode,
|
||||
}) {
|
||||
final isCurr = playMode == _controller.playMode.value;
|
||||
final color = isCurr ? colorScheme.primary : colorScheme.outline;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
if (!isCurr) {
|
||||
_controller.playMode.value = playMode;
|
||||
GStorage.setting.put(SettingBoxKey.audioPlayMode, playMode.index);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
spacing: 6,
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isCurr
|
||||
? colorScheme.primary.withValues(alpha: 0.15)
|
||||
: colorScheme.onInverseSurface.withValues(alpha: 0.8),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(
|
||||
size: 26,
|
||||
playMode.icon,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
playMode.desc,
|
||||
style: TextStyle(fontSize: 13, color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMore() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(640, context.mediaQueryShortestSide),
|
||||
),
|
||||
builder: (context) {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: Get.back,
|
||||
borderRadius: StyleString.bottomSheetRadius,
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outline,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// ListTile(
|
||||
// dense: true,
|
||||
// title: const Text(
|
||||
// '定时关闭',
|
||||
// style: TextStyle(fontSize: 14),
|
||||
// ),
|
||||
// onTap: () {
|
||||
// Get.back();
|
||||
// _controller.showTimerDialog();
|
||||
// },
|
||||
// ),
|
||||
if (_controller.itemType == 1)
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'举报',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
PageUtils.reportVideo(_controller.oid.toInt());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActions(DetailItem audioItem) {
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(
|
||||
() => ActionItem(
|
||||
animation: _controller.tripleAnimation,
|
||||
icon: const Icon(FontAwesomeIcons.thumbsUp),
|
||||
selectIcon: const Icon(
|
||||
FontAwesomeIcons.solidThumbsUp,
|
||||
),
|
||||
selectStatus: _controller.hasLike.value,
|
||||
semanticsLabel: '点赞',
|
||||
text: NumUtils.numFormat(audioItem.stat.like),
|
||||
onStartTriple: _controller.onStartTriple,
|
||||
onCancelTriple: _controller.onCancelTriple,
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => ActionItem(
|
||||
animation: _controller.tripleAnimation,
|
||||
icon: const Icon(FontAwesomeIcons.b),
|
||||
selectIcon: const Icon(FontAwesomeIcons.b),
|
||||
onTap: _controller.actionCoinVideo,
|
||||
selectStatus: _controller.hasCoin,
|
||||
semanticsLabel: '投币',
|
||||
text: NumUtils.numFormat(
|
||||
audioItem.stat.coin,
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => ActionItem(
|
||||
animation: _controller.tripleAnimation,
|
||||
icon: const Icon(FontAwesomeIcons.star),
|
||||
selectIcon: const Icon(
|
||||
FontAwesomeIcons.solidStar,
|
||||
),
|
||||
onTap: () => _controller.showFavBottomSheet(context),
|
||||
onLongPress: () => _controller.showFavBottomSheet(
|
||||
context,
|
||||
isLongPress: true,
|
||||
),
|
||||
selectStatus: _controller.hasFav.value,
|
||||
semanticsLabel: '收藏',
|
||||
text: NumUtils.numFormat(
|
||||
audioItem.stat.favourite,
|
||||
),
|
||||
),
|
||||
),
|
||||
ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.comment),
|
||||
onTap: _controller.showReply,
|
||||
semanticsLabel: '评论',
|
||||
text: NumUtils.numFormat(
|
||||
audioItem.stat.reply,
|
||||
),
|
||||
),
|
||||
ActionItem(
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.shareFromSquare,
|
||||
),
|
||||
onTap: () => _controller.actionShareVideo(context),
|
||||
selectStatus: false,
|
||||
semanticsLabel: '分享',
|
||||
text: NumUtils.numFormat(
|
||||
audioItem.stat.share,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressBar(ColorScheme colorScheme) {
|
||||
final primary = colorScheme.primary;
|
||||
final thumbGlowColor = primary.withAlpha(80);
|
||||
final bufferedBarColor = primary.withValues(alpha: 0.4);
|
||||
final baseBarColor = colorScheme.brightness.isDark
|
||||
? const Color(0x33FFFFFF)
|
||||
: const Color(0x33999999);
|
||||
return Obx(() {
|
||||
final child = ProgressBar(
|
||||
progress: _controller.position.value,
|
||||
total: _controller.duration.value,
|
||||
baseBarColor: baseBarColor,
|
||||
progressBarColor: primary,
|
||||
bufferedBarColor: bufferedBarColor,
|
||||
thumbColor: primary,
|
||||
thumbGlowColor: thumbGlowColor,
|
||||
thumbGlowRadius: 0,
|
||||
thumbRadius: 6,
|
||||
onDragStart: (_) {},
|
||||
onDragUpdate: (details) {
|
||||
_controller
|
||||
..isDragging = true
|
||||
..position.value = details.timeStamp;
|
||||
},
|
||||
onSeek: (value) {
|
||||
_controller
|
||||
..player?.platform?.seek(value)
|
||||
..isDragging = false;
|
||||
},
|
||||
);
|
||||
if (Utils.isDesktop) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildDuration(ColorScheme colorScheme) {
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(fontSize: 13, color: colorScheme.outline),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Obx(() {
|
||||
final position = _controller.position.value;
|
||||
if (_controller.player != null) {
|
||||
return Text(
|
||||
DurationUtils.formatDuration(position.inSeconds),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
Obx(() {
|
||||
final duration = _controller.duration.value;
|
||||
if (_controller.player != null) {
|
||||
return Text(
|
||||
DurationUtils.formatDuration(duration.inSeconds),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControls() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Obx(
|
||||
() => IconButton(
|
||||
onPressed: _showPlaySettings,
|
||||
icon: Icon(
|
||||
size: 26,
|
||||
_controller.playMode.value.icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _controller.playPrev,
|
||||
icon: const Icon(
|
||||
size: 40,
|
||||
Icons.skip_previous_rounded,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _controller.playOrPause,
|
||||
icon: AnimatedIcon(
|
||||
size: 40,
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: _controller.animController,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _controller.playNext,
|
||||
icon: const Icon(
|
||||
size: 40,
|
||||
Icons.skip_next_rounded,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _showPlaylist,
|
||||
icon: const Icon(
|
||||
size: 26,
|
||||
Icons.menu_rounded,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfo(ColorScheme colorScheme, bool isPortrait) {
|
||||
return Obx(() {
|
||||
final audioItem = _controller.audioItem.value;
|
||||
if (audioItem != null) {
|
||||
final cover = audioItem.arc.cover.http2https;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => PageUtils.imageView(
|
||||
imgList: [SourceModel(url: cover)],
|
||||
),
|
||||
child: Hero(
|
||||
tag: cover,
|
||||
child: NetworkImgLayer(
|
||||
src: cover,
|
||||
width: 150,
|
||||
height: 150,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
audioItem.arc.title,
|
||||
style: const TextStyle(
|
||||
height: 1.7,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
icon: Icons.keyboard_arrow_down,
|
||||
onPressed: () => _showIntro(audioItem),
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: colorScheme.outline,
|
||||
size: 26,
|
||||
iconSize: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (audioItem.owner.hasName())
|
||||
Row(
|
||||
spacing: 6,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (audioItem.owner.hasAvatar())
|
||||
NetworkImgLayer(
|
||||
src: audioItem.owner.avatar,
|
||||
width: 22,
|
||||
height: 22,
|
||||
type: ImageType.avatar,
|
||||
),
|
||||
Text(
|
||||
audioItem.owner.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isPortrait)
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: _buildActions(audioItem),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
});
|
||||
}
|
||||
|
||||
void _showIntro(DetailItem audioItem) {
|
||||
final arc = audioItem.arc;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: min(640, context.mediaQueryShortestSide),
|
||||
),
|
||||
builder: (context) {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: Get.back,
|
||||
borderRadius: StyleString.bottomSheetRadius,
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.outline,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 20,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('简介', style: TextStyle(fontSize: 15)),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
arc.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
size: 14,
|
||||
Icons.headphones_outlined,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
Text(
|
||||
' ${NumUtils.numFormat(audioItem.stat.view)} '
|
||||
'${DateFormatUtils.dateFormat(arc.publish.toInt(), long: DateFormatUtils.longFormatD)} '
|
||||
'${arc.displayedOid}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SelectableText(arc.desc),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension _PlayReatExt on PlayRepeat {
|
||||
IconData get icon => switch (this) {
|
||||
PlayRepeat.pause => Icons.pause_rounded,
|
||||
PlayRepeat.listOrder => Icons.keyboard_double_arrow_right_rounded,
|
||||
PlayRepeat.singleCycle => Icons.play_circle_outline_rounded,
|
||||
PlayRepeat.listCycle => Icons.sync_rounded,
|
||||
PlayRepeat.autoPlayRelated => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user