feat: 视频详情页,分类页。个人中心

This commit is contained in:
chengsiwen committed 2026-07-07 16:04:15 +08:00
1 parent b5bce6edcf
commit a3426831ed
26 files changed
+2878 -1077

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:novyronst/common/api/api_string.dart';
import 'package:novyronst/common/help/app_nav.dart';
import 'package:novyronst/pages/my/pages/mine_link_page.dart';
import 'package:novyronst/pages/widgets/ny_loading_widget.dart';
import 'package:novyronst/root/lib_export.dart';
import 'package:novyronst/tools/util/ny_device_info.dart';
import 'package:novyronst/tools/widgets/extend_widget.dart';
import 'package:novyronst/tools/widgets/novy_app_bar.dart';
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutUsPage extends StatelessWidget {
const AboutUsPage({super.key});
@override
Widget build(BuildContext context) {
// return NyLoadingWidget();
return Scaffold(
backgroundColor: ConstColor.withValue('#110D08'),
appBar: NovyAppBar(
title: 'About',
titleColor: Colors.white,
),
body: SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 50),
SizedBox(
width: 80, height: 80,
child: HelpImage.asset(ConstImage.appLogo),
),
const SizedBox(height: 10),
NovyText( ApiString.appName,
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
const SizedBox(height: 5),
NovyText( 'V ${NyDeviceInfo.instance.appVersion}',
fontSize: 12,
color: Colors.grey,
),
const SizedBox(height: 20),
_buildRowItem('Privacy Policy', () {
AppNav.to(MineLinkPage(
title: 'Privacy Policy',
urlString: ApiString.webPrivacy,
));
}),
_buildRowItem('User Agreement', () {
AppNav.to(MineLinkPage(
title: 'User Agreement',
urlString: ApiString.webAgreement,
));
}),
_buildRowItem('Visit Website', () async {
final encodedUrl = Uri.encodeFull(ApiString.webIndex);
try {
final bool launched = await launchUrl(
Uri.parse(encodedUrl),
mode: LaunchMode.externalApplication,
);
if (!launched) {
print('无法打开链接: $encodedUrl');
}
} catch (e) {
print('打开链接异常: $e');
}
}),
],
),
),
);
}
_buildRowItem(String title, Function() onTap) {
return Container(
height: 46,
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
margin: const EdgeInsets.only(bottom: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
NovyText( title, fontSize: 14, color: Colors.white),
Icon(Icons.arrow_forward_ios, color: ConstColor.btWhite, size: 18,),
],
),
).addInkWell(onTap: onTap);
}
Future<void> openUrl(String url) async {
final encodedUrl = Uri.encodeFull(url);
try {
final bool launched = await launchUrl(
Uri.parse(encodedUrl),
mode: LaunchMode.externalApplication,
);
if (!launched) {
print('无法打开链接: $encodedUrl');
}
} catch (e) {
print('打开链接异常: $e');
}
}
}
+198
View File
@@ -0,0 +1,198 @@
import 'package:flutter/material.dart';
import 'package:novyronst/common/api/video_request.dart';
import 'package:novyronst/common/model/ny_drama_model.dart';
import 'package:novyronst/pages/collect/pages/collect_page_helpers.dart';
import 'package:novyronst/pages/widgets/ny_empty_view.dart';
import 'package:novyronst/pages/widgets/ny_loading_widget.dart';
import 'package:novyronst/tools/widgets/novy_app_bar.dart';
import '../../../common/const/const_color.dart';
class MineHistoryPage extends StatefulWidget {
const MineHistoryPage({super.key});
@override
State<MineHistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<MineHistoryPage> {
List<NyDramaModel> _itemsHis = [];
bool _isLoading = false;
bool _isEditing = false;
@override
void initState() {
super.initState();
_loadHistory();
}
Future<void> _loadHistory() async {
setState(() {
_isLoading = true;
});
final fetch = VideoRequest.getHistory;
final items = await fetch(page: 1, pageSize: 20);
if (!mounted) return;
setState(() {
_itemsHis
..clear()
..addAll(items);
_isLoading = false;
});
}
void _deleteHistory(int index) {
if (index < 0 || index >= _itemsHis.length) return;
setState(() {
_itemsHis.removeAt(index);
});
}
@override
Widget build(BuildContext context) {
if (_isLoading && _itemsHis.isEmpty) {
return const NyLoadingWidget();
}
if (_itemsHis.isEmpty) {
return const NyEmptyView();
}
return Scaffold(
backgroundColor: ConstColor.withValue('#110D08'),
appBar: NovyAppBar(
title: 'History',
titleColor: Colors.white,
actions: [
IconButton(
onPressed: () {
setState(() {
_isEditing = !_isEditing;
});
},
icon: Icon(
_isEditing ? Icons.done : Icons.edit_outlined,
color: Colors.white,
size: 22,
),
)
],
),
body: ListView.separated(
padding: const EdgeInsets.fromLTRB(30, 8, 20, 96),
itemCount: _itemsHis.length,
separatorBuilder: (context, index) => const SizedBox(height: 13),
itemBuilder: (context, index) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 30,
child: _HistoryLeadingButton(
isEditing: _isEditing,
onDelete: () => _deleteHistory(index),
),
),
const SizedBox(width: 18),
Expanded(child: _HistoryItem(item: _itemsHis[index])),
],
);
},
),
);
}
}
class _HistoryLeadingButton extends StatelessWidget {
const _HistoryLeadingButton({
required this.isEditing,
required this.onDelete,
});
final bool isEditing;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) {
if (isEditing) {
return IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(width: 30, height: 40),
onPressed: onDelete,
icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 28),
);
}
return Icon(
Icons.bookmark_border,
color: Colors.white.withValues(alpha: 0.28),
size: 32,
);
}
}
class _HistoryItem extends StatelessWidget {
const _HistoryItem({required this.item});
final NyDramaModel item;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 68,
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(2),
child: Image.network(
item.imageUrl,
width: 51,
height: 68,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 51,
height: 68,
color: Colors.white.withValues(alpha: 0.12),
);
},
),
),
const SizedBox(width: 15),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
height: 1.22,
fontFamily: 'Montserrat-Bold',
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
dramaEpisodeText(item),
style: TextStyle(
color: Colors.white.withValues(alpha: 0.45),
fontSize: 12,
fontFamily: 'Montserrat-Medium',
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
);
}
}
+298
View File
@@ -0,0 +1,298 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:novyronst/common/const/const_color.dart';
import 'package:novyronst/pages/widgets/ny_loading_widget.dart';
import 'package:novyronst/tools/widgets/novy_app_bar.dart';
import 'package:webview_flutter/webview_flutter.dart';
/// WebView 页面
class MineLinkPage extends StatefulWidget {
final String urlString;
final String? title;
final bool showAppbar;
const MineLinkPage({
super.key,
required this.urlString,
this.title,
this.showAppbar = true,
});
@override
State<MineLinkPage> createState() => _WebLinkViewState();
}
class _WebLinkViewState extends State<MineLinkPage> {
late WebViewController _webViewController;
int _loadingProgress = 0;
bool _isLoading = true;
bool _hasError = false;
String _errorMessage = '';
String _urlString = '';
@override
void initState() {
super.initState();
_initializeParameters();
_initializeWebView();
_loadUrl();
}
void _initializeParameters() {
_urlString = _validateAndFixUrl(widget.urlString);
}
void _initializeWebView() {
_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00FF0000))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
setState(() {
_loadingProgress = progress;
});
},
onPageStarted: (String url) {
setState(() {
_isLoading = true;
_hasError = false;
_loadingProgress = 0;
});
},
onPageFinished: (String url) async {
setState(() {
_isLoading = false;
_loadingProgress = 100;
});
// 修改网页背景为透明,文字为白色
await _webViewController.runJavaScript('''
document.body.style.background = 'none';
document.body.style.color = '#fff';
// 修改标题样式
var titleElements = document.getElementsByClassName('title');
for (var i = 0; i < titleElements.length; i++) {
titleElements[i].style.display = 'none';
}
// 修改文本背景为白色
var bgElements = document.getElementsByClassName('bg-fff');
for (var i = 0; i < bgElements.length; i++) {
bgElements[i].style.background = 'none';
bgElements[i].style.paddingTop = '0px';
}
// 修改所有文本元素为白色
var allElements = document.getElementsByTagName('*');
for (var i = 0; i < allElements.length; i++) {
allElements[i].style.color = '#fff';
}
// 修改链接颜色
var links = document.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
links[i].style.color = '#87CEEB';
}
''');
},
onHttpError: (HttpResponseError error) {
setState(() {
_hasError = true;
_errorMessage = 'HTTP Error: ${error.response?.statusCode ?? "Unknown"}';
_isLoading = false;
});
},
onWebResourceError: (WebResourceError error) {
setState(() {
_hasError = true;
_errorMessage = error.description;
_isLoading = false;
});
},
onNavigationRequest: (NavigationRequest request) {
return NavigationDecision.navigate;
},
),
);
}
void _loadUrl() {
if (_urlString.isEmpty) {
setState(() {
_hasError = true;
_errorMessage = 'Invalid URL';
_isLoading = false;
});
return;
}
try {
final uri = Uri.parse(_urlString);
_webViewController.loadRequest(uri);
} catch (e) {
setState(() {
_hasError = true;
_errorMessage = 'Failed to load URL: $e';
_isLoading = false;
});
}
}
String _validateAndFixUrl(String url) {
if (url.isEmpty) {
return '';
}
url = url.trim();
// Already has a valid scheme
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// Looks like a domain (has a dot and no spaces)
if (url.contains('.') && !url.contains(' ')) {
return 'https://$url';
}
// Treat as search query
return 'https://www.google.com/search?q=${Uri.encodeComponent(url)}';
}
void _reload() {
setState(() {
_hasError = false;
_isLoading = true;
_loadingProgress = 0;
_errorMessage = '';
});
_initializeWebView();
_loadUrl();
}
void _goBack() {
_webViewController.goBack();
}
void _goForward() {
_webViewController.goForward();
}
@override
void dispose() {
_webViewController.clearCache();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: widget.showAppbar
? _buildAppBar()
: null,
body: _buildBody(),
);
}
PreferredSizeWidget _buildAppBar() {
return NovyAppBar(
backgroundColor: Colors.transparent,
title: widget.title,
titleColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.refresh, color: Colors.white,),
onPressed: _reload,
),
],
);
}
Widget _buildBody() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
ConstColor.black,
Color(0xFF2D1B4E),
],
),
),
child: SafeArea(
child: Column(
children: [
// 加载进度条(可选)
if (_isLoading)
LinearProgressIndicator(
value: _loadingProgress / 100,
backgroundColor: Colors.white.withOpacity(0.2),
valueColor: AlwaysStoppedAnimation<Color>(
ConstColor.btGreen,
),
),
Expanded(
child: _buildContent(),
),
],
),
),
);
}
Widget _buildContent() {
if (_isLoading) {
return Center(
child: NyLoadingWidget(),
);
}
if (_hasError) {
return _buildErrorWidget();
}
return WebViewWidget(
controller: _webViewController,
);
}
Widget _buildErrorWidget() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64.w,
color: Colors.white.withOpacity(0.6),
),
SizedBox(height: 16.h),
Text(
_errorMessage,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16.sp,
),
textAlign: TextAlign.center,
),
SizedBox(height: 24.h),
ElevatedButton(
onPressed: _reload,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFB98E0),
foregroundColor: Colors.white,
),
child: const Text('重试'),
),
],
),
);
}
}
+53 -7
View File
@@ -1,8 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:novyronst/common/api/api_string.dart';
import 'package:novyronst/common/api/user_request.dart';
import 'package:novyronst/common/help/app_nav.dart';
import 'package:novyronst/common/help/help_image.dart';
import 'package:novyronst/common/model/novyronst_user.dart';
import 'package:novyronst/pages/my/pages/about_page.dart';
import 'package:novyronst/pages/my/pages/mine_history_page.dart';
import 'package:novyronst/pages/my/pages/mine_link_page.dart';
import 'package:novyronst/pages/my/widgets/ny_dash_line.dart';
import 'package:novyronst/root/lib_export.dart';
import 'package:novyronst/tools/widgets/app_bg_page.dart';
import 'package:novyronst/tools/widgets/app_toast_util.dart';
import 'package:novyronst/tools/widgets/extend_widget.dart';
import 'package:novyronst/tools/widgets/novy_com_widget.dart';
@@ -17,9 +26,25 @@ class MyPage extends StatefulWidget {
class _RootPageState extends State<MyPage> {
List<String> _titles = [
final List<String> _titles = [
'History', 'Settings', 'About', 'Privacy Policy', 'User Agreement'
];
NovyronstUser? _userInfo;
@override
void initState() {
super.initState();
_getUserInfo();
}
void _getUserInfo() {
UserRequest.getUserInfo().then((value) {
setState(() {
_userInfo = value;
});
});
}
@override
Widget build(BuildContext context) {
@@ -35,15 +60,21 @@ class _RootPageState extends State<MyPage> {
...List.generate(_titles.length, (index) {
return _buildSectionView(_titles[index], index, onTap: () {
if (index == 0) {
AppNav.to(MineHistoryPage());
} else if (index == 1) {
} else if (index == 2) {
AppNav.to(AboutUsPage());
} else if (index == 3) {
AppNav.to(MineLinkPage(urlString: ApiString.webPrivacy,
title: 'Privacy Policy',
));
} else if (index == 4) {
AppNav.to(MineLinkPage(urlString: ApiString.webAgreement,
title: 'User Agreement',
));
}
});
})
@@ -91,6 +122,16 @@ class _RootPageState extends State<MyPage> {
}
Widget _buildUserInfo() {
String _name = 'Visitor';
String _customerId = '';
if (_userInfo != null) {
_name = _userInfo!.givingName!.isEmpty || _userInfo!.givingName==''
? 'Visitor'
: _userInfo!.givingName!;
_customerId = _userInfo!.customerId;
}
return Row(
children: [
Container(
@@ -108,7 +149,7 @@ class _RootPageState extends State<MyPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NovyText('Visitor',
NovyText(_name,
fontSize: 18,
),
const SizedBox(height: 6),
@@ -121,14 +162,19 @@ class _RootPageState extends State<MyPage> {
),
child: Row(
children: [
NovyText('ID: 3242545454',
NovyText('ID: $_customerId',
fontSize: 11,
),
const SizedBox(width: 6),
HelpImage.asset(ConstImage.copy, width: 12, height: 12)
],
),
)
).addInkWell(onTap: () async {
// copy
if (_userInfo!.customerId.isEmpty) return;
await Clipboard.setData(ClipboardData(text: _userInfo!.customerId));
AppToastUtil.showSuccess('ID is Copied!');
}),
],
)
],