import 'package:flutter/material.dart'; import 'dart:html' as html; import 'dart:convert'; import 'dart:js_util' as js_util; void main() { runApp(const BrowserTabManagerApp()); } class BrowserTabManagerApp extends StatelessWidget { const BrowserTabManagerApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Browser Tab Manager', theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF0175C2), brightness: Brightness.light, ), useMaterial3: true, ), darkTheme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF0175C2), brightness: Brightness.dark, ), useMaterial3: true, ), home: const TabManagerHome(), ); } } class TabData { String id; String title; String url; String favicon; DateTime lastAccessed; bool isPinned; String type; // 'tab', 'bookmark', 'history' int? visitCount; String? folder; TabData({ required this.id, required this.title, required this.url, this.favicon = '', DateTime? lastAccessed, this.isPinned = false, this.type = 'tab', this.visitCount, this.folder, }) : lastAccessed = lastAccessed ?? DateTime.now(); factory TabData.fromJson(Map json) => TabData( id: json['id'].toString(), title: json['title'] ?? 'Untitled', url: json['url'] ?? '', favicon: json['favicon'] ?? json['favIconUrl'] ?? '', lastAccessed: json['lastAccessed'] != null ? DateTime.parse(json['lastAccessed']) : (json['lastVisitTime'] != null ? DateTime.parse(json['lastVisitTime']) : (json['dateAdded'] != null ? DateTime.parse(json['dateAdded']) : (json['timestamp'] != null ? DateTime.parse(json['timestamp']) : DateTime.now()))), isPinned: json['isPinned'] ?? false, type: json['type'] ?? 'tab', visitCount: json['visitCount'], folder: json['folder'], ); } class TabManagerHome extends StatefulWidget { const TabManagerHome({super.key}); @override State createState() => _TabManagerHomeState(); } class _TabManagerHomeState extends State { List allItems = []; List filteredItems = []; final TextEditingController searchController = TextEditingController(); bool isGridView = true; String sortBy = 'recent'; String filterType = 'all'; // 'all', 'tabs', 'bookmarks', 'history' bool isLoading = true; bool extensionConnected = false; bool extensionMode = false; @override void initState() { super.initState(); _setupExtensionListener(); _loadAllData(); searchController.addListener(_filterItems); } @override void dispose() { searchController.dispose(); super.dispose(); } // Setup extension listener void _setupExtensionListener() { html.window.onMessage.listen((event) { final data = event.data; if (data is Map && data['source'] == 'tab-tracker-extension') { _handleExtensionMessage(Map.from(data)); } }); } // Handle messages from extension void _handleExtensionMessage(Map data) { print('Received from extension: $data'); if (data['action'] == 'updateTabs') { setState(() { final extensionTabs = (data['tabs'] as List).map((tab) { tab['type'] = 'tab'; return TabData.fromJson(tab); }).toList(); allItems = extensionTabs; extensionConnected = true; extensionMode = true; _filterItems(); }); } else if (data['action'] == 'clear') { if (extensionMode) { setState(() { extensionMode = false; _loadAllData(); }); } } else if (data['response'] != null) { final response = data['response']; if (response['status'] == 'started') { setState(() { extensionMode = true; extensionConnected = true; }); } else if (response['status'] == 'stopped') { setState(() { extensionMode = false; _loadAllData(); }); } } } // Send message to extension void _sendToExtension(Map message) { html.window.postMessage({ 'source': 'tab-tracker-webapp', ...message }, '*'); } // Start extension tracking void _startExtensionTracking() { _sendToExtension({'action': 'startTracking'}); setState(() { extensionMode = true; allItems.clear(); }); } // Stop extension tracking void _stopExtensionTracking() { _sendToExtension({'action': 'stopTracking'}); setState(() { extensionMode = false; }); _loadAllData(); } Future _loadAllData() async { if (extensionMode) return; // Don't load if in extension mode setState(() { isLoading = true; }); try { final tabs = await _getTabs(); final bookmarks = await _getBookmarks(); final history = await _getHistory(); setState(() { allItems = [...tabs, ...bookmarks, ...history]; _filterItems(); isLoading = false; }); // Check if extension is available _checkExtensionConnection(); } catch (e) { print('Error loading data: $e'); setState(() { isLoading = false; }); } } void _checkExtensionConnection() { _sendToExtension({'action': 'getStatus'}); Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { setState(() { // extensionConnected will be set by message handler if extension responds }); } }); } Future> _getTabs() async { try { final result = await _callBrowserAPI('getTabs'); if (result != null) { final List data = json.decode(result); return data.map((item) { item['type'] = 'tab'; return TabData.fromJson(item); }).toList(); } } catch (e) { print('Error getting tabs: $e'); } return []; } Future> _getBookmarks() async { try { final result = await _callBrowserAPI('getBookmarks'); if (result != null) { final List data = json.decode(result); return data.map((item) { item['type'] = 'bookmark'; return TabData.fromJson(item); }).toList(); } } catch (e) { print('Error getting bookmarks: $e'); } return []; } Future> _getHistory() async { try { final result = await _callBrowserAPI('getHistory', [100]); if (result != null) { final List data = json.decode(result); return data.map((item) { item['type'] = 'history'; return TabData.fromJson(item); }).toList(); } } catch (e) { print('Error getting history: $e'); } return []; } Future _callBrowserAPI(String method, [List? args]) async { try { // Check if BrowserAPI exists if (!js_util.hasProperty(html.window, 'BrowserAPI')) { print('BrowserAPI not found - running in development mode'); return null; } final browserAPI = js_util.getProperty(html.window, 'BrowserAPI'); final function = js_util.getProperty(browserAPI, method); final result = args == null ? await js_util.promiseToFuture(js_util.callMethod(function, 'call', [browserAPI])) : await js_util.promiseToFuture(js_util.callMethod(function, 'call', [browserAPI, ...args])); return json.encode(result); } catch (e) { print('Error calling $method: $e'); return null; } } void _filterItems() { final query = searchController.text.toLowerCase(); setState(() { filteredItems = allItems.where((item) { // Filter by type if (filterType != 'all' && item.type != filterType.replaceAll('s', '')) { return false; } // Filter by search query if (query.isNotEmpty) { return item.title.toLowerCase().contains(query) || item.url.toLowerCase().contains(query); } return true; }).toList(); _sortItems(); }); } void _sortItems() { switch (sortBy) { case 'recent': filteredItems.sort((a, b) => b.lastAccessed.compareTo(a.lastAccessed)); break; case 'title': filteredItems.sort((a, b) => a.title.compareTo(b.title)); break; case 'url': filteredItems.sort((a, b) => a.url.compareTo(b.url)); break; case 'visits': filteredItems.sort((a, b) => (b.visitCount ?? 0).compareTo(a.visitCount ?? 0)); break; } // Keep pinned tabs at the top filteredItems.sort((a, b) => b.isPinned ? 1 : (a.isPinned ? -1 : 0)); } Future _openItem(TabData item) async { if (extensionMode) { // In extension mode, just open URL in new tab html.window.open(item.url, '_blank'); } else { if (item.type == 'tab') { await _callBrowserAPI('switchToTab', [item.id]); } else { await _callBrowserAPI('openTab', [item.url]); } } } Future _deleteItem(TabData item) async { if (extensionMode) return; // Can't delete in extension mode if (item.type == 'tab') { await _callBrowserAPI('closeTab', [item.id]); } else if (item.type == 'bookmark') { await _callBrowserAPI('removeBookmark', [item.id]); } await _loadAllData(); } Future _togglePin(TabData item) async { if (extensionMode) return; // Can't pin in extension mode if (item.type == 'tab') { await _callBrowserAPI('togglePinTab', [item.id, !item.isPinned]); await _loadAllData(); } } String _getIcon(TabData item) { if (item.favicon.isNotEmpty && !item.favicon.contains('data:')) { return '🌐'; } switch (item.type) { case 'tab': return '📑'; case 'bookmark': return '⭐'; case 'history': return '🕐'; default: return '🌐'; } } @override Widget build(BuildContext context) { final stats = { 'tabs': allItems.where((i) => i.type == 'tab').length, 'bookmarks': allItems.where((i) => i.type == 'bookmark').length, 'history': allItems.where((i) => i.type == 'history').length, }; return Scaffold( appBar: AppBar( title: Row( children: [ const Text('Browser Tab Manager'), if (extensionMode) ...[ const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.green, borderRadius: BorderRadius.circular(12), ), child: const Text( 'TRACKING', style: TextStyle(fontSize: 10, color: Colors.white), ), ), ], ], ), actions: [ // Extension control button if (extensionMode) TextButton.icon( onPressed: _stopExtensionTracking, icon: const Icon(Icons.stop, color: Colors.white), label: const Text('Stop', style: TextStyle(color: Colors.white)), ) else TextButton.icon( onPressed: extensionConnected ? _startExtensionTracking : null, icon: const Icon(Icons.play_arrow, color: Colors.white), label: const Text('Track Tabs', style: TextStyle(color: Colors.white)), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.refresh), onPressed: extensionMode ? null : _loadAllData, tooltip: 'Refresh', ), IconButton( icon: Icon(isGridView ? Icons.view_list : Icons.grid_view), onPressed: () { setState(() { isGridView = !isGridView; }); }, tooltip: isGridView ? 'List View' : 'Grid View', ), PopupMenuButton( icon: const Icon(Icons.sort), tooltip: 'Sort by', onSelected: (value) { setState(() { sortBy = value; _filterItems(); }); }, itemBuilder: (context) => [ const PopupMenuItem(value: 'recent', child: Text('Recent')), const PopupMenuItem(value: 'title', child: Text('Title')), const PopupMenuItem(value: 'url', child: Text('URL')), const PopupMenuItem(value: 'visits', child: Text('Most Visited')), ], ), ], ), body: Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: searchController, decoration: InputDecoration( hintText: extensionMode ? 'Search tracked tabs...' : 'Search tabs, bookmarks, and history...', prefixIcon: const Icon(Icons.search), suffixIcon: searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { searchController.clear(); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), ), const SizedBox(height: 12), if (!extensionMode) SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ FilterChip( label: Text('All (${allItems.length})'), selected: filterType == 'all', onSelected: (selected) { setState(() { filterType = 'all'; _filterItems(); }); }, ), const SizedBox(width: 8), FilterChip( label: Text('Tabs (${stats['tabs']})'), selected: filterType == 'tabs', onSelected: (selected) { setState(() { filterType = 'tabs'; _filterItems(); }); }, ), const SizedBox(width: 8), FilterChip( label: Text('Bookmarks (${stats['bookmarks']})'), selected: filterType == 'bookmarks', onSelected: (selected) { setState(() { filterType = 'bookmarks'; _filterItems(); }); }, ), const SizedBox(width: 8), FilterChip( label: Text('History (${stats['history']})'), selected: filterType == 'history', onSelected: (selected) { setState(() { filterType = 'history'; _filterItems(); }); }, ), ], ), ), ], ), ), Expanded( child: isLoading ? const Center(child: CircularProgressIndicator()) : filteredItems.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( extensionMode ? Icons.track_changes : Icons.search_off, size: 80, color: Theme.of(context) .colorScheme .primary .withOpacity(0.3), ), const SizedBox(height: 16), Text( extensionMode ? 'Waiting for tabs...' : 'No items found', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( extensionMode ? 'Open some tabs to see them here' : 'Try a different search or filter', style: Theme.of(context).textTheme.bodyMedium, ), ], ), ) : isGridView ? GridView.builder( padding: const EdgeInsets.all(16), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 300, childAspectRatio: 1.3, crossAxisSpacing: 16, mainAxisSpacing: 16, ), itemCount: filteredItems.length, itemBuilder: (context, index) { return ItemCard( item: filteredItems[index], icon: _getIcon(filteredItems[index]), onTap: () => _openItem(filteredItems[index]), onDelete: () => _deleteItem(filteredItems[index]), onTogglePin: () => _togglePin(filteredItems[index]), extensionMode: extensionMode, ); }, ) : ListView.builder( padding: const EdgeInsets.all(16), itemCount: filteredItems.length, itemBuilder: (context, index) { return ItemListTile( item: filteredItems[index], icon: _getIcon(filteredItems[index]), onTap: () => _openItem(filteredItems[index]), onDelete: () => _deleteItem(filteredItems[index]), onTogglePin: () => _togglePin(filteredItems[index]), extensionMode: extensionMode, ); }, ), ), ], ), ); } } class ItemCard extends StatelessWidget { final TabData item; final String icon; final VoidCallback onTap; final VoidCallback onDelete; final VoidCallback onTogglePin; final bool extensionMode; const ItemCard({ super.key, required this.item, required this.icon, required this.onTap, required this.onDelete, required this.onTogglePin, this.extensionMode = false, }); @override Widget build(BuildContext context) { return Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 60, color: Theme.of(context).colorScheme.primaryContainer, child: Center( child: item.favicon.isNotEmpty && item.favicon.startsWith('http') ? Image.network( item.favicon, width: 32, height: 32, errorBuilder: (context, error, stackTrace) { return Text(icon, style: const TextStyle(fontSize: 32)); }, ) : Text(icon, style: const TextStyle(fontSize: 32)), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (item.isPinned) Icon( Icons.push_pin, size: 14, color: Theme.of(context).colorScheme.primary, ), if (item.isPinned) const SizedBox(width: 4), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: _getTypeColor(context), borderRadius: BorderRadius.circular(4), ), child: Text( item.type.toUpperCase(), style: TextStyle( fontSize: 10, color: Theme.of(context).colorScheme.onPrimary, ), ), ), ], ), const SizedBox(height: 6), Expanded( child: Text( item.title, style: Theme.of(context).textTheme.titleSmall, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), const SizedBox(height: 4), Text( item.url, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.secondary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), if (item.visitCount != null) ...[ const SizedBox(height: 4), Text( '${item.visitCount} visits', style: Theme.of(context).textTheme.bodySmall, ), ], ], ), ), ), if (!extensionMode) ButtonBar( alignment: MainAxisAlignment.end, buttonPadding: EdgeInsets.zero, children: [ if (item.type == 'tab') IconButton( icon: Icon( item.isPinned ? Icons.push_pin : Icons.push_pin_outlined, size: 18, ), onPressed: onTogglePin, tooltip: item.isPinned ? 'Unpin' : 'Pin', ), if (item.type != 'history') IconButton( icon: const Icon(Icons.delete_outline, size: 18), onPressed: onDelete, tooltip: 'Delete', ), ], ), ], ), ), ); } Color _getTypeColor(BuildContext context) { switch (item.type) { case 'tab': return Colors.blue; case 'bookmark': return Colors.orange; case 'history': return Colors.purple; default: return Theme.of(context).colorScheme.primary; } } } class ItemListTile extends StatelessWidget { final TabData item; final String icon; final VoidCallback onTap; final VoidCallback onDelete; final VoidCallback onTogglePin; final bool extensionMode; const ItemListTile({ super.key, required this.item, required this.icon, required this.onTap, required this.onDelete, required this.onTogglePin, this.extensionMode = false, }); @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: CircleAvatar( child: item.favicon.isNotEmpty && item.favicon.startsWith('http') ? Image.network( item.favicon, width: 20, height: 20, errorBuilder: (context, error, stackTrace) { return Text(icon, style: const TextStyle(fontSize: 20)); }, ) : Text(icon, style: const TextStyle(fontSize: 20)), ), title: Row( children: [ if (item.isPinned) ...[ Icon( Icons.push_pin, size: 14, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 4), ], Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _getTypeColor(context), borderRadius: BorderRadius.circular(4), ), child: Text( item.type.toUpperCase(), style: TextStyle( fontSize: 10, color: Theme.of(context).colorScheme.onPrimary, ), ), ), const SizedBox(width: 8), Expanded( child: Text( item.title, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.url, maxLines: 1, overflow: TextOverflow.ellipsis, ), if (item.visitCount != null) Text('${item.visitCount} visits', style: Theme.of(context).textTheme.bodySmall), ], ), trailing: extensionMode ? null : Row( mainAxisSize: MainAxisSize.min, children: [ if (item.type == 'tab') IconButton( icon: Icon( item.isPinned ? Icons.push_pin : Icons.push_pin_outlined, ), onPressed: onTogglePin, tooltip: item.isPinned ? 'Unpin' : 'Pin', ), if (item.type != 'history') IconButton( icon: const Icon(Icons.delete_outline), onPressed: onDelete, tooltip: 'Delete', ), ], ), onTap: onTap, ), ); } Color _getTypeColor(BuildContext context) { switch (item.type) { case 'tab': return Colors.blue; case 'bookmark': return Colors.orange; case 'history': return Colors.purple; default: return Theme.of(context).colorScheme.primary; } } }