diff --git a/backend/internal/api/auth.go b/backend/internal/api/auth.go index 5eabc06..9fec6b8 100644 --- a/backend/internal/api/auth.go +++ b/backend/internal/api/auth.go @@ -20,6 +20,11 @@ type LoginRequest struct { Password string `json:"password" binding:"required"` } +type ChangePasswordRequest struct { + OldPassword string `json:"old_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required"` +} + func Register(c *gin.Context) { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -95,3 +100,40 @@ func Login(c *gin.Context) { "username": user.Username, }) } + +func ChangePassword(c *gin.Context) { + userID, _ := c.Get("user_id") + + var req ChangePasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var user model.User + if err := db.DB.First(&user, userID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + // Verify old password + if !utils.CheckPasswordHash(req.OldPassword, user.Password) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid old password"}) + return + } + + // Hash new password + hashedPassword, err := utils.HashPassword(req.NewPassword) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) + return + } + + // Update user + if err := db.DB.Model(&user).Update("password", hashedPassword).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update password"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"}) +} diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go index 2bb1273..5039e10 100644 --- a/backend/internal/api/router.go +++ b/backend/internal/api/router.go @@ -27,6 +27,7 @@ func SetupRouter() *gin.Engine { authGroup.Use(AuthMiddleware()) { authGroup.GET("/room/my", GetMyRoom) + authGroup.POST("/user/change-password", ChangePassword) } return r diff --git a/backend/internal/chat/hub.go b/backend/internal/chat/hub.go index 3d4335b..e99d1b7 100644 --- a/backend/internal/chat/hub.go +++ b/backend/internal/chat/hub.go @@ -16,10 +16,11 @@ const ( ) type Message struct { - Type string `json:"type"` // "chat", "system", "danmaku" - Username string `json:"username"` - Content string `json:"content"` - RoomID string `json:"room_id"` + Type string `json:"type"` // "chat", "system", "danmaku" + Username string `json:"username"` + Content string `json:"content"` + RoomID string `json:"room_id"` + IsHistory bool `json:"is_history"` } type Client struct { @@ -31,19 +32,21 @@ type Client struct { } type Hub struct { - rooms map[string]map[*Client]bool - broadcast chan Message - register chan *Client - unregister chan *Client - mutex sync.RWMutex + rooms map[string]map[*Client]bool + roomsHistory map[string][]Message + broadcast chan Message + register chan *Client + unregister chan *Client + mutex sync.RWMutex } func NewHub() *Hub { return &Hub{ - broadcast: make(chan Message), - register: make(chan *Client), - unregister: make(chan *Client), - rooms: make(map[string]map[*Client]bool), + broadcast: make(chan Message), + register: make(chan *Client), + unregister: make(chan *Client), + rooms: make(map[string]map[*Client]bool), + roomsHistory: make(map[string][]Message), } } @@ -56,6 +59,20 @@ func (h *Hub) Run() { h.rooms[client.RoomID] = make(map[*Client]bool) } h.rooms[client.RoomID][client] = true + + // Send existing history to the newly joined client + if history, ok := h.roomsHistory[client.RoomID]; ok { + for _, msg := range history { + msg.IsHistory = true + msgBytes, _ := json.Marshal(msg) + // Use select to avoid blocking if client's send channel is full + select { + case client.Send <- msgBytes: + default: + // If send fails, we could potentially log or ignore + } + } + } h.mutex.Unlock() case client := <-h.unregister: @@ -64,6 +81,10 @@ func (h *Hub) Run() { if _, ok := rooms[client]; ok { delete(rooms, client) close(client.Send) + // We no longer delete the room from h.rooms here if we want history to persist + // even if everyone leaves (as long as it's active in DB). + // But we should clean up if the room is empty and we want to save memory. + // However, the history is what matters. if len(rooms) == 0 { delete(h.rooms, client.RoomID) } @@ -72,7 +93,16 @@ func (h *Hub) Run() { h.mutex.Unlock() case message := <-h.broadcast: - h.mutex.RLock() + h.mutex.Lock() + // Only store "chat" and "danmaku" messages in history + if message.Type == "chat" || message.Type == "danmaku" { + h.roomsHistory[message.RoomID] = append(h.roomsHistory[message.RoomID], message) + // Limit history size to avoid memory leak (e.g., last 100 messages) + if len(h.roomsHistory[message.RoomID]) > 100 { + h.roomsHistory[message.RoomID] = h.roomsHistory[message.RoomID][1:] + } + } + clients := h.rooms[message.RoomID] if clients != nil { msgBytes, _ := json.Marshal(message) @@ -85,11 +115,18 @@ func (h *Hub) Run() { } } } - h.mutex.RUnlock() + h.mutex.Unlock() } } } +// ClearRoomHistory removes history for a room, should be called when stream ends +func (h *Hub) ClearRoomHistory(roomID string) { + h.mutex.Lock() + defer h.mutex.Unlock() + delete(h.roomsHistory, roomID) +} + func (h *Hub) RegisterClient(c *Client) { h.register <- c } diff --git a/backend/internal/stream/server.go b/backend/internal/stream/server.go index e594036..7fa7a3f 100644 --- a/backend/internal/stream/server.go +++ b/backend/internal/stream/server.go @@ -11,6 +11,7 @@ import ( "github.com/nareix/joy4/format" "github.com/nareix/joy4/format/rtmp" + "hightube/internal/chat" "hightube/internal/db" "hightube/internal/model" ) @@ -83,6 +84,10 @@ func NewRTMPServer() *RTMPServer { q.Close() // Explicitly set is_active to false using map db.DB.Model(&room).Updates(map[string]interface{}{"is_active": false}) + + // Clear chat history for this room + chat.MainHub.ClearRoomHistory(fmt.Sprintf("%d", room.ID)) + fmt.Printf("[INFO] Publishing ended for Room ID: %d\n", room.ID) }() diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml index c561285..0a3bb1e 100644 --- a/frontend/android/app/src/main/AndroidManifest.xml +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:icon="@mipmap/launcher_icon"> (); + final settings = context.watch(); return MaterialApp( title: 'Hightube', - // 设置深色主题为主,更符合视频类应用的审美 theme: ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( - seedColor: Colors.deepPurple, + seedColor: settings.themeColor, brightness: Brightness.light, ), ), darkTheme: ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( - seedColor: Colors.deepPurple, + seedColor: settings.themeColor, brightness: Brightness.dark, ), ), diff --git a/frontend/lib/pages/player_page.dart b/frontend/lib/pages/player_page.dart index e8c3f5b..91c7cec 100644 --- a/frontend/lib/pages/player_page.dart +++ b/frontend/lib/pages/player_page.dart @@ -62,7 +62,7 @@ class _PlayerPageState extends State { if (mounted) { setState(() { _messages.insert(0, msg); - if (msg.type == "chat" || msg.type == "danmaku") { + if (!msg.isHistory && (msg.type == "chat" || msg.type == "danmaku")) { _addDanmaku(msg.content); } }); @@ -71,15 +71,15 @@ class _PlayerPageState extends State { } void _addDanmaku(String text) { - final id = DateTime.now().millisecondsSinceEpoch; - final top = 20.0 + (id % 6) * 30.0; + final key = UniqueKey(); + final top = 20.0 + (DateTime.now().millisecondsSinceEpoch % 6) * 30.0; final danmaku = _DanmakuItem( - key: ValueKey(id), + key: key, text: text, top: top, onFinished: () { - if (mounted) setState(() => _danmakus.removeWhere((w) => w.key == ValueKey(id))); + if (mounted) setState(() => _danmakus.removeWhere((w) => w.key == key)); }, ); diff --git a/frontend/lib/pages/settings_page.dart b/frontend/lib/pages/settings_page.dart index 1ff9c62..66662e2 100644 --- a/frontend/lib/pages/settings_page.dart +++ b/frontend/lib/pages/settings_page.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/settings_provider.dart'; +import '../providers/auth_provider.dart'; +import '../services/api_service.dart'; class SettingsPage extends StatefulWidget { @override @@ -9,6 +12,18 @@ class SettingsPage extends StatefulWidget { class _SettingsPageState extends State { late TextEditingController _urlController; + final TextEditingController _oldPasswordController = TextEditingController(); + final TextEditingController _newPasswordController = TextEditingController(); + + final List _availableColors = [ + Colors.blue, + Colors.deepPurple, + Colors.red, + Colors.green, + Colors.orange, + Colors.teal, + Colors.pink, + ]; @override void initState() { @@ -16,22 +31,60 @@ class _SettingsPageState extends State { _urlController = TextEditingController(text: context.read().baseUrl); } + @override + void dispose() { + _urlController.dispose(); + _oldPasswordController.dispose(); + _newPasswordController.dispose(); + super.dispose(); + } + + void _handleChangePassword() async { + final settings = context.read(); + final auth = context.read(); + final api = ApiService(settings, auth.token); + + if (_oldPasswordController.text.isEmpty || _newPasswordController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Please fill in both password fields"))); + return; + } + + try { + final resp = await api.changePassword(_oldPasswordController.text, _newPasswordController.text); + final data = jsonDecode(resp.body); + if (resp.statusCode == 200) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Password updated successfully"))); + _oldPasswordController.clear(); + _newPasswordController.clear(); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Error: ${data['error']}"))); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Failed to connect to server"))); + } + } + @override Widget build(BuildContext context) { + final auth = context.watch(); + final settings = context.watch(); + return Scaffold( - appBar: AppBar(title: Text("Settings", style: TextStyle(fontWeight: FontWeight.bold))), + appBar: AppBar( + title: Text("Settings", style: TextStyle(fontWeight: FontWeight.bold)), + centerTitle: true, + ), body: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Network Configuration", - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), + // User Profile Section + _buildProfileSection(auth), + SizedBox(height: 32), + + // Network Configuration + _buildSectionTitle("Network Configuration"), SizedBox(height: 16), TextField( controller: _urlController, @@ -40,43 +93,172 @@ class _SettingsPageState extends State { 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: 24), + SizedBox(height: 12), SizedBox( width: double.infinity, - height: 50, child: ElevatedButton.icon( onPressed: () { context.read().setBaseUrl(_urlController.text); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Server URL Updated"), - behavior: SnackBarBehavior.floating, - ), - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Server URL Updated"), behavior: SnackBarBehavior.floating)); }, icon: Icon(Icons.save), - label: Text("Save Configuration"), + label: Text("Save Network Settings"), style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + ), + SizedBox(height: 32), + + // Theme Color Section + _buildSectionTitle("Theme Customization"), + SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + children: _availableColors.map((color) { + bool isSelected = settings.themeColor.value == color.value; + return GestureDetector( + onTap: () => settings.setThemeColor(color), + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: isSelected ? Border.all(color: Theme.of(context).colorScheme.onSurface, width: 3) : null, + boxShadow: [ + BoxShadow(color: Colors.black26, blurRadius: 4, offset: Offset(0, 2)), + ], + ), + child: isSelected ? Icon(Icons.check, color: Colors.white) : null, + ), + ); + }).toList(), + ), + SizedBox(height: 32), + + // Security Section + _buildSectionTitle("Security"), + SizedBox(height: 16), + TextField( + controller: _oldPasswordController, + obscureText: true, + decoration: InputDecoration( + labelText: "Old Password", + prefixIcon: Icon(Icons.lock_outline), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + SizedBox(height: 12), + TextField( + controller: _newPasswordController, + obscureText: true, + decoration: InputDecoration( + labelText: "New Password", + prefixIcon: Icon(Icons.lock_reset), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _handleChangePassword, + icon: Icon(Icons.update), + label: Text("Change Password"), + style: OutlinedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), SizedBox(height: 40), + + // About Section Divider(), SizedBox(height: 20), - Text( - "About Hightube", - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey), + Center( + child: Column( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: settings.themeColor, + borderRadius: BorderRadius.circular(12), + ), + child: Center(child: Text("H", style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold))), + ), + SizedBox(height: 12), + Text("Hightube", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + Text("Version: 1.0.0-beta3.5", style: TextStyle(color: Colors.grey)), + Text("Author: Highground-Soft & Minimax", style: TextStyle(color: Colors.grey)), + SizedBox(height: 20), + Text("© 2026 Hightube Project", style: TextStyle(fontSize: 12, color: Colors.grey)), + ], + ), ), - SizedBox(height: 10), - Text("Version: 1.0.0-MVP"), - Text("Status: Phase 3.5 (UI Refinement)"), + SizedBox(height: 40), ], ), ), ); } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildProfileSection(AuthProvider auth) { + return Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + CircleAvatar( + radius: 35, + backgroundColor: Theme.of(context).colorScheme.primary, + child: Text( + (auth.username ?? "U")[0].toUpperCase(), + style: TextStyle(fontSize: 32, color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + auth.username ?? "Unknown User", + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + ), + Text( + "Self-hosted Streamer", + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ], + ), + ), + IconButton( + onPressed: () => auth.logout(), + icon: Icon(Icons.logout, color: Colors.redAccent), + tooltip: "Logout", + ), + ], + ), + ); + } } diff --git a/frontend/lib/providers/settings_provider.dart b/frontend/lib/providers/settings_provider.dart index 9a3c01d..7ad4cd5 100644 --- a/frontend/lib/providers/settings_provider.dart +++ b/frontend/lib/providers/settings_provider.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; - class SettingsProvider with ChangeNotifier { // Default server address for local development. // Using 10.0.2.2 for Android emulator or localhost for Desktop. String _baseUrl = "http://localhost:8080"; - + Color _themeColor = Colors.blue; + String get baseUrl => _baseUrl; + Color get themeColor => _themeColor; SettingsProvider() { _loadSettings(); @@ -15,6 +16,10 @@ class SettingsProvider with ChangeNotifier { void _loadSettings() async { final prefs = await SharedPreferences.getInstance(); _baseUrl = prefs.getString('baseUrl') ?? _baseUrl; + final colorValue = prefs.getInt('themeColor'); + if (colorValue != null) { + _themeColor = Color(colorValue); + } notifyListeners(); } @@ -24,7 +29,14 @@ class SettingsProvider with ChangeNotifier { await prefs.setString('baseUrl', url); notifyListeners(); } - + + void setThemeColor(Color color) async { + _themeColor = color; + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('themeColor', color.value); + notifyListeners(); + } + // Also provide the RTMP URL based on the same hostname String get rtmpUrl { final uri = Uri.parse(_baseUrl); diff --git a/frontend/lib/services/api_service.dart b/frontend/lib/services/api_service.dart index 2dbd5b4..640a80d 100644 --- a/frontend/lib/services/api_service.dart +++ b/frontend/lib/services/api_service.dart @@ -42,4 +42,12 @@ class ApiService { headers: _headers, ); } + + Future changePassword(String oldPassword, String newPassword) async { + return await http.post( + Uri.parse("${settings.baseUrl}/api/user/change-password"), + headers: _headers, + body: jsonEncode({"old_password": oldPassword, "new_password": newPassword}), + ); + } } diff --git a/frontend/lib/services/chat_service.dart b/frontend/lib/services/chat_service.dart index 320a6e9..5bcd837 100644 --- a/frontend/lib/services/chat_service.dart +++ b/frontend/lib/services/chat_service.dart @@ -7,8 +7,15 @@ class ChatMessage { final String username; final String content; final String roomId; + final bool isHistory; - ChatMessage({required this.type, required this.username, required this.content, required this.roomId}); + ChatMessage({ + required this.type, + required this.username, + required this.content, + required this.roomId, + this.isHistory = false, + }); factory ChatMessage.fromJson(Map json) { return ChatMessage( @@ -16,6 +23,7 @@ class ChatMessage { username: json['username'] ?? 'Anonymous', content: json['content'] ?? '', roomId: json['room_id'] ?? '', + isHistory: json['is_history'] ?? false, ); } @@ -24,6 +32,7 @@ class ChatMessage { 'username': username, 'content': content, 'room_id': roomId, + 'is_history': isHistory, }; } diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index ff4ed74..0517524 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -25,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -102,6 +134,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -168,6 +208,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -312,6 +368,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -328,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" provider: dependency: "direct main" description: @@ -549,6 +621,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index cde0b8a..9f7294d 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: 1.0.0-beta3.5 environment: sdk: ^3.11.1 @@ -44,6 +44,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + flutter_launcher_icons: ^0.13.1 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is @@ -52,6 +53,21 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^6.0.0 +flutter_launcher_icons: + android: "launcher_icon" + ios: false + image_path: "assets/icon/app_icon.png" + min_sdk_android: 21 + web: + generate: true + image_path: "assets/icon/app_icon.png" + background_color: "#ffffff" + theme_color: "#2196F3" + windows: + generate: true + image_path: "assets/icon/app_icon.png" + icon_size: 256 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart index 00e8ab7..ef12de0 100644 --- a/frontend/test/widget_test.dart +++ b/frontend/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:hightube/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(HightubeApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/frontend/windows/runner/resources/app_icon.ico b/frontend/windows/runner/resources/app_icon.ico index c04e20c..c9180fe 100644 Binary files a/frontend/windows/runner/resources/app_icon.ico and b/frontend/windows/runner/resources/app_icon.ico differ