Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-09-19 15:32:46 +08:00
parent 099c7b4dff
commit 51c605f5d0
16 changed files with 227 additions and 171 deletions

View File

@@ -22,22 +22,18 @@
<br/> <br/>
</div> </div>
## 开发环境
```bash
[] Flutter (Channel stable, 3.24.0, on Microsoft Windows [版本 10.0.19045.4046], locale zh-CN)
[] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[] Xcode - develop for iOS and macOS (Xcode 15.1)
[] Chrome - develop for the web
[] Android Studio (version 2022.3)
[] VS Code (version 1.85.1)
[] Connected device (3 available)
[] Network resources
```
<br/> <br/>
## 适配平台
- [x] Android
- [x] iOS
- [x] Pad
- [x] Windows
<br/>
## refactor ## refactor
@@ -136,11 +132,6 @@
## 功能 ## 功能
目前着重移动端(Android、iOS)和Pad端暂时没有适配桌面端、手表端等
<br/>
- [x] 推荐视频列表(app端) - [x] 推荐视频列表(app端)
- [x] 最热视频列表 - [x] 最热视频列表
- [x] 热门直播 - [x] 热门直播

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -42,9 +42,11 @@ class _EmotePanelState extends State<EmotePanel>
ThemeData theme, ThemeData theme,
LoadingState<List<Package>?> loadingState, LoadingState<List<Package>?> loadingState,
) { ) {
late final color = Get.currentRoute.startsWith('/whisperDetail') late final color = ElevationOverlay.colorWithOverlay(
? theme.colorScheme.surface theme.colorScheme.surface,
: theme.colorScheme.onInverseSurface; theme.hoverColor,
Get.currentRoute.startsWith('/whisperDetail') ? 8 : 2,
);
return switch (loadingState) { return switch (loadingState) {
Loading() => loadingWidget, Loading() => loadingWidget,
Success(:var response) => Success(:var response) =>

View File

@@ -74,7 +74,7 @@ class EpisodePanel extends CommonSlidePage {
final int initialTabIndex; final int initialTabIndex;
final bool? isSupportReverse; final bool? isSupportReverse;
final bool? isReversed; final bool? isReversed;
final ValueChanged<ugc.BaseEpisodeItem> onChangeEpisode; final Future<bool> Function(ugc.BaseEpisodeItem) onChangeEpisode;
final VoidCallback? onReverse; final VoidCallback? onReverse;
final VoidCallback? onClose; final VoidCallback? onClose;
@@ -432,19 +432,23 @@ class _EpisodePanelState extends State<EpisodePanel>
} }
SmartDialog.showToast('切换到:$title'); SmartDialog.showToast('切换到:$title');
widget.onClose?.call(); widget.onClose?.call();
if (!showTitle) {
_currentItemIndex = index; widget.onChangeEpisode(episode).then((res) {
} if (res) {
widget.onChangeEpisode(episode); if (!showTitle) {
if (widget.type == EpisodeType.season) { _currentItemIndex = index;
try { }
Get.find<VideoDetailController>( if (widget.type == EpisodeType.season) {
tag: widget.ugcIntroController!.heroTag, try {
).seasonCid = episode.cid; Get.find<VideoDetailController>(
} catch (_) { tag: widget.ugcIntroController!.heroTag,
if (kDebugMode) rethrow; ).seasonCid = episode.cid;
} catch (_) {
if (kDebugMode) rethrow;
}
}
} }
} });
}, },
onLongPress: () { onLongPress: () {
if (cover?.isNotEmpty == true) { if (cover?.isNotEmpty == true) {

View File

@@ -259,57 +259,58 @@ class _CreateFavPageState extends State<CreateFavPage> {
], ],
ListTile( ListTile(
tileColor: theme.colorScheme.onInverseSurface, tileColor: theme.colorScheme.onInverseSurface,
leading: Text.rich( title: Row(
style: const TextStyle( children: [
height: 1, SizedBox(
fontSize: 14, width: 55,
), child: Text.rich(
TextSpan( TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: '*', text: '*',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
height: 1, color: theme.colorScheme.error,
color: theme.colorScheme.error, ),
),
const TextSpan(
text: '名称',
style: TextStyle(fontSize: 14),
),
],
), ),
), ),
const TextSpan( ),
text: '名称', Expanded(
child: TextField(
autofocus: true,
readOnly: _attr != null && FavUtils.isDefaultFav(_attr!),
controller: _titleController,
style: TextStyle( style: TextStyle(
height: 1,
fontSize: 14, fontSize: 14,
color: _attr != null && FavUtils.isDefaultFav(_attr!)
? theme.colorScheme.outline
: null,
),
inputFormatters: [
LengthLimitingTextInputFormatter(20),
],
decoration: InputDecoration(
isDense: true,
hintText: '名称',
hintStyle: TextStyle(
fontSize: 14,
color: theme.colorScheme.outline,
),
border: const OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0,
),
contentPadding: EdgeInsets.zero,
), ),
), ),
], ),
),
),
title: TextField(
autofocus: true,
readOnly: _attr != null && FavUtils.isDefaultFav(_attr!),
controller: _titleController,
style: TextStyle(
fontSize: 14,
color: _attr != null && FavUtils.isDefaultFav(_attr!)
? theme.colorScheme.outline
: null,
),
inputFormatters: [
LengthLimitingTextInputFormatter(20),
], ],
decoration: InputDecoration(
isDense: true,
hintText: '名称',
hintStyle: TextStyle(
fontSize: 14,
color: theme.colorScheme.outline,
),
border: const OutlineInputBorder(
borderSide: BorderSide.none,
gapPadding: 0,
),
contentPadding: EdgeInsets.zero,
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -319,24 +320,16 @@ class _CreateFavPageState extends State<CreateFavPage> {
title: Row( title: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text.rich( SizedBox(
TextSpan( width: 55,
children: [ child: Text(
TextSpan( '简介',
text: '简介', style: TextStyle(
style: TextStyle( fontSize: 14,
fontSize: 14, color: theme.colorScheme.onSurfaceVariant,
color: theme.colorScheme.onSurfaceVariant, ),
),
),
const TextSpan(
text: '*',
style: TextStyle(color: Colors.transparent),
),
],
), ),
), ),
const SizedBox(width: 16),
Expanded( Expanded(
child: TextField( child: TextField(
minLines: 6, minLines: 6,

View File

@@ -254,20 +254,25 @@ class _HistoryPageState extends State<HistoryPage>
padding: const EdgeInsets.only(left: 16, right: 6), padding: const EdgeInsets.only(left: 16, right: 6),
child: Row( child: Row(
children: [ children: [
Icon(
Icons.info_outline,
size: 18,
color: theme.onSecondaryContainer,
),
const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text.rich(
'历史记录功能已关闭',
strutStyle: const StrutStyle(height: 1, leading: 0), strutStyle: const StrutStyle(height: 1, leading: 0),
style: TextStyle( style: TextStyle(
height: 1, height: 1,
color: theme.onSecondaryContainer, color: theme.onSecondaryContainer,
), ),
TextSpan(
children: [
WidgetSpan(
child: Icon(
Icons.info_outline,
size: 18,
color: theme.onSecondaryContainer,
),
),
const TextSpan(text: ' 历史记录功能已关闭'),
],
),
), ),
), ),
GestureDetector( GestureDetector(

View File

@@ -45,7 +45,11 @@ class _LiveEmotePanelState extends State<LiveEmotePanel>
Widget _buildBody(LoadingState<List<LiveEmoteDatum>?> loadingState) { Widget _buildBody(LoadingState<List<LiveEmoteDatum>?> loadingState) {
late final theme = Theme.of(context); late final theme = Theme.of(context);
late final color = theme.colorScheme.onInverseSurface; late final color = ElevationOverlay.colorWithOverlay(
theme.colorScheme.surface,
theme.hoverColor,
2,
);
return switch (loadingState) { return switch (loadingState) {
Loading() => loadingWidget, Loading() => loadingWidget,
Success(:var response) => Success(:var response) =>

View File

@@ -140,7 +140,7 @@ class LiveRoomChatPanel extends StatelessWidget {
), ),
padding: const EdgeInsets.fromLTRB(10, 4, 4, 4), padding: const EdgeInsets.fromLTRB(10, 4, 4, 4),
child: Text.rich( child: Text.rich(
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white, height: 1),
strutStyle: const StrutStyle(height: 1, leading: 0), strutStyle: const StrutStyle(height: 1, leading: 0),
TextSpan( TextSpan(
children: [ children: [

View File

@@ -51,8 +51,12 @@ class _MainAppState extends State<MainApp>
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
final brightness = Theme.brightnessOf(context);
NetworkImgLayer.reduce = NetworkImgLayer.reduce =
NetworkImgLayer.reduceLuxColor != null && context.isDarkMode; NetworkImgLayer.reduceLuxColor != null && brightness.isDark;
if (Utils.isDesktop) {
windowManager.setBrightness(brightness);
}
PageUtils.routeObserver.subscribe( PageUtils.routeObserver.subscribe(
this, this,
ModalRoute.of(context) as PageRoute, ModalRoute.of(context) as PageRoute,
@@ -160,6 +164,8 @@ class _MainAppState extends State<MainApp>
Future<void> _handleTray() async { Future<void> _handleTray() async {
if (Platform.isWindows) { if (Platform.isWindows) {
await trayManager.setIcon('assets/images/logo/app_icon.ico'); await trayManager.setIcon('assets/images/logo/app_icon.ico');
} else {
await trayManager.setIcon('assets/images/logo/logo_large.png');
} }
if (!Platform.isLinux) { if (!Platform.isLinux) {
await trayManager.setToolTip(Constants.appName); await trayManager.setToolTip(Constants.appName);

View File

@@ -1,6 +1,7 @@
import 'package:PiliPlus/models_new/video/video_ai_conclusion/model_result.dart'; import 'package:PiliPlus/models_new/video/video_ai_conclusion/model_result.dart';
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/controller.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/selectable_text.dart';
import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -69,7 +70,7 @@ class _AiDetailState extends State<AiConclusionPanel>
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14), padding: const EdgeInsets.symmetric(horizontal: 14),
child: SelectableText( child: selectableText(
widget.item.summary!, widget.item.summary!,
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
@@ -98,57 +99,59 @@ class _AiDetailState extends State<AiConclusionPanel>
itemCount: widget.item.outline!.length, itemCount: widget.item.outline!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = widget.item.outline![index]; final item = widget.item.outline![index];
return Column( return SelectionArea(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
if (index != 0) const SizedBox(height: 10), children: [
SelectableText( if (index != 0) const SizedBox(height: 10),
item.title!, Text(
style: const TextStyle( item.title!,
fontSize: 14, style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 14,
height: 1.5, fontWeight: FontWeight.bold,
height: 1.5,
),
), ),
), const SizedBox(height: 6),
const SizedBox(height: 6), ...?item.partOutline?.map(
...?item.partOutline?.map( (item) => Wrap(
(item) => Wrap( children: [
children: [ Text.rich(
SelectableText.rich( TextSpan(
TextSpan( style: TextStyle(
style: TextStyle( fontSize: 14,
fontSize: 14, color: theme.colorScheme.onSurface,
color: theme.colorScheme.onSurface, height: 1.5,
height: 1.5,
),
children: [
TextSpan(
text: DurationUtils.formatDuration(
item.timestamp,
),
style: TextStyle(
color: theme.colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
try {
Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'],
).plPlayerController.seekTo(
Duration(seconds: item.timestamp!),
);
} catch (_) {}
},
), ),
const TextSpan(text: ' '), children: [
TextSpan(text: item.content!), TextSpan(
], text: DurationUtils.formatDuration(
item.timestamp,
),
style: TextStyle(
color: theme.colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
try {
Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'],
).plPlayerController.seekTo(
Duration(seconds: item.timestamp!),
);
} catch (_) {}
},
),
const TextSpan(text: ' '),
TextSpan(text: item.content!),
],
),
), ),
), ],
], ),
), ),
), ],
], ),
); );
}, },
), ),

View File

@@ -277,7 +277,7 @@ class PgcIntroController extends CommonIntroController {
} }
// 修改分P或番剧分集 // 修改分P或番剧分集
Future<void> onChangeEpisode(BaseEpisodeItem episode) async { Future<bool> onChangeEpisode(BaseEpisodeItem episode) async {
try { try {
final int epId = episode.epId ?? episode.id!; final int epId = episode.epId ?? episode.id!;
final String bvid = episode.bvid ?? this.bvid; final String bvid = episode.bvid ?? this.bvid;
@@ -285,7 +285,7 @@ class PgcIntroController extends CommonIntroController {
final int? cid = final int? cid =
episode.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); episode.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
if (cid == null) { if (cid == null) {
return; return false;
} }
final String? cover = episode.cover; final String? cover = episode.cover;
@@ -323,8 +323,10 @@ class PgcIntroController extends CommonIntroController {
this.cid.value = cid; this.cid.value = cid;
queryOnlineTotal(); queryOnlineTotal();
queryVideoIntro(episode as EpisodeItem); queryVideoIntro(episode as EpisodeItem);
return true;
} catch (e) { } catch (e) {
if (kDebugMode) debugPrint('pgc onChangeEpisode: $e'); if (kDebugMode) debugPrint('pgc onChangeEpisode: $e');
return false;
} }
} }

View File

@@ -9,6 +9,7 @@ import 'package:PiliPlus/models_new/video/video_tag/data.dart';
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
import 'package:PiliPlus/pages/pgc_review/view.dart'; import 'package:PiliPlus/pages/pgc_review/view.dart';
import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/selectable_text.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart' hide TabBarView; import 'package:flutter/material.dart' hide TabBarView;
@@ -108,7 +109,7 @@ class _IntroDetailState extends State<PgcIntroPanel>
bottom: MediaQuery.viewPaddingOf(context).bottom + 100, bottom: MediaQuery.viewPaddingOf(context).bottom + 100,
), ),
children: [ children: [
SelectableText( selectableText(
widget.item.title!, widget.item.title!,
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
), ),
@@ -152,7 +153,7 @@ class _IntroDetailState extends State<PgcIntroPanel>
style: theme.textTheme.titleMedium, style: theme.textTheme.titleMedium,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
SelectableText( selectableText(
widget.item.evaluate!, widget.item.evaluate!,
style: textStyle, style: textStyle,
), ),

View File

@@ -463,7 +463,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
} }
// 修改分P或番剧分集 // 修改分P或番剧分集
Future<void> onChangeEpisode( Future<bool> onChangeEpisode(
BaseEpisodeItem episode, { BaseEpisodeItem episode, {
bool isStein = false, bool isStein = false,
}) async { }) async {
@@ -473,7 +473,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
final int? cid = final int? cid =
episode.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); episode.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
if (cid == null) { if (cid == null) {
return; return false;
} }
final String? cover = episode.cover; final String? cover = episode.cover;
@@ -488,7 +488,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
cid: cid, cid: cid,
cover: cover, cover: cover,
); );
return; return false;
} }
} }
@@ -546,8 +546,10 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
this.cid.value = cid; this.cid.value = cid;
queryOnlineTotal(); queryOnlineTotal();
return true;
} catch (e) { } catch (e) {
if (kDebugMode) debugPrint('ugc onChangeEpisode: $e'); if (kDebugMode) debugPrint('ugc onChangeEpisode: $e');
return false;
} }
} }

View File

@@ -15,6 +15,7 @@ import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/page.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/page.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/season.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/season.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/selectable_text.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_state.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_state.dart';
import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/date_utils.dart';
@@ -327,13 +328,9 @@ class _UgcIntroPanelState extends TripleState<UgcIntroPanel> {
), ),
if (videoDetail.descV2?.isNotEmpty == true) ...[ if (videoDetail.descV2?.isNotEmpty == true) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
SelectableText.rich( selectableRichText(
style: const TextStyle(height: 1.4), style: const TextStyle(height: 1.4),
TextSpan( buildContent(theme, videoDetail),
children: [
buildContent(theme, videoDetail),
],
),
), ),
], ],
Obx(() { Obx(() {
@@ -601,7 +598,7 @@ class _UgcIntroPanelState extends TripleState<UgcIntroPanel> {
caseSensitive: false, caseSensitive: false,
); );
InlineSpan buildContent(ThemeData theme, VideoDetailData content) { TextSpan buildContent(ThemeData theme, VideoDetailData content) {
if (content.descV2.isNullOrEmpty) { if (content.descV2.isNullOrEmpty) {
return const TextSpan(); return const TextSpan();
} }

View File

@@ -0,0 +1,38 @@
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
Widget selectableText(
String text, {
TextStyle? style,
}) {
if (Utils.isDesktop) {
return SelectionArea(
child: Text(
style: style,
text,
),
);
}
return SelectableText(
style: style,
text,
);
}
Widget selectableRichText(
TextSpan textSpan, {
TextStyle? style,
}) {
if (Utils.isDesktop) {
return SelectionArea(
child: Text.rich(
style: style,
textSpan,
),
);
}
return SelectableText.rich(
style: style,
textSpan,
);
}

View File

@@ -44,6 +44,11 @@ class _WhisperDetailPageState
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final padding = MediaQuery.viewPaddingOf(context); final padding = MediaQuery.viewPaddingOf(context);
late final containerColor = ElevationOverlay.colorWithOverlay(
theme.colorScheme.surface,
theme.hoverColor,
1,
);
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
@@ -145,8 +150,11 @@ class _WhisperDetailPageState
), ),
), ),
if (_whisperDetailController.mid != null) ...[ if (_whisperDetailController.mid != null) ...[
_buildInputView(theme), _buildInputView(theme, containerColor),
buildPanelContainer(theme, theme.colorScheme.onInverseSurface), buildPanelContainer(
theme,
containerColor,
),
] else ] else
SizedBox(height: padding.bottom), SizedBox(height: padding.bottom),
], ],
@@ -228,11 +236,11 @@ class _WhisperDetailPageState
); );
} }
Widget _buildInputView(ThemeData theme) { Widget _buildInputView(ThemeData theme, Color containerColor) {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface, color: containerColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
), ),
child: Row( child: Row(