Browser-Tab-Manager/_backup/main.dart

1207 lines
39 KiB
Dart
Raw Normal View History

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());
// ☝️ STATEMENT: Command that starts the app
}
class BrowserTabManagerApp extends StatelessWidget {
// This is a STATELESS widget - no changing data
const BrowserTabManagerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// STATEMENT: Return/create a MaterialApp widget
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 {
// This is a DATA MODEL - holds information about a tab
String id; // STATEMENT: Declare a variable
String title; // STATEMENT: Declare a variable
String url; // STATEMENT: Declare a variable
String favicon;
DateTime lastAccessed;
bool isPinned;
String type;
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();
// ☝️ STATEMENT: Constructor that creates TabData objects
factory TabData.fromJson(Map<String, dynamic> json) => TabData(
// ☝️ STATEMENT: Factory method to create TabData from JSON
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 {
// This is a STATEFUL widget - has changing data (STATE)
const TabManagerHome({super.key});
@override
State<TabManagerHome> createState() => _TabManagerHomeState();
// STATEMENT: Create the state object
}
class _TabManagerHomeState extends State<TabManagerHome> {
// This class holds all the STATE (changing data)
List<TabData> allItems = [];
// 🔴 STATE: List of all tabs/bookmarks/history
// When this changes, UI rebuilds to show new data
List<TabData> filteredItems = [];
// 🔴 STATE: Filtered/searched items currently displayed
final TextEditingController searchController = TextEditingController();
// 🔴 STATE: Controls search text field
bool isGridView = true;
// 🔴 STATE: Whether showing grid or list view
String sortBy = 'recent';
// 🔴 STATE: Current sort method
String filterType = 'all';
// 🔴 STATE: Current filter (all, tabs, bookmarks, history)
bool isLoading = true;
// 🔴 STATE: Whether data is loading
bool extensionConnected = false;
// 🔴 STATE: Whether browser extension is connected
bool extensionMode = false;
// 🔴 STATE: Whether in extension tracking mode
@override
void initState() {
// STATEMENT: Method that runs when widget is created
super.initState();
// STATEMENT: Call parent's initState
_setupExtensionListener();
// STATEMENT: Call function to setup listener
_loadAllData();
// STATEMENT: Call function to load data
searchController.addListener(_filterItems);
// STATEMENT: Add listener to search field
}
@override
void dispose() {
// STATEMENT: Method that runs when widget is destroyed
searchController.dispose();
// STATEMENT: Clean up resources
super.dispose();
// STATEMENT: Call parent's dispose
}
void _setupExtensionListener() {
// STATEMENT: Function declaration
html.window.onMessage.listen((event) {
// STATEMENT: Start listening for messages
final data = event.data;
// STATEMENT: Get data from event
if (data is Map && data['source'] == 'tab-tracker-extension') {
// STATEMENT: Conditional check
_handleExtensionMessage(Map<String, dynamic>.from(data));
// STATEMENT: Call function with data
}
});
}
void _handleExtensionMessage(Map<String, dynamic> data) {
print('Received from extension: $data');
// STATEMENT: Print to console
if (data['action'] == 'updateTabs') {
// STATEMENT: Check if action is updateTabs
setState(() {
// 🔥 SPECIAL STATEMENT: Updates STATE and rebuilds UI
// Everything inside this block changes STATE
final extensionTabs = (data['tabs'] as List).map((tab) {
tab['type'] = 'tab';
return TabData.fromJson(tab);
}).toList();
// STATEMENT: Transform data
allItems = extensionTabs;
// 🔴 STATE CHANGE: Update the list
extensionConnected = true;
// 🔴 STATE CHANGE: Mark extension as connected
extensionMode = true;
// 🔴 STATE CHANGE: Enable extension mode
_filterItems();
// STATEMENT: Call filter function
});
// After setState finishes, Flutter rebuilds the UI automatically
} else if (data['action'] == 'clear') {
// STATEMENT: Check another condition
if (extensionMode) {
setState(() {
extensionMode = false;
// 🔴 STATE CHANGE: Disable extension mode
_loadAllData();
// STATEMENT: Reload data
});
}
} 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();
});
}
}
}
void _sendToExtension(Map<String, dynamic> message) {
// STATEMENT: Function to send message
html.window.postMessage({
'source': 'tab-tracker-webapp',
...message
}, '*');
// STATEMENT: Post message to window
}
void _startExtensionTracking() {
_sendToExtension({'action': 'startTracking'});
// STATEMENT: Send message to extension
setState(() {
// 🔥 UPDATE STATE
extensionMode = true;
// 🔴 STATE CHANGE
allItems.clear();
// 🔴 STATE CHANGE: Clear the list
});
}
void _stopExtensionTracking() {
_sendToExtension({'action': 'stopTracking'});
// STATEMENT: Send stop message
setState(() {
extensionMode = false;
// 🔴 STATE CHANGE
});
_loadAllData();
// STATEMENT: Reload data
}
Future<void> _loadAllData() async {
// STATEMENT: Async function declaration
if (extensionMode) return;
// STATEMENT: Early return if in extension mode
setState(() {
isLoading = true;
// 🔴 STATE CHANGE: Show loading spinner
});
try {
// STATEMENT: Try block for error handling
final tabs = await _getTabs();
// STATEMENT: Wait for tabs to load
final bookmarks = await _getBookmarks();
// STATEMENT: Wait for bookmarks to load
final history = await _getHistory();
// STATEMENT: Wait for history to load
setState(() {
// 🔥 UPDATE STATE
allItems = [...tabs, ...bookmarks, ...history];
// 🔴 STATE CHANGE: Combine all items
_filterItems();
// STATEMENT: Filter items
isLoading = false;
// 🔴 STATE CHANGE: Hide loading spinner
});
_checkExtensionConnection();
// STATEMENT: Check extension
} catch (e) {
// STATEMENT: Catch errors
print('Error loading data: $e');
// STATEMENT: Print error
setState(() {
isLoading = false;
// 🔴 STATE CHANGE: Hide loading spinner
});
}
}
void _checkExtensionConnection() {
_sendToExtension({'action': 'getStatus'});
// STATEMENT: Request status
Future.delayed(const Duration(milliseconds: 500), () {
// STATEMENT: Wait 500ms
if (mounted) {
// STATEMENT: Check if widget still exists
setState(() {
// 🔥 UPDATE STATE (even if empty, triggers rebuild)
});
}
});
}
Future<List<TabData>> _getTabs() async {
// STATEMENT: Function that returns Future
try {
final result = await _callBrowserAPI('getTabs');
// STATEMENT: Call browser API
if (result != null) {
// STATEMENT: Check if got result
final List<dynamic> data = json.decode(result);
// STATEMENT: Parse JSON
return data.map((item) {
// STATEMENT: Transform each item
item['type'] = 'tab';
// STATEMENT: Set type
return TabData.fromJson(item);
// STATEMENT: Create TabData object
}).toList();
}
} catch (e) {
print('Error getting tabs: $e');
// STATEMENT: Print error
}
return [];
// STATEMENT: Return empty list
}
Future<List<TabData>> _getBookmarks() async {
try {
final result = await _callBrowserAPI('getBookmarks');
if (result != null) {
final List<dynamic> 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<List<TabData>> _getHistory() async {
try {
final result = await _callBrowserAPI('getHistory', [100]);
if (result != null) {
final List<dynamic> 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<String?> _callBrowserAPI(String method, [List<dynamic>? args]) async {
try {
if (!js_util.hasProperty(html.window, 'BrowserAPI')) {
// STATEMENT: Check if API exists
print('BrowserAPI not found - running in development mode');
return null;
// STATEMENT: Return null
}
final browserAPI = js_util.getProperty(html.window, 'BrowserAPI');
// STATEMENT: Get API object
final function = js_util.getProperty(browserAPI, method);
// STATEMENT: Get function
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]));
// STATEMENT: Call function and wait for result
return json.encode(result);
// STATEMENT: Convert to JSON string
} catch (e) {
print('Error calling $method: $e');
return null;
}
}
void _filterItems() {
final query = searchController.text.toLowerCase();
// STATEMENT: Get search text
setState(() {
// 🔥 UPDATE STATE
filteredItems = allItems.where((item) {
// STATEMENT: Filter list
if (filterType != 'all' && item.type != filterType.replaceAll('s', '')) {
return false;
// STATEMENT: Exclude item
}
if (query.isNotEmpty) {
return item.title.toLowerCase().contains(query) ||
item.url.toLowerCase().contains(query);
// STATEMENT: Check if matches search
}
return true;
// STATEMENT: Include item
}).toList();
_sortItems();
// STATEMENT: Sort the filtered items
});
}
void _sortItems() {
switch (sortBy) {
// STATEMENT: Switch based on sortBy value
case 'recent':
filteredItems.sort((a, b) => b.lastAccessed.compareTo(a.lastAccessed));
// STATEMENT: Sort by date
break;
case 'title':
filteredItems.sort((a, b) => a.title.compareTo(b.title));
// STATEMENT: Sort alphabetically
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;
}
filteredItems.sort((a, b) => b.isPinned ? 1 : (a.isPinned ? -1 : 0));
// STATEMENT: Keep pinned items at top
}
Future<void> _openItem(TabData item) async {
if (extensionMode) {
html.window.open(item.url, '_blank');
// STATEMENT: Open in new tab
} else {
if (item.type == 'tab') {
await _callBrowserAPI('switchToTab', [item.id]);
// STATEMENT: Switch to tab
} else {
await _callBrowserAPI('openTab', [item.url]);
// STATEMENT: Open new tab
}
}
}
Future<void> _deleteItem(TabData item) async {
if (extensionMode) return;
// STATEMENT: Can't delete in extension mode
if (item.type == 'tab') {
await _callBrowserAPI('closeTab', [item.id]);
// STATEMENT: Close tab
} else if (item.type == 'bookmark') {
await _callBrowserAPI('removeBookmark', [item.id]);
// STATEMENT: Delete bookmark
}
await _loadAllData();
// STATEMENT: Reload data
}
Future<void> _togglePin(TabData item) async {
if (extensionMode) return;
if (item.type == 'tab') {
await _callBrowserAPI('togglePinTab', [item.id, !item.isPinned]);
// STATEMENT: Toggle pin status
await _loadAllData();
// STATEMENT: Reload data
}
}
String _getIcon(TabData item) {
// STATEMENT: Function that returns icon emoji
if (item.favicon.isNotEmpty && !item.favicon.contains('data:')) {
return '🌐';
// STATEMENT: Return globe icon
}
switch (item.type) {
case 'tab':
return '📑';
case 'bookmark':
return '';
case 'history':
return '🕐';
default:
return '🌐';
}
}
@override
Widget build(BuildContext context) {
// STATEMENT: Build method that creates UI
// This method runs every time setState() is called
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,
};
// STATEMENT: Calculate statistics
// This uses current STATE (allItems) to calculate counts
return Scaffold(
// STATEMENT: Return Scaffold widget
appBar: AppBar(
title: Row(
children: [
const Text('Browser Tab Manager'),
if (extensionMode) ...[
// STATEMENT: Conditionally show badge based on STATE
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: [
if (extensionMode)
// STATEMENT: Show stop button if in extension mode (based on STATE)
TextButton.icon(
onPressed: _stopExtensionTracking,
// STATEMENT: When pressed, call function that changes STATE
icon: const Icon(Icons.stop, color: Colors.white),
label: const Text('Stop', style: TextStyle(color: Colors.white)),
)
else
TextButton.icon(
onPressed: extensionConnected ? _startExtensionTracking : null,
// STATEMENT: Enable button based on STATE
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,
// STATEMENT: Disable button based on STATE
tooltip: 'Refresh',
),
IconButton(
icon: Icon(isGridView ? Icons.view_list : Icons.grid_view),
// STATEMENT: Show different icon based on STATE
onPressed: () {
setState(() {
// 🔥 UPDATE STATE
isGridView = !isGridView;
// 🔴 STATE CHANGE: Toggle view mode
});
},
tooltip: isGridView ? 'List View' : 'Grid View',
),
PopupMenuButton<String>(
icon: const Icon(Icons.sort),
tooltip: 'Sort by',
onSelected: (value) {
setState(() {
// 🔥 UPDATE STATE
sortBy = value;
// 🔴 STATE CHANGE: Change sort method
_filterItems();
// STATEMENT: Re-filter with new sort
});
},
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,
// Uses STATE (searchController)
decoration: InputDecoration(
hintText: extensionMode
? 'Search tracked tabs...'
: 'Search tabs, bookmarks, and history...',
// STATEMENT: Show different hint based on STATE
prefixIcon: const Icon(Icons.search),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
searchController.clear();
// STATEMENT: Clear search (changes STATE)
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 12),
if (!extensionMode)
// STATEMENT: Show filters only if not in extension mode (based on STATE)
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
FilterChip(
label: Text('All (${allItems.length})'),
// Uses STATE to show count
selected: filterType == 'all',
// Uses STATE to show selection
onSelected: (selected) {
setState(() {
// 🔥 UPDATE STATE
filterType = 'all';
// 🔴 STATE CHANGE
_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
// STATEMENT: Show different UI based on STATE
? const Center(child: CircularProgressIndicator())
// If isLoading is true, show spinner
: filteredItems.isEmpty
// If filteredItems is empty, show message
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
extensionMode ? Icons.track_changes : Icons.search_off,
// Different icon based on STATE
size: 80,
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
extensionMode
? 'Waiting for tabs...'
: 'No items found',
// Different message based on STATE
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
// If isGridView is true, show grid
? GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 300,
childAspectRatio: 1.3,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: filteredItems.length,
// Uses STATE to determine how many items
itemBuilder: (context, index) {
return ItemCard(
item: filteredItems[index],
// Passes STATE data to child widget
icon: _getIcon(filteredItems[index]),
onTap: () => _openItem(filteredItems[index]),
onDelete: () => _deleteItem(filteredItems[index]),
onTogglePin: () => _togglePin(filteredItems[index]),
extensionMode: extensionMode,
// Passes STATE to child
);
},
)
: ListView.builder(
// Otherwise show list
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,
);
},
),
),
],
),
);
}
}
// ItemCard and ItemListTile are STATELESS widgets
// They receive data (STATE) from parent and display it
// They don't manage their own STATE
class ItemCard extends StatelessWidget {
// This widget receives STATE from parent via constructor parameters
final TabData item; // STATE from parent
final String icon; // STATE from parent
final VoidCallback onTap; // Function from parent
final VoidCallback onDelete; // Function from parent
final VoidCallback onTogglePin; // Function from parent
final bool extensionMode; // STATE from parent
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) {
// STATEMENT: Build method creates UI
// This widget doesn't have setState because it's StatelessWidget
// It just displays the data it receives
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
// STATEMENT: When tapped, call function from parent
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)
// STATEMENT: Show pin icon if item is pinned
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(),
// STATEMENT: Display item type from STATE
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
),
const SizedBox(height: 6),
Expanded(
child: Text(
item.title,
// STATEMENT: Display title from STATE
style: Theme.of(context).textTheme.titleSmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 4),
Text(
item.url,
// STATEMENT: Display URL from STATE
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (item.visitCount != null) ...[
// STATEMENT: Show visit count if it exists
const SizedBox(height: 4),
Text(
'${item.visitCount} visits',
// STATEMENT: Display visit count from STATE
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
),
),
if (!extensionMode)
// STATEMENT: Show buttons only if not in extension mode (based on STATE)
ButtonBar(
alignment: MainAxisAlignment.end,
buttonPadding: EdgeInsets.zero,
children: [
if (item.type == 'tab')
// STATEMENT: Show pin button only for tabs
IconButton(
icon: Icon(
item.isPinned ? Icons.push_pin : Icons.push_pin_outlined,
// STATEMENT: Different icon based on pin STATE
size: 18,
),
onPressed: onTogglePin,
// STATEMENT: When pressed, call parent's function
tooltip: item.isPinned ? 'Unpin' : 'Pin',
),
if (item.type != 'history')
// STATEMENT: Show delete button for tabs and bookmarks (not history)
IconButton(
icon: const Icon(Icons.delete_outline, size: 18),
onPressed: onDelete,
// STATEMENT: When pressed, call parent's delete function
tooltip: 'Delete',
),
],
),
],
),
),
);
}
Color _getTypeColor(BuildContext context) {
// STATEMENT: Function that returns color based on item type
switch (item.type) {
// STATEMENT: Check item type (from STATE)
case 'tab':
return Colors.blue;
// STATEMENT: Return blue for tabs
case 'bookmark':
return Colors.orange;
// STATEMENT: Return orange for bookmarks
case 'history':
return Colors.purple;
// STATEMENT: Return purple for history
default:
return Theme.of(context).colorScheme.primary;
// STATEMENT: Return theme color as fallback
}
}
}
class ItemListTile extends StatelessWidget {
// Another STATELESS widget - displays data from parent
final TabData item; // STATE from parent
final String icon; // STATE from parent
final VoidCallback onTap; // Function from parent
final VoidCallback onDelete; // Function from parent
final VoidCallback onTogglePin; // Function from parent
final bool extensionMode; // STATE from parent
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) {
// STATEMENT: Build method creates UI
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) ...[
// STATEMENT: Show pin icon if pinned
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(),
// STATEMENT: Display type from STATE
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
item.title,
// STATEMENT: Display title from STATE
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.url,
// STATEMENT: Display URL from STATE
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (item.visitCount != null)
// STATEMENT: Show visit count if exists
Text('${item.visitCount} visits',
style: Theme.of(context).textTheme.bodySmall),
],
),
trailing: extensionMode ? null : Row(
// STATEMENT: Show buttons based on extensionMode STATE
mainAxisSize: MainAxisSize.min,
children: [
if (item.type == 'tab')
// STATEMENT: Pin button only for tabs
IconButton(
icon: Icon(
item.isPinned ? Icons.push_pin : Icons.push_pin_outlined,
),
onPressed: onTogglePin,
// STATEMENT: Call parent's function
tooltip: item.isPinned ? 'Unpin' : 'Pin',
),
if (item.type != 'history')
// STATEMENT: Delete button for tabs and bookmarks
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: onDelete,
// STATEMENT: Call parent's function
tooltip: 'Delete',
),
],
),
onTap: onTap,
// STATEMENT: When tapped, call parent's function
),
);
}
Color _getTypeColor(BuildContext context) {
// STATEMENT: Function to get color based on type
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;
}
}
}