393 lines
No EOL
12 KiB
Dart
393 lines
No EOL
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'dart:html' as html;
|
|
import '../models/tab_data.dart';
|
|
import '../services/browser_api_service.dart';
|
|
import '../services/extension_service.dart';
|
|
import '../widgets/item_card.dart';
|
|
import '../widgets/item_list_tile.dart';
|
|
import '../widgets/search_bar.dart' as custom;
|
|
import '../widgets/filter_chips.dart';
|
|
import '../widgets/app_bar_actions.dart';
|
|
|
|
class TabManagerHome extends StatefulWidget {
|
|
const TabManagerHome({super.key});
|
|
|
|
@override
|
|
State<TabManagerHome> createState() => _TabManagerHomeState();
|
|
}
|
|
|
|
class _TabManagerHomeState extends State<TabManagerHome> {
|
|
// State variables
|
|
List<TabData> allItems = [];
|
|
List<TabData> filteredItems = [];
|
|
final TextEditingController searchController = TextEditingController();
|
|
bool isGridView = true;
|
|
String sortBy = 'recent';
|
|
String filterType = 'all';
|
|
bool isLoading = true;
|
|
bool extensionConnected = false;
|
|
bool extensionMode = false;
|
|
|
|
// Services
|
|
final BrowserApiService _browserApi = BrowserApiService();
|
|
final ExtensionService _extensionService = ExtensionService();
|
|
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_setupExtensionService();
|
|
_loadAllData();
|
|
searchController.addListener(_filterItems);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _setupExtensionService() {
|
|
_extensionService.onTabsUpdate = (tabs) {
|
|
setState(() {
|
|
allItems = tabs;
|
|
extensionConnected = true;
|
|
extensionMode = true;
|
|
_filterItems();
|
|
});
|
|
};
|
|
|
|
_extensionService.onTrackingStart = () {
|
|
setState(() {
|
|
extensionMode = true;
|
|
extensionConnected = true;
|
|
});
|
|
};
|
|
|
|
_extensionService.onTrackingStop = () {
|
|
setState(() {
|
|
extensionMode = false;
|
|
_loadAllData();
|
|
});
|
|
};
|
|
|
|
_extensionService.setupListener();
|
|
}
|
|
|
|
void _startExtensionTracking() {
|
|
_extensionService.startTracking();
|
|
setState(() {
|
|
extensionMode = true;
|
|
allItems.clear();
|
|
});
|
|
}
|
|
|
|
void _stopExtensionTracking() {
|
|
_extensionService.stopTracking();
|
|
setState(() {
|
|
extensionMode = false;
|
|
});
|
|
_loadAllData();
|
|
}
|
|
|
|
Future<void> _loadAllData() async {
|
|
if (extensionMode) return;
|
|
|
|
setState(() {
|
|
isLoading = true;
|
|
});
|
|
|
|
try {
|
|
final tabs = await _browserApi.getTabs();
|
|
final bookmarks = await _browserApi.getBookmarks();
|
|
final history = await _browserApi.getHistory();
|
|
|
|
setState(() {
|
|
allItems = [...tabs, ...bookmarks, ...history];
|
|
_filterItems();
|
|
isLoading = false;
|
|
});
|
|
|
|
_checkExtensionConnection();
|
|
} catch (e) {
|
|
print('Error loading data: $e');
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _checkExtensionConnection() {
|
|
_extensionService.getStatus();
|
|
Future.delayed(const Duration(milliseconds: 500), () {
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
});
|
|
}
|
|
|
|
void _filterItems() {
|
|
final query = searchController.text.toLowerCase();
|
|
setState(() {
|
|
filteredItems = allItems.where((item) {
|
|
if (filterType != 'all' && item.type != filterType.replaceAll('s', '')) {
|
|
return false;
|
|
}
|
|
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;
|
|
}
|
|
filteredItems.sort((a, b) => b.isPinned ? 1 : (a.isPinned ? -1 : 0));
|
|
}
|
|
|
|
|
|
Future<void> _openItem(TabData item) async {
|
|
print('🔥 DEBUG: _openItem called with item: ${item.title}');
|
|
print('🔥 DEBUG: Extension mode: $extensionMode');
|
|
print('🔥 DEBUG: Item type: ${item.type}');
|
|
print('🔥 DEBUG: Item URL: ${item.url}');
|
|
|
|
if (extensionMode) {
|
|
print('🔥 DEBUG: Opening in new tab via html.window.open');
|
|
html.window.open(item.url, '_blank');
|
|
} else {
|
|
if (item.type == 'tab') {
|
|
print('🔥 DEBUG: Switching to existing tab with ID: ${item.id}');
|
|
await _browserApi.switchToTab(item.id);
|
|
} else {
|
|
print('🔥 DEBUG: Opening new tab for bookmark/history');
|
|
await _browserApi.openTab(item.url);
|
|
}
|
|
}
|
|
print('🔥 DEBUG: _openItem completed');
|
|
}
|
|
|
|
Future<void> _deleteItem(TabData item) async {
|
|
if (extensionMode) return;
|
|
|
|
if (item.type == 'tab') {
|
|
await _browserApi.closeTab(item.id);
|
|
} else if (item.type == 'bookmark') {
|
|
await _browserApi.removeBookmark(item.id);
|
|
}
|
|
|
|
await _loadAllData();
|
|
}
|
|
|
|
Future<void> _togglePin(TabData item) async {
|
|
if (extensionMode) return;
|
|
|
|
if (item.type == 'tab') {
|
|
await _browserApi.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) {
|
|
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: [
|
|
AppBarActions(
|
|
extensionMode: extensionMode,
|
|
extensionConnected: extensionConnected,
|
|
isGridView: isGridView,
|
|
sortBy: sortBy,
|
|
onStartTracking: _startExtensionTracking,
|
|
onStopTracking: _stopExtensionTracking,
|
|
onRefresh: _loadAllData,
|
|
onToggleView: () {
|
|
setState(() {
|
|
isGridView = !isGridView;
|
|
});
|
|
},
|
|
onSortChanged: (value) {
|
|
setState(() {
|
|
sortBy = value;
|
|
_filterItems();
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
custom.SearchBar(
|
|
controller: searchController,
|
|
extensionMode: extensionMode,
|
|
onClear: () {
|
|
searchController.clear();
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (!extensionMode)
|
|
FilterChips(
|
|
allItems: allItems,
|
|
filterType: filterType,
|
|
onFilterChanged: (type) {
|
|
setState(() {
|
|
filterType = type;
|
|
_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,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// This is the main screen of our app where users see and manage their browser tabs.
|
|
//
|
|
// It is a StatefulWidget which means it holds data that can change over time.
|
|
//
|
|
// The state includes lists of tabs, search text, view preferences, and loading status.
|
|
//
|
|
// It communicates with two services: BrowserApiService for browser data and ExtensionService for real-time tracking.
|
|
//
|
|
// When the screen loads, it fetches all tabs, bookmarks, and history from the browser.
|
|
//
|
|
// Users can search items, filter by type, sort by different criteria, and switch between grid and list views.
|
|
//
|
|
// Extension mode allows real-time tracking of browser tabs as they open and close.
|
|
//
|
|
// The build method creates the UI with an app bar, search box, filters, and either a grid or list of items.
|
|
//
|
|
// User actions like opening, deleting, or pinning items trigger methods that update the browser and refresh the display.
|
|
//
|
|
// This is the central hub that coordinates all the app functionality and user interactions. |