mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
opt: sponsor block
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -8,6 +8,7 @@ class HttpString {
|
|||||||
static const String messageBaseUrl = 'https://message.bilibili.com';
|
static const String messageBaseUrl = 'https://message.bilibili.com';
|
||||||
static const String dynamicShareBaseUrl = 'https://t.bilibili.com';
|
static const String dynamicShareBaseUrl = 'https://t.bilibili.com';
|
||||||
static const String spaceBaseUrl = 'https://space.bilibili.com';
|
static const String spaceBaseUrl = 'https://space.bilibili.com';
|
||||||
|
static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top';
|
||||||
static const List<int> validateStatusCodes = [
|
static const List<int> validateStatusCodes = [
|
||||||
302,
|
302,
|
||||||
304,
|
304,
|
||||||
|
|||||||
@@ -224,7 +224,8 @@ class Request {
|
|||||||
/*
|
/*
|
||||||
* post请求
|
* post请求
|
||||||
*/
|
*/
|
||||||
post(url, {data, queryParameters, options, cancelToken, extra}) async {
|
Future<Response> post(url,
|
||||||
|
{data, queryParameters, options, cancelToken, extra}) async {
|
||||||
// debugPrint('post-data: $data');
|
// debugPrint('post-data: $data');
|
||||||
Response response;
|
Response response;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:PiliPalaX/utils/utils.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class SponsorBlockPage extends StatefulWidget {
|
class SponsorBlockPage extends StatefulWidget {
|
||||||
const SponsorBlockPage({super.key});
|
const SponsorBlockPage({super.key});
|
||||||
@@ -17,10 +18,11 @@ class SponsorBlockPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
||||||
|
final _url = 'https://github.com/hanydd/BilibiliSponsorBlock';
|
||||||
late double _blockLimit;
|
late double _blockLimit;
|
||||||
late List<Pair<SegmentType, SkipType>> _blockSettings;
|
late List<Pair<SegmentType, SkipType>> _blockSettings;
|
||||||
late List<Color> _blockColor;
|
late List<Color> _blockColor;
|
||||||
final _url = 'https://github.com/hanydd/BilibiliSponsorBlock';
|
late String _userId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -28,8 +30,139 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
|||||||
_blockLimit = GStorage.blockLimit;
|
_blockLimit = GStorage.blockLimit;
|
||||||
_blockSettings = GStorage.blockSettings;
|
_blockSettings = GStorage.blockSettings;
|
||||||
_blockColor = GStorage.blockColor;
|
_blockColor = GStorage.blockColor;
|
||||||
|
_userId = GStorage.blockUserID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextStyle get _titleStyle => TextStyle(fontSize: 15);
|
||||||
|
TextStyle get _subTitleStyle =>
|
||||||
|
TextStyle(color: Theme.of(context).colorScheme.outline);
|
||||||
|
|
||||||
|
Widget get _blockLimitItem => ListTile(
|
||||||
|
onTap: () {
|
||||||
|
final textController =
|
||||||
|
TextEditingController(text: _blockLimit.toString());
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('最短片段时长'),
|
||||||
|
content: TextFormField(
|
||||||
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||||
|
controller: textController,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: InputDecoration(suffixText: 's'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: Get.back,
|
||||||
|
child: Text(
|
||||||
|
'取消',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Get.back();
|
||||||
|
_blockLimit =
|
||||||
|
max(0.0, double.tryParse(textController.text) ?? 0.0);
|
||||||
|
await GStorage.setting
|
||||||
|
.put(SettingBoxKey.blockLimit, _blockLimit);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: Text('确定'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
title: Text('最短片段时长', style: _titleStyle),
|
||||||
|
subtitle: Text(
|
||||||
|
'忽略短于此时长的片段',
|
||||||
|
style: _subTitleStyle,
|
||||||
|
),
|
||||||
|
trailing: Text(
|
||||||
|
'${_blockLimit}s',
|
||||||
|
style: TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _aboudItem => ListTile(
|
||||||
|
title: Text('关于 SponsorBlock', style: _titleStyle),
|
||||||
|
subtitle: Text(_url, style: _subTitleStyle),
|
||||||
|
onTap: () => Utils.launchURL(_url),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _userIdItem => ListTile(
|
||||||
|
title: Text('用户ID', style: _titleStyle),
|
||||||
|
subtitle: Text(_userId, style: _subTitleStyle),
|
||||||
|
onTap: () {
|
||||||
|
final key = GlobalKey<FormState>();
|
||||||
|
final textController = TextEditingController(text: _userId);
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('用户ID'),
|
||||||
|
content: Form(
|
||||||
|
key: key,
|
||||||
|
child: TextFormField(
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 4,
|
||||||
|
autofocus: true,
|
||||||
|
controller: textController,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\d]+')),
|
||||||
|
],
|
||||||
|
validator: (value) {
|
||||||
|
if ((value?.length ?? -1) < 30) {
|
||||||
|
return '用户ID要求至少为30个字符长度的纯字符串';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Get.back();
|
||||||
|
_userId = Uuid().v4().replaceAll('-', '');
|
||||||
|
await GStorage.setting
|
||||||
|
.put(SettingBoxKey.blockUserID, _userId);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: Text('随机'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: Get.back,
|
||||||
|
child: Text(
|
||||||
|
'取消',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (key.currentState?.validate() == true) {
|
||||||
|
Get.back();
|
||||||
|
_userId = textController.text;
|
||||||
|
await GStorage.setting
|
||||||
|
.put(SettingBoxKey.blockUserID, _userId);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('确定'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -41,198 +174,155 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
|||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: ListView.separated(
|
body: CustomScrollView(
|
||||||
itemCount: _blockSettings.length + 2,
|
slivers: [
|
||||||
itemBuilder: (_, index) => index == _blockSettings.length + 1
|
SliverToBoxAdapter(child: _blockLimitItem),
|
||||||
? ListTile(
|
SliverToBoxAdapter(child: Divider(height: 1)),
|
||||||
leading: Icon(Icons.code),
|
SliverToBoxAdapter(child: _userIdItem),
|
||||||
title: const Text('About'),
|
SliverToBoxAdapter(child: Divider(height: 1)),
|
||||||
subtitle: Text(_url),
|
SliverList.separated(
|
||||||
onTap: () => Utils.launchURL(_url),
|
itemCount: _blockSettings.length,
|
||||||
)
|
itemBuilder: (_, index) => ListTile(
|
||||||
: index == 0
|
enabled: _blockSettings[index].second != SkipType.disable,
|
||||||
? ListTile(
|
onTap: () {
|
||||||
onTap: () {
|
showDialog(
|
||||||
final textController =
|
context: context,
|
||||||
TextEditingController(text: _blockLimit.toString());
|
builder: (_) => AlertDialog(
|
||||||
showDialog(
|
clipBehavior: Clip.hardEdge,
|
||||||
context: context,
|
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
builder: (BuildContext context) {
|
title: Text.rich(
|
||||||
return AlertDialog(
|
style: TextStyle(height: 1),
|
||||||
title: const Text('Block Limit'),
|
strutStyle: StrutStyle(height: 1, leading: 0),
|
||||||
content: TextFormField(
|
TextSpan(
|
||||||
keyboardType: TextInputType.numberWithOptions(
|
children: [
|
||||||
decimal: true),
|
TextSpan(
|
||||||
controller: textController,
|
text: 'Color Picker\n',
|
||||||
autofocus: true,
|
style: TextStyle(fontSize: 18, height: 1.5),
|
||||||
decoration: InputDecoration(suffixText: 's'),
|
),
|
||||||
),
|
WidgetSpan(
|
||||||
actions: [
|
alignment: PlaceholderAlignment.middle,
|
||||||
TextButton(
|
child: Container(
|
||||||
onPressed: Get.back,
|
height:
|
||||||
child: Text(
|
MediaQuery.textScalerOf(context).scale(16),
|
||||||
'取消',
|
width: MediaQuery.textScalerOf(context).scale(16),
|
||||||
style: TextStyle(
|
alignment: Alignment.center,
|
||||||
color:
|
child: Container(
|
||||||
Theme.of(context).colorScheme.outline,
|
height: 10,
|
||||||
),
|
width: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: _blockColor[index],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
Get.back();
|
|
||||||
_blockLimit = max(
|
|
||||||
0.0,
|
|
||||||
double.tryParse(textController.text) ??
|
|
||||||
0.0);
|
|
||||||
await GStorage.setting.put(
|
|
||||||
SettingBoxKey.blockLimit, _blockLimit);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
child: Text('确定'),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
leading: Icon(Icons.av_timer),
|
|
||||||
title: const Text('Block Limit'),
|
|
||||||
trailing: Text(
|
|
||||||
'${_blockLimit}s',
|
|
||||||
style: TextStyle(fontSize: 13),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ListTile(
|
|
||||||
enabled:
|
|
||||||
_blockSettings[index - 1].second != SkipType.disable,
|
|
||||||
onTap: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => AlertDialog(
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
contentPadding:
|
|
||||||
const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
title: Text.rich(
|
|
||||||
style: TextStyle(height: 1),
|
|
||||||
strutStyle: StrutStyle(height: 1, leading: 0),
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: 'Color Picker\n',
|
|
||||||
style: TextStyle(fontSize: 18, height: 1.5),
|
|
||||||
),
|
|
||||||
WidgetSpan(
|
|
||||||
alignment: PlaceholderAlignment.middle,
|
|
||||||
child: Container(
|
|
||||||
height: MediaQuery.textScalerOf(context)
|
|
||||||
.scale(16),
|
|
||||||
width: MediaQuery.textScalerOf(context)
|
|
||||||
.scale(16),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Container(
|
|
||||||
height: 10,
|
|
||||||
width: 10,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: _blockColor[index - 1],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: TextStyle(fontSize: 16, height: 1),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
' ${_blockSettings[index - 1].first.name}',
|
|
||||||
style: TextStyle(fontSize: 16, height: 1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
style: TextStyle(fontSize: 16, height: 1),
|
||||||
),
|
),
|
||||||
content: SlideColorPicker(
|
TextSpan(
|
||||||
color: _blockColor[index - 1],
|
text: ' ${_blockSettings[index].first.title}',
|
||||||
callback: (Color? color) async {
|
style: TextStyle(fontSize: 16, height: 1),
|
||||||
_blockColor[index - 1] = color ??
|
|
||||||
_blockSettings[index - 1].first.color;
|
|
||||||
await GStorage.setting.put(
|
|
||||||
SettingBoxKey.blockColor,
|
|
||||||
_blockColor
|
|
||||||
.map((item) => item.value
|
|
||||||
.toRadixString(16)
|
|
||||||
.substring(2))
|
|
||||||
.toList());
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
leading: Container(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Container(
|
|
||||||
height: 10,
|
|
||||||
width: 10,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: _blockColor[index - 1],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
_blockSettings[index - 1].first.name,
|
|
||||||
style:
|
|
||||||
_blockSettings[index - 1].second == SkipType.disable
|
|
||||||
? TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
trailing: PopupMenuButton(
|
|
||||||
initialValue: _blockSettings[index - 1].second,
|
|
||||||
onSelected: (item) async {
|
|
||||||
_blockSettings[index - 1].second = item;
|
|
||||||
await GStorage.setting.put(
|
|
||||||
SettingBoxKey.blockSettings,
|
|
||||||
_blockSettings
|
|
||||||
.map((item) => item.second.index)
|
|
||||||
.toList());
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
itemBuilder: (context) => SkipType.values
|
|
||||||
.map((item) => PopupMenuItem<SkipType>(
|
|
||||||
value: item,
|
|
||||||
child: Text(item.title),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_blockSettings[index - 1].second.title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: _blockSettings[index - 1].second ==
|
|
||||||
SkipType.disable
|
|
||||||
? Theme.of(context).colorScheme.error
|
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
size: 20,
|
|
||||||
Icons.keyboard_arrow_right,
|
|
||||||
color: _blockSettings[index - 1].second ==
|
|
||||||
SkipType.disable
|
|
||||||
? Theme.of(context).colorScheme.error
|
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
content: SlideColorPicker(
|
||||||
|
color: _blockColor[index],
|
||||||
|
callback: (Color? color) async {
|
||||||
|
_blockColor[index] =
|
||||||
|
color ?? _blockSettings[index].first.color;
|
||||||
|
await GStorage.setting.put(
|
||||||
|
SettingBoxKey.blockColor,
|
||||||
|
_blockColor
|
||||||
|
.map((item) =>
|
||||||
|
item.value.toRadixString(16).substring(2))
|
||||||
|
.toList());
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
separatorBuilder: (_, index) => Divider(height: 1),
|
);
|
||||||
|
},
|
||||||
|
title: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Container(
|
||||||
|
height: MediaQuery.textScalerOf(context).scale(15),
|
||||||
|
width: 10,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Container(
|
||||||
|
height: 10,
|
||||||
|
width: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: _blockColor[index],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextStyle(fontSize: 15),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ${_blockSettings[index].first.title}',
|
||||||
|
style: TextStyle(fontSize: 15),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
_blockSettings[index].first.description,
|
||||||
|
style: _blockSettings[index].second == SkipType.disable
|
||||||
|
? null
|
||||||
|
: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: PopupMenuButton(
|
||||||
|
initialValue: _blockSettings[index].second,
|
||||||
|
onSelected: (item) async {
|
||||||
|
_blockSettings[index].second = item;
|
||||||
|
await GStorage.setting.put(SettingBoxKey.blockSettings,
|
||||||
|
_blockSettings.map((item) => item.second.index).toList());
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => SkipType.values
|
||||||
|
.map((item) => PopupMenuItem<SkipType>(
|
||||||
|
value: item,
|
||||||
|
child: Text(item.title),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_blockSettings[index].second.title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _blockSettings[index].second == SkipType.disable
|
||||||
|
? Theme.of(context).colorScheme.error
|
||||||
|
: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
size: 20,
|
||||||
|
Icons.keyboard_arrow_right,
|
||||||
|
color: _blockSettings[index].second == SkipType.disable
|
||||||
|
? Theme.of(context).colorScheme.error
|
||||||
|
: Theme.of(context).colorScheme.primary,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
separatorBuilder: (_, index) => Divider(height: 1),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(child: Divider(height: 1)),
|
||||||
|
SliverToBoxAdapter(child: _aboudItem),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 25 + MediaQuery.paddingOf(context).bottom,
|
||||||
|
)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,24 +33,49 @@ enum SegmentType {
|
|||||||
preview,
|
preview,
|
||||||
music_offtopic,
|
music_offtopic,
|
||||||
poi_highlight,
|
poi_highlight,
|
||||||
chapter,
|
|
||||||
filler,
|
filler,
|
||||||
exclusive_access
|
exclusive_access
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SegmentTypeExt on SegmentType {
|
extension SegmentTypeExt on SegmentType {
|
||||||
|
/// from https://github.com/hanydd/BilibiliSponsorBlock/*/public/_locales/zh_CN/messages.json
|
||||||
|
String get title => [
|
||||||
|
'赞助广告', //sponsor
|
||||||
|
'无偿/自我推广', //selfpromo
|
||||||
|
'三连/订阅提醒', //interaction
|
||||||
|
'过场/开场动画', //intro
|
||||||
|
'鸣谢/结束画面', //outro
|
||||||
|
'回顾/概要', //preview
|
||||||
|
'音乐:非音乐部分', //music_offtopic
|
||||||
|
'精彩时刻/重点', //poi_highlight
|
||||||
|
'离题闲聊/玩笑', //filler
|
||||||
|
'品牌合作', //exclusive_access
|
||||||
|
][index];
|
||||||
|
|
||||||
|
String get description => [
|
||||||
|
'付费推广、付费推荐和直接广告。不是自我推广或免费提及他们喜欢的商品/创作者/网站/产品。', //sponsor
|
||||||
|
'类似于 “赞助广告” ,但无报酬或是自我推广。包括有关商品、捐赠的部分或合作者的信息。', //selfpromo
|
||||||
|
'视频中间简短提醒观众来一键三连或关注。 如果片段较长,或是有具体内容,则应分类为自我推广。', //interaction
|
||||||
|
'没有实际内容的间隔片段。可以是暂停、静态帧或重复动画。不适用于包含内容的过场。', //intro
|
||||||
|
'致谢画面或片尾画面。不包含内容的结尾。', //outro
|
||||||
|
'展示此视频或同系列视频将出现的画面集锦,片段中所有内容都将在之后的正片中再次出现。', //preview
|
||||||
|
'仅用于音乐视频。此分类只能用于音乐视频中未包括于其他分类的部分。', //music_offtopic
|
||||||
|
'大部分人都在寻找的空降时间。类似于“封面在12:34”的评论。', //poi_highlight
|
||||||
|
"仅作为填充内容或增添趣味而添加的离题片段,这些内容对理解视频的主要内容并非必需。这不包括提供背景信息或上下文的片段。这是一个非常激进的分类,适用于当你不想看'娱乐性'内容的时候。", //filler
|
||||||
|
'仅用于对整个视频进行标记。适用于展示UP主免费或获得补贴后使用的产品、服务或场地的视频。', //exclusive_access
|
||||||
|
][index];
|
||||||
|
|
||||||
Color get color => [
|
Color get color => [
|
||||||
Colors.amber,
|
Color(0xFF00d400), //sponsor
|
||||||
Colors.blue,
|
Color(0xFFffff00), //selfpromo
|
||||||
Colors.red,
|
Color(0xFFcc00ff), //interaction
|
||||||
Colors.indigo,
|
Color(0xFF00ffff), //intro
|
||||||
Colors.pink,
|
Color(0xFF0202ed), //outro
|
||||||
Colors.purple,
|
Color(0xFF008fd6), //preview
|
||||||
Colors.lightGreen,
|
Color(0xFFff9900), //music_offtopic
|
||||||
Colors.teal,
|
Color(0xFFff1684), //poi_highlight
|
||||||
Colors.cyan,
|
Color(0xFF7300FF), //filler
|
||||||
Colors.yellow,
|
Color(0xFF008a5c), //exclusive_access
|
||||||
Colors.orange
|
|
||||||
][index];
|
][index];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,11 +87,15 @@ extension SkipTypeExt on SkipType {
|
|||||||
|
|
||||||
class SegmentModel {
|
class SegmentModel {
|
||||||
SegmentModel({
|
SegmentModel({
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
required this.UUID,
|
||||||
required this.segmentType,
|
required this.segmentType,
|
||||||
required this.segment,
|
required this.segment,
|
||||||
required this.skipType,
|
required this.skipType,
|
||||||
required this.hasSkipped,
|
required this.hasSkipped,
|
||||||
});
|
});
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
String UUID;
|
||||||
SegmentType segmentType;
|
SegmentType segmentType;
|
||||||
Pair<int, int> segment;
|
Pair<int, int> segment;
|
||||||
SkipType skipType;
|
SkipType skipType;
|
||||||
@@ -215,13 +244,256 @@ class VideoDetailController extends GetxController
|
|||||||
double? _blockLimit;
|
double? _blockLimit;
|
||||||
List<Pair<SegmentType, SkipType>>? _blockSettings;
|
List<Pair<SegmentType, SkipType>>? _blockSettings;
|
||||||
List<Color>? _blockColor;
|
List<Color>? _blockColor;
|
||||||
List<SegmentModel>? _segmentList;
|
RxList<SegmentModel> segmentList = <SegmentModel>[].obs;
|
||||||
List<Segment>? _segmentProgressList;
|
List<Segment>? _segmentProgressList;
|
||||||
|
Color _getColor(SegmentType segment) =>
|
||||||
|
_blockColor?[segment.index] ?? segment.color;
|
||||||
|
|
||||||
|
Future _vote(String uuid, int type) async {
|
||||||
|
Request()
|
||||||
|
.post(
|
||||||
|
'${HttpString.sponsorBlockBaseUrl}/api/voteOnSponsorTime',
|
||||||
|
queryParameters: {
|
||||||
|
'UUID': uuid,
|
||||||
|
'userID': GStorage.blockUserID,
|
||||||
|
'type': 1,
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then((res) {
|
||||||
|
SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCategoryDialog(BuildContext context, SegmentModel segment) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: SegmentType.values
|
||||||
|
.map((item) => ListTile(
|
||||||
|
dense: true,
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
Request().post(
|
||||||
|
'${HttpString.sponsorBlockBaseUrl}/api/voteOnSponsorTime',
|
||||||
|
queryParameters: {
|
||||||
|
'UUID': segment.UUID,
|
||||||
|
'userID': GStorage.blockUserID,
|
||||||
|
'category': item.name,
|
||||||
|
},
|
||||||
|
).then((res) {
|
||||||
|
SmartDialog.showToast(
|
||||||
|
'类别更改${res.statusCode == 200 ? '成功' : '失败'}');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Container(
|
||||||
|
height:
|
||||||
|
MediaQuery.textScalerOf(context).scale(14),
|
||||||
|
width: 10,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Container(
|
||||||
|
height: 10,
|
||||||
|
width: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: _getColor(item),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ${item.title}',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showVoteDialog(BuildContext context, SegmentModel segment) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
'赞成票',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
_vote(segment.UUID, 1);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
'反对票',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
_vote(segment.UUID, 0);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
'更改类别',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
_showCategoryDialog(context, segment);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showSponsorBlock(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: segmentList
|
||||||
|
.map(
|
||||||
|
(item) => ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
_showVoteDialog(context, item);
|
||||||
|
},
|
||||||
|
dense: true,
|
||||||
|
title: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Container(
|
||||||
|
height:
|
||||||
|
MediaQuery.textScalerOf(context).scale(14),
|
||||||
|
width: 10,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Container(
|
||||||
|
height: 10,
|
||||||
|
width: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: _getColor(item.segmentType),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ${item.segmentType.title}',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.only(left: 16, right: 8),
|
||||||
|
subtitle: Text(
|
||||||
|
'${Utils.formatDuration(item.segment.first)} 至 ${Utils.formatDuration(item.segment.second)}',
|
||||||
|
style: TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.skipType.title,
|
||||||
|
style: TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
if (item.skipType == SkipType.showOnly)
|
||||||
|
SizedBox(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
child: IconButton(
|
||||||
|
tooltip: '跳转至此片段',
|
||||||
|
onPressed: () async {
|
||||||
|
Get.back();
|
||||||
|
try {
|
||||||
|
plPlayerController.danmakuController?.clear();
|
||||||
|
await plPlayerController.videoPlayerController
|
||||||
|
?.seek(Duration(
|
||||||
|
seconds: item.segment.first));
|
||||||
|
SmartDialog.showToast(
|
||||||
|
'已跳至${Utils.formatDuration(item.segment.first)}',
|
||||||
|
displayType: SmartToastType.normal,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
SmartDialog.showToast(
|
||||||
|
'跳转失败: $e',
|
||||||
|
displayType: SmartToastType.normal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
Icons.my_location,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future _sponsorBlock() async {
|
Future _sponsorBlock() async {
|
||||||
dynamic result = await Request().get(
|
dynamic result = await Request().get(
|
||||||
'https://www.bsbsb.top/api/skipSegments',
|
'${HttpString.sponsorBlockBaseUrl}/api/skipSegments',
|
||||||
data: {'videoID': bvid},
|
data: {
|
||||||
|
'videoID': bvid,
|
||||||
|
'cid': cid.value,
|
||||||
|
},
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {
|
headers: {
|
||||||
'env': '',
|
'env': '',
|
||||||
@@ -243,10 +515,10 @@ class VideoDetailController extends GetxController
|
|||||||
.toList()
|
.toList()
|
||||||
.map((item) => item.first.name)
|
.map((item) => item.first.name)
|
||||||
.toList();
|
.toList();
|
||||||
_segmentList = (result.data as List)
|
segmentList.value = (result.data as List)
|
||||||
.where((item) =>
|
.where((item) =>
|
||||||
enableList.contains(item['category']) &&
|
enableList.contains(item['category']) &&
|
||||||
item['segment'][1] > 0 &&
|
// item['segment'][1] > 0 &&
|
||||||
item['segment'][1] >= item['segment'][0])
|
item['segment'][1] >= item['segment'][0])
|
||||||
.map(
|
.map(
|
||||||
(item) {
|
(item) {
|
||||||
@@ -260,6 +532,7 @@ class VideoDetailController extends GetxController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SegmentModel(
|
return SegmentModel(
|
||||||
|
UUID: item['UUID'],
|
||||||
segmentType: segmentType,
|
segmentType: segmentType,
|
||||||
segment: Pair(
|
segment: Pair(
|
||||||
first: _convert(item['segment'][0]),
|
first: _convert(item['segment'][0]),
|
||||||
@@ -270,7 +543,7 @@ class VideoDetailController extends GetxController
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
).toList();
|
).toList();
|
||||||
_segmentProgressList = _segmentList?.map((item) {
|
_segmentProgressList = segmentList.map((item) {
|
||||||
double start = (item.segment.first / ((data.timeLength ?? 0) / 1000))
|
double start = (item.segment.first / ((data.timeLength ?? 0) / 1000))
|
||||||
.clamp(0.0, 1.0);
|
.clamp(0.0, 1.0);
|
||||||
double end = (item.segment.second / ((data.timeLength ?? 0) / 1000))
|
double end = (item.segment.second / ((data.timeLength ?? 0) / 1000))
|
||||||
@@ -278,11 +551,11 @@ class VideoDetailController extends GetxController
|
|||||||
return Segment(
|
return Segment(
|
||||||
start,
|
start,
|
||||||
start == end ? (end + 0.01).clamp(0.0, 1.0) : end,
|
start == end ? (end + 0.01).clamp(0.0, 1.0) : end,
|
||||||
_blockColor?[item.segmentType.index] ?? item.segmentType.color,
|
_getColor(item.segmentType),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint(e.toString());
|
debugPrint('filed to parse sponsorblock: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,14 +569,14 @@ class VideoDetailController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _initSkip() {
|
void _initSkip() {
|
||||||
if (_segmentList != null && _segmentList!.isNotEmpty) {
|
if (segmentList.isNotEmpty) {
|
||||||
positionSubscription = plPlayerController
|
positionSubscription = plPlayerController
|
||||||
.videoPlayerController?.stream.position
|
.videoPlayerController?.stream.position
|
||||||
.listen((position) async {
|
.listen((position) async {
|
||||||
int currentPos = position.inSeconds;
|
int currentPos = position.inSeconds;
|
||||||
if (currentPos != _lastPos) {
|
if (currentPos != _lastPos) {
|
||||||
_lastPos = currentPos;
|
_lastPos = currentPos;
|
||||||
for (SegmentModel item in _segmentList!) {
|
for (SegmentModel item in segmentList) {
|
||||||
// debugPrint(
|
// debugPrint(
|
||||||
// '${position.inSeconds},,${item.segment.first},,${item.segment.second},,${item.skipType.name},,${item.hasSkipped}');
|
// '${position.inSeconds},,${item.segment.first},,${item.segment.second},,${item.skipType.name},,${item.hasSkipped}');
|
||||||
if (item.segment.first == position.inSeconds) {
|
if (item.segment.first == position.inSeconds) {
|
||||||
@@ -315,11 +588,17 @@ class VideoDetailController extends GetxController
|
|||||||
?.seek(Duration(seconds: item.segment.second));
|
?.seek(Duration(seconds: item.segment.second));
|
||||||
// await plPlayerController
|
// await plPlayerController
|
||||||
// .seekTo(Duration(seconds: item.segment.second));
|
// .seekTo(Duration(seconds: item.segment.second));
|
||||||
SmartDialog.showToast('已跳过${item.segmentType.name}');
|
SmartDialog.showToast(
|
||||||
|
'已跳过${item.segmentType.title}片段',
|
||||||
|
displayType: SmartToastType.normal,
|
||||||
|
);
|
||||||
item.hasSkipped = true;
|
item.hasSkipped = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('failed to skip: $e');
|
debugPrint('failed to skip: $e');
|
||||||
SmartDialog.showToast('${item.segmentType.name}跳过失败');
|
SmartDialog.showToast(
|
||||||
|
'${item.segmentType.title}片段跳过失败',
|
||||||
|
displayType: SmartToastType.normal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1454,6 +1454,27 @@ class _HeaderControlState extends State<HeaderControl> {
|
|||||||
// ),
|
// ),
|
||||||
// fuc: () => _.screenshot(),
|
// fuc: () => _.screenshot(),
|
||||||
// ),
|
// ),
|
||||||
|
Obx(
|
||||||
|
() => widget.videoDetailCtr?.segmentList.isNotEmpty == true
|
||||||
|
? SizedBox(
|
||||||
|
width: 42,
|
||||||
|
height: 34,
|
||||||
|
child: IconButton(
|
||||||
|
tooltip: 'SponsorBlock',
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
widget.videoDetailCtr?.showSponsorBlock(context),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.block,
|
||||||
|
size: 19,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 42,
|
width: 42,
|
||||||
height: 34,
|
height: 34,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:PiliPalaX/models/model_owner.dart';
|
|||||||
import 'package:PiliPalaX/models/search/hot.dart';
|
import 'package:PiliPalaX/models/search/hot.dart';
|
||||||
import 'package:PiliPalaX/models/user/info.dart';
|
import 'package:PiliPalaX/models/user/info.dart';
|
||||||
import 'global_data.dart';
|
import 'global_data.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class GStorage {
|
class GStorage {
|
||||||
static late final Box<dynamic> userInfo;
|
static late final Box<dynamic> userInfo;
|
||||||
@@ -49,6 +50,16 @@ class GStorage {
|
|||||||
static double get blockLimit =>
|
static double get blockLimit =>
|
||||||
setting.get(SettingBoxKey.blockLimit, defaultValue: 0.0);
|
setting.get(SettingBoxKey.blockLimit, defaultValue: 0.0);
|
||||||
|
|
||||||
|
static String get blockUserID {
|
||||||
|
String blockUserID =
|
||||||
|
setting.get(SettingBoxKey.blockUserID, defaultValue: '');
|
||||||
|
if (blockUserID.isEmpty) {
|
||||||
|
blockUserID = Uuid().v4().replaceAll('-', '');
|
||||||
|
setting.put(SettingBoxKey.blockUserID, blockUserID);
|
||||||
|
}
|
||||||
|
return blockUserID;
|
||||||
|
}
|
||||||
|
|
||||||
static ThemeMode get themeMode {
|
static ThemeMode get themeMode {
|
||||||
switch (setting.get(SettingBoxKey.themeMode,
|
switch (setting.get(SettingBoxKey.themeMode,
|
||||||
defaultValue: ThemeType.system.code)) {
|
defaultValue: ThemeType.system.code)) {
|
||||||
@@ -225,6 +236,7 @@ class SettingBoxKey {
|
|||||||
blockSettings = 'blockSettings',
|
blockSettings = 'blockSettings',
|
||||||
blockLimit = 'blockLimit',
|
blockLimit = 'blockLimit',
|
||||||
blockColor = 'blockColor',
|
blockColor = 'blockColor',
|
||||||
|
blockUserID = 'blockUserID',
|
||||||
|
|
||||||
// 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细
|
// 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细
|
||||||
danmakuWeight = 'danmakuWeight',
|
danmakuWeight = 'danmakuWeight',
|
||||||
|
|||||||
@@ -602,6 +602,22 @@ class Utils {
|
|||||||
return v.toString() + random.nextInt(9999).toString();
|
return v.toString() + random.nextInt(9999).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String formatDuration(int seconds) {
|
||||||
|
int hours = seconds ~/ 3600;
|
||||||
|
int minutes = (seconds % 3600) ~/ 60;
|
||||||
|
int remainingSeconds = seconds % 60;
|
||||||
|
|
||||||
|
String minutesStr = minutes.toString().padLeft(2, '0');
|
||||||
|
String secondsStr = remainingSeconds.toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
String hoursStr = hours.toString().padLeft(2, '0');
|
||||||
|
return "$hoursStr:$minutesStr:$secondsStr";
|
||||||
|
} else {
|
||||||
|
return "$minutesStr:$secondsStr";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int duration(String duration) {
|
static int duration(String duration) {
|
||||||
List timeList = duration.split(':');
|
List timeList = duration.split(':');
|
||||||
int len = timeList.length;
|
int len = timeList.length;
|
||||||
|
|||||||
Reference in New Issue
Block a user