From 548c765b90feefcca1c054f12212e9ed5cb6aef2 Mon Sep 17 00:00:00 2001 From: TheGuyDangerous Date: Thu, 10 Oct 2024 20:02:43 +0530 Subject: [PATCH] Added Offline threads functionality --- lib/screens/library/library_screen_state.dart | 198 +++++++++---- lib/screens/thread/thread_loading_screen.dart | 78 +++-- lib/screens/thread/thread_screen.dart | 6 +- lib/screens/thread/thread_screen_state.dart | 107 ++++++- lib/widgets/library/history_list.dart | 269 +++++++++--------- lib/widgets/library/offline_label.dart | 26 ++ 6 files changed, 463 insertions(+), 221 deletions(-) create mode 100644 lib/widgets/library/offline_label.dart diff --git a/lib/screens/library/library_screen_state.dart b/lib/screens/library/library_screen_state.dart index c098cde..62461d8 100644 --- a/lib/screens/library/library_screen_state.dart +++ b/lib/screens/library/library_screen_state.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; +import 'dart:io'; import '../../widgets/library/history_list.dart'; import '../../widgets/library/empty_state.dart'; import '../../widgets/library/incognito_message.dart'; @@ -9,6 +10,8 @@ import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:provider/provider.dart'; import '../../theme_provider.dart'; import 'library_screen.dart'; +import '../thread/thread_screen.dart'; +import '../thread/thread_loading_screen.dart'; const int maxHistoryItems = 50; @@ -29,11 +32,52 @@ class LibraryScreenState extends State { final prefs = await SharedPreferences.getInstance(); _isIncognitoMode = prefs.getBool('incognitoMode') ?? false; if (!_isIncognitoMode) { - final history = prefs.getStringList('search_history') ?? []; + final savedThreads = prefs.getStringList('saved_threads') ?? []; + final searchHistory = prefs.getStringList('search_history') ?? []; + debugPrint('Loaded saved_threads from SharedPreferences: $savedThreads'); + debugPrint( + 'Loaded search_history from SharedPreferences: $searchHistory'); + setState(() { - _searchHistory = history - .map((item) => json.decode(item) as Map) - .toList(); + _searchHistory = [ + ...savedThreads.map((item) { + try { + final data = json.decode(item) as Map; + data['isSaved'] = true; + debugPrint('Loaded saved thread: $data'); + return data; + } catch (e) { + debugPrint('Error decoding saved thread: $e'); + return null; + } + }).whereType>(), + ...searchHistory.map((item) { + try { + final data = json.decode(item) as Map; + data['isSaved'] = false; + debugPrint('Loaded search history item: $data'); + return data; + } catch (e) { + debugPrint('Error decoding search history item: $e'); + return null; + } + }).whereType>(), + ]; + + // Remove duplicates based on query and timestamp + _searchHistory = + _searchHistory.fold>>([], (list, item) { + if (!list.any((element) => + element['query'] == item['query'] && + element['timestamp'] == item['timestamp'])) { + list.add(item); + } + return list; + }); + + // Sort the combined list by timestamp + _searchHistory.sort((a, b) => DateTime.parse(b['timestamp']) + .compareTo(DateTime.parse(a['timestamp']))); }); } else { setState(() { @@ -42,69 +86,18 @@ class LibraryScreenState extends State { } } - @override - Widget build(BuildContext context) { - final themeProvider = Provider.of(context); - - return Scaffold( - appBar: AppBar( - title: Text('Library', - style: - TextStyle(fontFamily: 'Raleway', fontWeight: FontWeight.bold)), - backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white, - foregroundColor: themeProvider.isDarkMode ? Colors.white : Colors.black, - elevation: 0, - ), - body: SmartRefresher( - controller: _refreshController, - onRefresh: _onRefresh, - child: _isIncognitoMode - ? IncognitoMessage() - : _searchHistory.isEmpty - ? EmptyState() - : HistoryList( - searchHistory: _searchHistory, - onDeleteItem: _deleteHistoryItem, - onClearAll: _clearAllHistory, - onItemTap: (query) => - _searchService.performSearch(context, query), - ), - ), - ); - } - - void _deleteHistoryItem(int index) async { + Future _onDeleteItem(int index) async { setState(() { _searchHistory.removeAt(index); }); await _saveSearchHistory(); } - void _clearAllHistory() async { - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Clear All History'), - content: Text('Are you sure you want to clear all search history?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text('Clear'), - ), - ], - ), - ); - - if (confirmed == true) { - setState(() { - _searchHistory.clear(); - }); - await _saveSearchHistory(); - } + Future _onClearAll() async { + setState(() { + _searchHistory.clear(); + }); + await _saveSearchHistory(); } Future _saveSearchHistory() async { @@ -112,8 +105,15 @@ class LibraryScreenState extends State { if (_searchHistory.length > maxHistoryItems) { _searchHistory = _searchHistory.sublist(0, maxHistoryItems); } - final history = _searchHistory.map((item) => json.encode(item)).toList(); - await prefs.setStringList('search_history', history); + final savedThreads = + _searchHistory.where((item) => item['isSaved'] == true).toList(); + final searchHistory = + _searchHistory.where((item) => item['isSaved'] != true).toList(); + + await prefs.setStringList('saved_threads', + savedThreads.map((item) => json.encode(item)).toList()); + await prefs.setStringList('search_history', + searchHistory.map((item) => json.encode(item)).toList()); } void _onRefresh() async { @@ -121,6 +121,78 @@ class LibraryScreenState extends State { _refreshController.refreshCompleted(); } + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, themeProvider, child) { + return Scaffold( + appBar: AppBar( + title: Text('Library'), + ), + body: SmartRefresher( + controller: _refreshController, + onRefresh: _onRefresh, + child: _isIncognitoMode + ? IncognitoMessage() + : _searchHistory.isEmpty + ? EmptyState() + : HistoryList( + searchHistory: _searchHistory, + onDeleteItem: _onDeleteItem, + onClearAll: _onClearAll, + onItemTap: _handleItemTap, + ), + ), + ); + }, + ); + } + + void _handleItemTap(Map item) async { + final savedThreadPath = item['path'] as String?; + debugPrint('Tapped item with savedThreadPath: $savedThreadPath'); + + if (savedThreadPath != null) { + final file = File(savedThreadPath); + if (await file.exists()) { + try { + final jsonString = await file.readAsString(); + final threadData = json.decode(jsonString); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ThreadScreen( + query: threadData['query'], + searchResults: List>.from( + threadData['searchResults']), + summary: threadData['summary'], + savedSections: threadData['sections'] != null + ? List>.from(threadData['sections']) + : null, + ), + ), + ); + } catch (e) { + debugPrint('Error loading saved thread: $e'); + _performNewSearch(item['query']); + } + } else { + debugPrint('Saved thread file does not exist: $savedThreadPath'); + _performNewSearch(item['query']); + } + } else { + debugPrint('No saved thread path found'); + _performNewSearch(item['query']); + } + } + + void _performNewSearch(String query) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ThreadLoadingScreen(query: query), + ), + ); + } + @override void dispose() { _refreshController.dispose(); diff --git a/lib/screens/thread/thread_loading_screen.dart b/lib/screens/thread/thread_loading_screen.dart index 0f2a047..956a4fe 100644 --- a/lib/screens/thread/thread_loading_screen.dart +++ b/lib/screens/thread/thread_loading_screen.dart @@ -1,14 +1,24 @@ import 'package:flutter/material.dart'; +import 'dart:convert'; +import 'dart:io'; import '../../widgets/thread/loading_shimmer.dart'; import '../../services/search_service.dart'; import 'thread_screen.dart'; import '../../theme_provider.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class ThreadLoadingScreen extends StatefulWidget { final String query; + final String? savedThreadPath; + final Map? savedThreadData; - const ThreadLoadingScreen({super.key, required this.query}); + const ThreadLoadingScreen({ + Key? key, + required this.query, + this.savedThreadPath, + this.savedThreadData, + }) : super(key: key); @override State createState() => _ThreadLoadingScreenState(); @@ -21,35 +31,64 @@ class _ThreadLoadingScreenState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - _performSearch(); + _loadThreadOrSearch(); }); } + Future _loadThreadOrSearch() async { + if (widget.savedThreadData != null) { + _navigateToThreadScreen(widget.savedThreadData!); + } else if (widget.savedThreadPath != null) { + await _loadSavedThread(widget.savedThreadPath!); + } else { + await _performSearch(); + } + } + + Future _loadSavedThread(String path) async { + try { + final file = File(path); + if (await file.exists()) { + final jsonString = await file.readAsString(); + final threadData = json.decode(jsonString); + _navigateToThreadScreen(threadData); + } else { + await _performSearch(); + } + } catch (e) { + debugPrint('Error loading saved thread: $e'); + await _performSearch(); + } + } + Future _performSearch() async { final results = await _searchService.performSearch(context, widget.query); if (mounted) { if (results != null) { - Navigator.of(context).pushReplacement( - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => - ThreadScreen( - query: results['query'], - searchResults: results['searchResults'], - summary: results['summary'], - ), - transitionsBuilder: - (context, animation, secondaryAnimation, child) { - return FadeTransition(opacity: animation, child: child); - }, - transitionDuration: Duration(milliseconds: 300), - ), - ); + _navigateToThreadScreen(results); } else { Navigator.of(context).pop(); } } } + void _navigateToThreadScreen(Map data) { + Navigator.of(context).pushReplacement( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => ThreadScreen( + query: data['query'], + searchResults: List>.from(data['searchResults']), + summary: data['summary'], + savedSections: data['sections'] as List?, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + transitionDuration: Duration(milliseconds: 300), + ), + ); + } + @override Widget build(BuildContext context) { final themeProvider = Provider.of(context); @@ -57,7 +96,10 @@ class _ThreadLoadingScreenState extends State { return Scaffold( backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white, appBar: AppBar( - title: Text('Searching...'), + title: Text( + widget.savedThreadPath != null || widget.savedThreadData != null + ? 'Loading Saved Thread' + : 'Searching...'), backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white, foregroundColor: themeProvider.isDarkMode ? Colors.white : Colors.black, ), diff --git a/lib/screens/thread/thread_screen.dart b/lib/screens/thread/thread_screen.dart index 99da5c8..3a2d743 100644 --- a/lib/screens/thread/thread_screen.dart +++ b/lib/screens/thread/thread_screen.dart @@ -5,13 +5,15 @@ class ThreadScreen extends StatefulWidget { final String query; final List> searchResults; final String summary; + final List? savedSections; const ThreadScreen({ - super.key, + Key? key, required this.query, required this.searchResults, required this.summary, - }); + this.savedSections, + }) : super(key: key); @override ThreadScreenState createState() => ThreadScreenState(); diff --git a/lib/screens/thread/thread_screen_state.dart b/lib/screens/thread/thread_screen_state.dart index 65026df..a93cd75 100644 --- a/lib/screens/thread/thread_screen_state.dart +++ b/lib/screens/thread/thread_screen_state.dart @@ -15,13 +15,15 @@ import 'thread_screen.dart'; import 'package:provider/provider.dart'; import '../../theme_provider.dart'; import '../../utils/constants.dart'; +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; class ThreadScreenState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _animation; final TextEditingController _followUpController = TextEditingController(); - final List _threadSections = []; + List _threadSections = []; // Changed from final to non-final bool _isIncognitoMode = false; bool _isSpeaking = false; late FlutterTts _flutterTts; @@ -33,9 +35,13 @@ class ThreadScreenState extends State super.initState(); _initializeAnimation(); _loadIncognitoMode(); - _loadData(); _initializeTts(); - _addInitialSection(); + if (widget.savedSections != null) { + _loadSavedSections(); + } else { + _addInitialSection(); + _loadData(); + } } void _initializeAnimation() { @@ -201,16 +207,77 @@ class ThreadScreenState extends State } } - void _shareSearchResult() { - final String shareText = - 'Query: ${widget.query}\n\nAnswer: ${widget.summary}\n\nSearch with ${AppConstants.appName}: ${AppConstants.githubUrl}'; - Clipboard.setData(ClipboardData(text: shareText)).then((_) { + Future _downloadThread() async { + try { + final threadData = { + 'query': widget.query, + 'summary': widget.summary, + 'searchResults': widget.searchResults, + 'sections': _threadSections + .map((section) => { + 'query': section.query, + 'summary': section.summary, + 'searchResults': section.searchResults, + 'relatedQuestions': section.relatedQuestions, + 'images': section.images, + }) + .toList(), + }; + + final directory = await getApplicationDocumentsDirectory(); + final fileName = 'thread_${DateTime.now().millisecondsSinceEpoch}.json'; + final file = File('${directory.path}/$fileName'); + + await file.writeAsString(json.encode(threadData)); + debugPrint('Thread saved to file: ${file.path}'); + + // Save the file path to SharedPreferences + final prefs = await SharedPreferences.getInstance(); + final savedThreads = prefs.getStringList('saved_threads') ?? []; + final searchHistory = prefs.getStringList('search_history') ?? []; + + // Create the new thread entry + final newThreadEntry = json.encode({ + 'query': widget.query, + 'summary': widget.summary, + 'path': file.path, + 'timestamp': DateTime.now().toIso8601String(), + 'isSaved': true, + }); + + // Remove any existing entries with the same query from both lists + savedThreads.removeWhere((item) { + final decoded = json.decode(item); + return decoded['query'] == widget.query; + }); + searchHistory.removeWhere((item) { + final decoded = json.decode(item); + return decoded['query'] == widget.query; + }); + + // Add the new thread entry to the saved threads list + savedThreads.insert(0, newThreadEntry); + + // Save the updated lists + await prefs.setStringList('saved_threads', savedThreads); + await prefs.setStringList('search_history', searchHistory); + + debugPrint('Updated saved_threads in SharedPreferences: $savedThreads'); + debugPrint('Updated search_history in SharedPreferences: $searchHistory'); + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Search result copied to clipboard')), + SnackBar(content: Text('Thread saved successfully')), ); } - }); + } catch (e) { + debugPrint('Error saving thread: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to save thread')), + ); + } + } } void _addInitialSection() { @@ -251,6 +318,24 @@ class ThreadScreenState extends State } } + void _loadSavedSections() { + _threadSections = widget.savedSections!.map((section) { + return ThreadSection( + query: section['query'] as String, + summary: section['summary'] as String?, + searchResults: (section['searchResults'] as List?) + ?.map((item) => item as Map) + .toList(), + relatedQuestions: (section['relatedQuestions'] as List?) + ?.map((item) => item as String) + .toList(), + images: (section['images'] as List?) + ?.map((item) => Map.from(item)) + .toList(), + ); + }).toList(); + } + @override void dispose() { _animationController.dispose(); @@ -291,8 +376,8 @@ class ThreadScreenState extends State elevation: 0, actions: [ IconButton( - icon: Icon(Iconsax.export), - onPressed: _shareSearchResult, + icon: Icon(Iconsax.document_download), + onPressed: _downloadThread, ), ], ), diff --git a/lib/widgets/library/history_list.dart b/lib/widgets/library/history_list.dart index 804b83e..6a5542e 100644 --- a/lib/widgets/library/history_list.dart +++ b/lib/widgets/library/history_list.dart @@ -3,163 +3,178 @@ import 'package:iconsax/iconsax.dart'; import '../../screens/thread/thread_loading_screen.dart'; import '../../custom_page_route.dart'; import '../../utils/date_formatter.dart'; +import 'offline_label.dart'; -class HistoryList extends StatelessWidget { +class HistoryList extends StatefulWidget { final List> searchHistory; final Function(int) onDeleteItem; final VoidCallback onClearAll; - final Function(String) onItemTap; + final Function(Map) onItemTap; const HistoryList({ - super.key, + Key? key, required this.searchHistory, required this.onDeleteItem, required this.onClearAll, required this.onItemTap, - }); + }) : super(key: key); + + @override + _HistoryListState createState() => _HistoryListState(); +} + +class _HistoryListState extends State { + late List> _searchHistory; + + @override + void initState() { + super.initState(); + _searchHistory = List.from(widget.searchHistory); + } + + @override + void didUpdateWidget(HistoryList oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.searchHistory != oldWidget.searchHistory) { + setState(() { + _searchHistory = List.from(widget.searchHistory); + }); + } + } @override Widget build(BuildContext context) { - return ListView.separated( - itemCount: searchHistory.length + 1, - separatorBuilder: (context, index) { - if (index == 0) return SizedBox.shrink(); - return Divider( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[800] - : Colors.grey[200], - height: 1, - thickness: 0.5, - ); - }, - itemBuilder: (context, index) { - if (index == 0) { - return _buildClearAllButton(context); - } - final item = searchHistory[index - 1]; - return _buildHistoryItem(context, item, index - 1); - }, + return Column( + children: [ + _buildClearAllButton(context), + Expanded( + child: ListView.builder( + itemCount: _searchHistory.length, + itemBuilder: (context, index) { + return Dismissible( + key: ValueKey(_searchHistory[index]['timestamp']), + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20.0), + child: const Icon(Icons.delete, color: Colors.white), + ), + direction: DismissDirection.endToStart, + onDismissed: (direction) { + setState(() { + _searchHistory.removeAt(index); + }); + widget.onDeleteItem(index); + }, + confirmDismiss: (DismissDirection direction) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text("Confirm"), + content: const Text( + "Are you sure you want to delete this item?"), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text("CANCEL"), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text("DELETE"), + ), + ], + ); + }, + ); + }, + child: _buildHistoryItem(context, _searchHistory[index], index), + ); + }, + ), + ), + ], ); } Widget _buildClearAllButton(BuildContext context) { return Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: onClearAll, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[800], - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: widget.onClearAll, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[800], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: EdgeInsets.symmetric(vertical: 12), ), + child: Text('Clear All History'), ), - child: Text('Clear All History'), ), ); } Widget _buildHistoryItem( BuildContext context, Map item, int index) { - return Dismissible( - key: Key(item['timestamp']), - background: Container( - color: Colors.red, - alignment: Alignment.centerRight, - padding: EdgeInsets.only(right: 16), - child: Icon(Iconsax.trash, color: Colors.white), - ), - direction: DismissDirection.endToStart, - onDismissed: (direction) async { - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Delete History Item'), - content: Text('Are you sure you want to delete this history item?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text('Delete'), - ), - ], - ), - ); - - if (confirmed == true) { - onDeleteItem(index); - } - }, - child: GestureDetector( - onTap: () { - Navigator.of(context).push( - CustomPageRoute( - child: ThreadLoadingScreen(query: item['query']), - ), - ); - onItemTap(item['query']); - }, - child: Card( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[900] - : Colors.grey[100], // Make the container transparent - margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item['query'], - style: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 8), - Text( - item['summary'] != null - ? _truncateSummary(item['summary']) - : 'No summary available', - style: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white70 - : Colors.black87, - fontSize: 14, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - formatTimestamp(item['timestamp']), + return GestureDetector( + onTap: () => widget.onItemTap(item), + child: Card( + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + item['query'] ?? 'No query', style: TextStyle( - color: Colors.white70, - fontSize: 12, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + fontWeight: FontWeight.bold, + fontSize: 16, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - IconButton( - icon: - Icon(Iconsax.trash, color: Colors.white70, size: 20), - onPressed: () => onDeleteItem(index), - ), - ], + ), + if (item['isSaved'] == true) const OfflineLabel(), + ], + ), + const SizedBox(height: 8), + Text( + _truncateSummary(item['summary'] ?? 'No summary available'), + style: TextStyle( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white70 + : Colors.black87, + fontSize: 14, ), - ], - ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + formatTimestamp(item['timestamp'] ?? ''), + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + ], + ), + ], ), ), ), diff --git a/lib/widgets/library/offline_label.dart b/lib/widgets/library/offline_label.dart new file mode 100644 index 0000000..04ae592 --- /dev/null +++ b/lib/widgets/library/offline_label.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import ''; + +class OfflineLabel extends StatelessWidget { + const OfflineLabel({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Color.fromRGBO(28, 36, 31, 1), + border: Border.all(color: Color.fromRGBO(70, 92, 30, 1)), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Offline', + style: TextStyle( + color: Color.fromRGBO(168, 221, 32, 1), + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ); + } +}