Phase 3.5: Finalized UI polish, added Console, and fixed stale stream status bug
This commit is contained in:
@@ -41,5 +41,8 @@ func InitDB() {
|
|||||||
log.Fatalf("Failed to migrate database: %v", err)
|
log.Fatalf("Failed to migrate database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 3.5 Fix: Reset all rooms to inactive on startup using explicit map to ensure false is updated
|
||||||
|
DB.Model(&model.Room{}).Where("1 = 1").Updates(map[string]interface{}{"is_active": false})
|
||||||
|
|
||||||
log.Println("Database initialized successfully.")
|
log.Println("Database initialized successfully.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ func NewRTMPServer() *RTMPServer {
|
|||||||
s.channels[roomLivePath] = q
|
s.channels[roomLivePath] = q
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
|
|
||||||
// Mark room as active in DB
|
// Mark room as active in DB (using map to ensure true/false is correctly updated)
|
||||||
db.DB.Model(&room).Update("is_active", true)
|
db.DB.Model(&room).Updates(map[string]interface{}{"is_active": true})
|
||||||
|
|
||||||
// 3. Cleanup on end
|
// 3. Cleanup on end
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -81,7 +81,8 @@ func NewRTMPServer() *RTMPServer {
|
|||||||
delete(s.channels, roomLivePath)
|
delete(s.channels, roomLivePath)
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
q.Close()
|
q.Close()
|
||||||
db.DB.Model(&room).Update("is_active", false) // Mark room as inactive
|
// Explicitly set is_active to false using map
|
||||||
|
db.DB.Model(&room).Updates(map[string]interface{}{"is_active": false})
|
||||||
fmt.Printf("[INFO] Publishing ended for Room ID: %d\n", room.ID)
|
fmt.Printf("[INFO] Publishing ended for Room ID: %d\n", room.ID)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -17,49 +17,40 @@ class HomePage extends StatefulWidget {
|
|||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
|
|
||||||
// 根据当前选择的索引返回对应的页面
|
|
||||||
final List<Widget> _pages = [
|
|
||||||
_ExploreView(),
|
|
||||||
MyStreamPage(),
|
|
||||||
SettingsPage(),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 检查是否为桌面/宽屏环境
|
|
||||||
bool isWide = MediaQuery.of(context).size.width > 600;
|
bool isWide = MediaQuery.of(context).size.width > 600;
|
||||||
|
|
||||||
|
final List<Widget> _pages = [
|
||||||
|
_ExploreView(onGoLive: () => setState(() => _selectedIndex = 1)),
|
||||||
|
MyStreamPage(),
|
||||||
|
SettingsPage(),
|
||||||
|
];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
// 桌面端侧边导航
|
|
||||||
if (isWide)
|
if (isWide)
|
||||||
NavigationRail(
|
NavigationRail(
|
||||||
selectedIndex: _selectedIndex,
|
selectedIndex: _selectedIndex,
|
||||||
onDestinationSelected: (int index) {
|
onDestinationSelected: (int index) => setState(() => _selectedIndex = index),
|
||||||
setState(() => _selectedIndex = index);
|
|
||||||
},
|
|
||||||
labelType: NavigationRailLabelType.all,
|
labelType: NavigationRailLabelType.all,
|
||||||
destinations: const [
|
destinations: const [
|
||||||
NavigationRailDestination(icon: Icon(Icons.explore), label: Text('Explore')),
|
NavigationRailDestination(icon: Icon(Icons.explore), label: Text('Explore')),
|
||||||
NavigationRailDestination(icon: Icon(Icons.videocam), label: Text('Stream')),
|
NavigationRailDestination(icon: Icon(Icons.videocam), label: Text('Console')),
|
||||||
NavigationRailDestination(icon: Icon(Icons.settings), label: Text('Settings')),
|
NavigationRailDestination(icon: Icon(Icons.settings), label: Text('Settings')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// 主内容区
|
|
||||||
Expanded(child: _pages[_selectedIndex]),
|
Expanded(child: _pages[_selectedIndex]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// 移动端底部导航
|
|
||||||
bottomNavigationBar: !isWide
|
bottomNavigationBar: !isWide
|
||||||
? NavigationBar(
|
? NavigationBar(
|
||||||
selectedIndex: _selectedIndex,
|
selectedIndex: _selectedIndex,
|
||||||
onDestinationSelected: (int index) {
|
onDestinationSelected: (int index) => setState(() => _selectedIndex = index),
|
||||||
setState(() => _selectedIndex = index);
|
|
||||||
},
|
|
||||||
destinations: const [
|
destinations: const [
|
||||||
NavigationDestination(icon: Icon(Icons.explore), label: 'Explore'),
|
NavigationDestination(icon: Icon(Icons.explore), label: 'Explore'),
|
||||||
NavigationDestination(icon: Icon(Icons.videocam), label: 'Stream'),
|
NavigationDestination(icon: Icon(Icons.videocam), label: 'Console'),
|
||||||
NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'),
|
NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -68,8 +59,10 @@ class _HomePageState extends State<HomePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将原本的直播列表逻辑封装为一个内部 View
|
|
||||||
class _ExploreView extends StatefulWidget {
|
class _ExploreView extends StatefulWidget {
|
||||||
|
final VoidCallback onGoLive;
|
||||||
|
const _ExploreView({required this.onGoLive});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ExploreViewState createState() => _ExploreViewState();
|
_ExploreViewState createState() => _ExploreViewState();
|
||||||
}
|
}
|
||||||
@@ -96,7 +89,6 @@ class _ExploreViewState extends State<_ExploreView> {
|
|||||||
|
|
||||||
Future<void> _refreshRooms({bool isAuto = false}) async {
|
Future<void> _refreshRooms({bool isAuto = false}) async {
|
||||||
if (!isAuto && mounted) setState(() => _isLoading = true);
|
if (!isAuto && mounted) setState(() => _isLoading = true);
|
||||||
|
|
||||||
final settings = context.read<SettingsProvider>();
|
final settings = context.read<SettingsProvider>();
|
||||||
final auth = context.read<AuthProvider>();
|
final auth = context.read<AuthProvider>();
|
||||||
final api = ApiService(settings, auth.token);
|
final api = ApiService(settings, auth.token);
|
||||||
@@ -105,14 +97,10 @@ class _ExploreViewState extends State<_ExploreView> {
|
|||||||
final response = await api.getActiveRooms();
|
final response = await api.getActiveRooms();
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
if (mounted) {
|
if (mounted) setState(() => _activeRooms = data['active_rooms'] ?? []);
|
||||||
setState(() => _activeRooms = data['active_rooms'] ?? []);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!isAuto && mounted) {
|
if (!isAuto && mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Failed to load rooms")));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Failed to load rooms")));
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
if (!isAuto && mounted) setState(() => _isLoading = false);
|
if (!isAuto && mounted) setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
@@ -124,16 +112,17 @@ class _ExploreViewState extends State<_ExploreView> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("Hightube Live", style: TextStyle(fontWeight: FontWeight.bold)),
|
title: Text("Explore", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
centerTitle: true,
|
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(icon: Icon(Icons.refresh), onPressed: () => _refreshRooms()),
|
IconButton(icon: Icon(Icons.refresh), onPressed: () => _refreshRooms()),
|
||||||
IconButton(
|
IconButton(icon: Icon(Icons.logout), onPressed: () => context.read<AuthProvider>().logout()),
|
||||||
icon: Icon(Icons.logout),
|
|
||||||
onPressed: () => context.read<AuthProvider>().logout(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: widget.onGoLive,
|
||||||
|
label: Text("Go Live"),
|
||||||
|
icon: Icon(Icons.videocam),
|
||||||
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: _refreshRooms,
|
onRefresh: _refreshRooms,
|
||||||
child: _isLoading && _activeRooms.isEmpty
|
child: _isLoading && _activeRooms.isEmpty
|
||||||
@@ -178,15 +167,7 @@ class _ExploreViewState extends State<_ExploreView> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final rtmpUrl = "${settings.rtmpUrl}/${room['room_id']}";
|
final rtmpUrl = "${settings.rtmpUrl}/${room['room_id']}";
|
||||||
Navigator.push(
|
Navigator.push(context, MaterialPageRoute(builder: (_) => PlayerPage(title: room['title'], rtmpUrl: rtmpUrl)));
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => PlayerPage(
|
|
||||||
title: room['title'],
|
|
||||||
rtmpUrl: rtmpUrl,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../providers/auth_provider.dart';
|
|||||||
import '../providers/settings_provider.dart';
|
import '../providers/settings_provider.dart';
|
||||||
import '../services/api_service.dart';
|
import '../services/api_service.dart';
|
||||||
import 'register_page.dart';
|
import 'register_page.dart';
|
||||||
|
import 'settings_page.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@@ -17,17 +18,18 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
void _handleLogin() async {
|
void _handleLogin() async {
|
||||||
|
if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Please fill in all fields")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
final settings = context.read<SettingsProvider>();
|
final settings = context.read<SettingsProvider>();
|
||||||
final auth = context.read<AuthProvider>();
|
final auth = context.read<AuthProvider>();
|
||||||
final api = ApiService(settings, null);
|
final api = ApiService(settings, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await api.login(
|
final response = await api.login(_usernameController.text, _passwordController.text);
|
||||||
_usernameController.text,
|
|
||||||
_passwordController.text,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
await auth.login(data['token']);
|
await auth.login(data['token']);
|
||||||
@@ -36,29 +38,89 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error)));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error)));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Network Error")));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Network Error: Could not connect to server")));
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isLoading = false);
|
if (mounted) setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text("Login")),
|
appBar: AppBar(
|
||||||
body: Padding(
|
actions: [
|
||||||
padding: const EdgeInsets.all(16.0),
|
IconButton(
|
||||||
child: Column(
|
icon: Icon(Icons.settings),
|
||||||
children: [
|
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsPage())),
|
||||||
TextField(controller: _usernameController, decoration: InputDecoration(labelText: "Username")),
|
),
|
||||||
TextField(controller: _passwordController, decoration: InputDecoration(labelText: "Password"), obscureText: true),
|
],
|
||||||
SizedBox(height: 20),
|
),
|
||||||
_isLoading ? CircularProgressIndicator() : ElevatedButton(onPressed: _handleLogin, child: Text("Login")),
|
body: Center(
|
||||||
TextButton(
|
child: SingleChildScrollView(
|
||||||
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => RegisterPage())),
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||||
child: Text("Don't have an account? Register"),
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 400),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Logo & Name
|
||||||
|
Icon(Icons.flutter_dash, size: 80, color: Theme.of(context).colorScheme.primary),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"HIGHTUBE",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 4,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text("Open Source Live Platform", style: TextStyle(color: Colors.grey)),
|
||||||
|
SizedBox(height: 48),
|
||||||
|
|
||||||
|
// Fields
|
||||||
|
TextField(
|
||||||
|
controller: _usernameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Username",
|
||||||
|
prefixIcon: Icon(Icons.person),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Password",
|
||||||
|
prefixIcon: Icon(Icons.lock),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Login Button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _handleLogin,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
child: _isLoading ? CircularProgressIndicator() : Text("LOGIN", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Register Link
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => RegisterPage())),
|
||||||
|
child: Text("Don't have an account? Create one"),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,18 +15,19 @@ class _RegisterPageState extends State<RegisterPage> {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
void _handleRegister() async {
|
void _handleRegister() async {
|
||||||
|
if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Please fill in all fields")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
final settings = context.read<SettingsProvider>();
|
final settings = context.read<SettingsProvider>();
|
||||||
final api = ApiService(settings, null);
|
final api = ApiService(settings, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await api.register(
|
final response = await api.register(_usernameController.text, _passwordController.text);
|
||||||
_usernameController.text,
|
|
||||||
_passwordController.text,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 201) {
|
if (response.statusCode == 201) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Registered! Please login.")));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Account created! Please login.")));
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
} else {
|
} else {
|
||||||
final error = jsonDecode(response.body)['error'] ?? "Registration Failed";
|
final error = jsonDecode(response.body)['error'] ?? "Registration Failed";
|
||||||
@@ -35,23 +36,67 @@ class _RegisterPageState extends State<RegisterPage> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Network Error")));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Network Error")));
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isLoading = false);
|
if (mounted) setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text("Register")),
|
appBar: AppBar(title: Text("Create Account")),
|
||||||
body: Padding(
|
body: Center(
|
||||||
padding: const EdgeInsets.all(16.0),
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||||
children: [
|
child: ConstrainedBox(
|
||||||
TextField(controller: _usernameController, decoration: InputDecoration(labelText: "Username")),
|
constraints: BoxConstraints(maxWidth: 400),
|
||||||
TextField(controller: _passwordController, decoration: InputDecoration(labelText: "Password"), obscureText: true),
|
child: Column(
|
||||||
SizedBox(height: 20),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
_isLoading ? CircularProgressIndicator() : ElevatedButton(onPressed: _handleRegister, child: Text("Register")),
|
children: [
|
||||||
],
|
Icon(Icons.person_add_outlined, size: 64, color: Theme.of(context).colorScheme.primary),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
"Join Hightube",
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
SizedBox(height: 48),
|
||||||
|
TextField(
|
||||||
|
controller: _usernameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Desired Username",
|
||||||
|
prefixIcon: Icon(Icons.person),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Password",
|
||||||
|
prefixIcon: Icon(Icons.lock),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 32),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _handleRegister,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
child: _isLoading ? CircularProgressIndicator() : Text("REGISTER", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text("Already have an account? Login here"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,38 +8,72 @@ class SettingsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SettingsPageState extends State<SettingsPage> {
|
class _SettingsPageState extends State<SettingsPage> {
|
||||||
final _urlController = TextEditingController();
|
late TextEditingController _urlController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_urlController.text = context.read<SettingsProvider>().baseUrl;
|
_urlController = TextEditingController(text: context.read<SettingsProvider>().baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text("Server Settings")),
|
appBar: AppBar(title: Text("Settings", style: TextStyle(fontWeight: FontWeight.bold))),
|
||||||
body: Padding(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Text(
|
||||||
|
"Network Configuration",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _urlController,
|
controller: _urlController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Backend URL (e.g., http://127.0.0.1:8080)",
|
labelText: "Backend Server URL",
|
||||||
|
hintText: "http://127.0.0.1:8080",
|
||||||
|
prefixIcon: Icon(Icons.lan),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
helperText: "Restarting stream may be required after change",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 24),
|
||||||
ElevatedButton(
|
SizedBox(
|
||||||
onPressed: () {
|
width: double.infinity,
|
||||||
context.read<SettingsProvider>().setBaseUrl(_urlController.text);
|
height: 50,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
child: ElevatedButton.icon(
|
||||||
SnackBar(content: Text("Server URL Updated")),
|
onPressed: () {
|
||||||
);
|
context.read<SettingsProvider>().setBaseUrl(_urlController.text);
|
||||||
},
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
child: Text("Save Settings"),
|
SnackBar(
|
||||||
|
content: Text("Server URL Updated"),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.save),
|
||||||
|
label: Text("Save Configuration"),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 40),
|
||||||
|
Divider(),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
"About Hightube",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
Text("Version: 1.0.0-MVP"),
|
||||||
|
Text("Status: Phase 3.5 (UI Refinement)"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user