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/>
</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/>
## 适配平台
- [x] Android
- [x] iOS
- [x] Pad
- [x] Windows
<br/>
## refactor
@@ -136,11 +132,6 @@
## 功能
目前着重移动端(Android、iOS)和Pad端暂时没有适配桌面端、手表端等
<br/>
- [x] 推荐视频列表(app端)
- [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,
LoadingState<List<Package>?> loadingState,
) {
late final color = Get.currentRoute.startsWith('/whisperDetail')
? theme.colorScheme.surface
: theme.colorScheme.onInverseSurface;
late final color = ElevationOverlay.colorWithOverlay(
theme.colorScheme.surface,
theme.hoverColor,
Get.currentRoute.startsWith('/whisperDetail') ? 8 : 2,
);
return switch (loadingState) {
Loading() => loadingWidget,
Success(:var response) =>

View File

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

View File

@@ -259,57 +259,58 @@ class _CreateFavPageState extends State<CreateFavPage> {
],
ListTile(
tileColor: theme.colorScheme.onInverseSurface,
leading: Text.rich(
style: const TextStyle(
height: 1,
fontSize: 14,
),
TextSpan(
children: [
TextSpan(
text: '*',
style: TextStyle(
fontSize: 14,
height: 1,
color: theme.colorScheme.error,
title: Row(
children: [
SizedBox(
width: 55,
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '*',
style: TextStyle(
fontSize: 14,
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(
height: 1,
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),
@@ -319,24 +320,16 @@ class _CreateFavPageState extends State<CreateFavPage> {
title: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: '简介',
style: TextStyle(
fontSize: 14,
color: theme.colorScheme.onSurfaceVariant,
),
),
const TextSpan(
text: '*',
style: TextStyle(color: Colors.transparent),
),
],
SizedBox(
width: 55,
child: Text(
'简介',
style: TextStyle(
fontSize: 14,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
minLines: 6,

View File

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

View File

@@ -45,7 +45,11 @@ class _LiveEmotePanelState extends State<LiveEmotePanel>
Widget _buildBody(LoadingState<List<LiveEmoteDatum>?> loadingState) {
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) {
Loading() => loadingWidget,
Success(:var response) =>

View File

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

View File

@@ -51,8 +51,12 @@ class _MainAppState extends State<MainApp>
@override
void didChangeDependencies() {
super.didChangeDependencies();
final brightness = Theme.brightnessOf(context);
NetworkImgLayer.reduce =
NetworkImgLayer.reduceLuxColor != null && context.isDarkMode;
NetworkImgLayer.reduceLuxColor != null && brightness.isDark;
if (Utils.isDesktop) {
windowManager.setBrightness(brightness);
}
PageUtils.routeObserver.subscribe(
this,
ModalRoute.of(context) as PageRoute,
@@ -160,6 +164,8 @@ class _MainAppState extends State<MainApp>
Future<void> _handleTray() async {
if (Platform.isWindows) {
await trayManager.setIcon('assets/images/logo/app_icon.ico');
} else {
await trayManager.setIcon('assets/images/logo/logo_large.png');
}
if (!Platform.isLinux) {
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/pages/common/slide/common_slide_page.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:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -69,7 +70,7 @@ class _AiDetailState extends State<AiConclusionPanel>
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: SelectableText(
child: selectableText(
widget.item.summary!,
style: const TextStyle(
fontSize: 15,
@@ -98,57 +99,59 @@ class _AiDetailState extends State<AiConclusionPanel>
itemCount: widget.item.outline!.length,
itemBuilder: (context, index) {
final item = widget.item.outline![index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (index != 0) const SizedBox(height: 10),
SelectableText(
item.title!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
return SelectionArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (index != 0) const SizedBox(height: 10),
Text(
item.title!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
),
const SizedBox(height: 6),
...?item.partOutline?.map(
(item) => Wrap(
children: [
SelectableText.rich(
TextSpan(
style: TextStyle(
fontSize: 14,
color: theme.colorScheme.onSurface,
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 SizedBox(height: 6),
...?item.partOutline?.map(
(item) => Wrap(
children: [
Text.rich(
TextSpan(
style: TextStyle(
fontSize: 14,
color: theme.colorScheme.onSurface,
height: 1.5,
),
const TextSpan(text: ' '),
TextSpan(text: item.content!),
],
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: ' '),
TextSpan(text: item.content!),
],
),
),
),
],
],
),
),
),
],
],
),
);
},
),

View File

@@ -277,7 +277,7 @@ class PgcIntroController extends CommonIntroController {
}
// 修改分P或番剧分集
Future<void> onChangeEpisode(BaseEpisodeItem episode) async {
Future<bool> onChangeEpisode(BaseEpisodeItem episode) async {
try {
final int epId = episode.epId ?? episode.id!;
final String bvid = episode.bvid ?? this.bvid;
@@ -285,7 +285,7 @@ class PgcIntroController extends CommonIntroController {
final int? cid =
episode.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
if (cid == null) {
return;
return false;
}
final String? cover = episode.cover;
@@ -323,8 +323,10 @@ class PgcIntroController extends CommonIntroController {
this.cid.value = cid;
queryOnlineTotal();
queryVideoIntro(episode as EpisodeItem);
return true;
} catch (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/pgc_review/view.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/utils.dart';
import 'package:flutter/material.dart' hide TabBarView;
@@ -108,7 +109,7 @@ class _IntroDetailState extends State<PgcIntroPanel>
bottom: MediaQuery.viewPaddingOf(context).bottom + 100,
),
children: [
SelectableText(
selectableText(
widget.item.title!,
style: const TextStyle(fontSize: 16),
),
@@ -152,7 +153,7 @@ class _IntroDetailState extends State<PgcIntroPanel>
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 4),
SelectableText(
selectableText(
widget.item.evaluate!,
style: textStyle,
),

View File

@@ -463,7 +463,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
}
// 修改分P或番剧分集
Future<void> onChangeEpisode(
Future<bool> onChangeEpisode(
BaseEpisodeItem episode, {
bool isStein = false,
}) async {
@@ -473,7 +473,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
final int? cid =
episode.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
if (cid == null) {
return;
return false;
}
final String? cover = episode.cover;
@@ -488,7 +488,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
cid: cid,
cover: cover,
);
return;
return false;
}
}
@@ -546,8 +546,10 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
this.cid.value = cid;
queryOnlineTotal();
return true;
} catch (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/page.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/utils/app_scheme.dart';
import 'package:PiliPlus/utils/date_utils.dart';
@@ -327,13 +328,9 @@ class _UgcIntroPanelState extends TripleState<UgcIntroPanel> {
),
if (videoDetail.descV2?.isNotEmpty == true) ...[
const SizedBox(height: 8),
SelectableText.rich(
selectableRichText(
style: const TextStyle(height: 1.4),
TextSpan(
children: [
buildContent(theme, videoDetail),
],
),
buildContent(theme, videoDetail),
),
],
Obx(() {
@@ -601,7 +598,7 @@ class _UgcIntroPanelState extends TripleState<UgcIntroPanel> {
caseSensitive: false,
);
InlineSpan buildContent(ThemeData theme, VideoDetailData content) {
TextSpan buildContent(ThemeData theme, VideoDetailData content) {
if (content.descV2.isNullOrEmpty) {
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) {
final ThemeData theme = Theme.of(context);
final padding = MediaQuery.viewPaddingOf(context);
late final containerColor = ElevationOverlay.colorWithOverlay(
theme.colorScheme.surface,
theme.hoverColor,
1,
);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
@@ -145,8 +150,11 @@ class _WhisperDetailPageState
),
),
if (_whisperDetailController.mid != null) ...[
_buildInputView(theme),
buildPanelContainer(theme, theme.colorScheme.onInverseSurface),
_buildInputView(theme, containerColor),
buildPanelContainer(
theme,
containerColor,
),
] else
SizedBox(height: padding.bottom),
],
@@ -228,11 +236,11 @@ class _WhisperDetailPageState
);
}
Widget _buildInputView(ThemeData theme) {
Widget _buildInputView(ThemeData theme, Color containerColor) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
color: containerColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Row(