diff --git a/Study/code-to-ui-mapping.svg b/Study/code-to-ui-mapping.svg new file mode 100644 index 0000000..1db415e --- /dev/null +++ b/Study/code-to-ui-mapping.svg @@ -0,0 +1,149 @@ + + + + + + + + + + Browser Tab Manager + ▶ Track Tabs ⟳ ☰ + + + + 🔍 Search tabs, bookmarks, and history... + + + + All (0) + + + Tabs (0) + + + Bookmarks (0) + + + History (0) + + + + + + + No items found + Try a different search or filter + + + + + + + appBar: AppBar( + title: Row(children: [ + const Text('Browser Tab Manager'), + ]), + + + + + + + TextField( + controller: searchController, + decoration: InputDecoration( + hintText: 'Search tabs, bookmarks... + ), + + + + + + + FilterChip( + label: Text('All (${allItems.length})'), + selected: filterType == 'all', + onSelected: (selected) { + setState(() { filterType = 'all'; }); + }, + + + + + + + filteredItems.isEmpty + ? Center(child: Column( + children: [ + Icon(Icons.search_off, size: 80), + Text('No items found'), + Text('Try a different search...'), + ], + + + + + + + // 🔴 STATE variables in _TabManagerHomeState: + List<TabData> allItems = []; + List<TabData> filteredItems = []; + TextEditingController searchController; + bool isGridView = true; + String sortBy = 'recent'; + String filterType = 'all'; + bool isLoading = true; + + + UI Elements: + + AppBar + + + Search Field + + + Filter Chips + + + Empty State + + + State Variables + + + + Key Concepts: + + setState(() { ... }) + • Updates STATE variables + • Triggers UI rebuild automatically + + StatefulWidget + • _TabManagerHomeState holds all STATE + • When STATE changes, UI updates + + StatelessWidget + • ItemCard and ItemListTile receive data + • Display data but don't manage STATE + + Data Flow + • User action → function called → setState → STATE changes → UI rebuilds + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Study/tabdata-complete-breakdown.html b/Study/tabdata-complete-breakdown.html new file mode 100644 index 0000000..563e7f1 --- /dev/null +++ b/Study/tabdata-complete-breakdown.html @@ -0,0 +1,595 @@ + + + + + + TabData Complete Breakdown + + + +
+

📚 Complete TabData Breakdown

+ + +
+

🎯 The Three Main Parts

+ +

+ Your TabData class has THREE main sections. Let's understand what each one is called and what it does: +

+ +
+
+

Part 1️⃣

+

CLASS DEFINITION

+
+class TabData { + String id; + String title; + // ... properties +}
+

Declares the class and its properties (variables)

+
+ +
+

Part 2️⃣

+

REGULAR CONSTRUCTOR

+
+TabData({ + required this.id, + // ... parameters +})
+

The default way to create TabData objects

+
+ +
+

Part 3️⃣

+

FACTORY CONSTRUCTOR

+
+factory TabData.fromJson( + Map json +) => TabData(...)
+

A special way to create TabData from JSON

+
+
+ +
+

🔗 How They Connect:

+
+ 1. Class Definition defines WHAT a TabData object IS +
+ → "A TabData has an id, title, url, etc." +
+
+
⬇️
+
+ 2. Regular Constructor creates TabData objects manually +
+ → TabData(id: '123', title: 'Test', ...) +
+
+
⬇️
+
+ 3. Factory Constructor ALSO creates TabData objects, but from JSON +
+ → TabData.fromJson({'id': '123', 'title': 'Test'}) +
+
+ → Internally, it calls the Regular Constructor! +
+
+
+
+ + +
+

Part 1️⃣: Class Definition & Properties

+ +
+
Line 1
+
+class TabData {
+ +
🔍 Breaking It Down:
+
    +
  • class - Keyword that declares a new class
  • +
  • TabData - The name of our class
  • +
  • { - Opening brace, starts the class body
  • +
+ +
+ What is a class? A class is a blueprint or template for creating objects. Like a cookie cutter - the class is the cutter, and each TabData object is a cookie made from it. +
+ +
+ 📝 Official Name: "Class Declaration" +
+
+ +
+
Lines 2-10
+
+String id; +String title; +String url; +String favicon; +DateTime lastAccessed; +bool isPinned; +String type; +int? visitCount; +String? folder;
+ +
🔍 What These Are:
+

These are called properties, fields, or instance variables.

+ +

They define what data each TabData object will store.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LineProperty NameTypeCan be null?
2idString❌ No
3titleString❌ No
4urlString❌ No
5faviconString❌ No
6lastAccessedDateTime❌ No
7isPinnedbool❌ No
8typeString❌ No
9visitCountint?✅ Yes (the ? means nullable)
10folderString?✅ Yes (the ? means nullable)
+ +
+ 📝 Official Name: "Instance Variables" or "Properties" or "Fields" +
+
+
+ + +
+

Part 2️⃣: Regular Constructor

+ +
+
Line 12
+
+TabData({
+ +
🔍 What This Is:
+
    +
  • TabData - Constructor name (same as class name)
  • +
  • ( - Opens parameter list
  • +
  • { - Curly brace means "named parameters"
  • +
+ +
+ 🎯 This is called: "Default Constructor" or "Regular Constructor" or "Named Parameter Constructor" +
+ +
+ What does a constructor do? A constructor is a special method that creates and initializes new objects. When you write TabData(...), this constructor runs. +
+
+ +
+
Lines 13-21
+
+required this.id, // Line 13 +required this.title, // Line 14 +required this.url, // Line 15 +this.favicon = '', // Line 16 +DateTime? lastAccessed, // Line 17 +this.isPinned = false, // Line 18 +this.type = 'tab', // Line 19 +this.visitCount, // Line 20 +this.folder, // Line 21
+ +
🔍 Each Line Explained:
+ +
+ Lines 13-15: required this.id +
    +
  • required = MUST be provided
  • +
  • this.id = assign parameter value to the id property
  • +
+
+ +
+ Line 16: this.favicon = '' +
    +
  • Optional parameter (no required)
  • +
  • Has default value: empty string ''
  • +
  • If not provided → uses ''
  • +
+
+ +
+ Line 17: DateTime? lastAccessed +
    +
  • NOT this.lastAccessed - just the parameter name!
  • +
  • The ? means it can be null
  • +
  • This is handled specially in the initializer list (line 22)
  • +
+
+ +
+ Lines 18-19: this.isPinned = false +
    +
  • Optional with defaults
  • +
  • isPinned defaults to false
  • +
  • type defaults to 'tab'
  • +
+
+ +
+ Lines 20-21: this.visitCount +
    +
  • Optional, no default value
  • +
  • If not provided → becomes null
  • +
+
+ +
+ 📝 Official Name: "Constructor Parameters" +
+
+ +
+
Line 22
+
+}) : lastAccessed = lastAccessed ?? DateTime.now();
+ +
🔍 The Most Important Line!
+ +

Let's break this into 4 parts:

+ +
+

Part 1: }

+

Closes the parameter list

+
+ +
+

Part 2: :

+

Starts the initializer list

+

The initializer list runs BEFORE the object is created. It's for special initialization logic.

+
+ +
+

Part 3: lastAccessed = lastAccessed

+

Left side: lastAccessed → the property (same as this.lastAccessed)

+

Right side: lastAccessed → the parameter from line 17

+
+ +
+

Part 4: ?? DateTime.now()

+

?? = "if null, use this instead"

+

DateTime.now() = current date/time

+
+ +
+

🎯 In Plain English:

+

+ "Set this.lastAccessed (the property) to the value of lastAccessed (the parameter), + BUT if the parameter is null, use DateTime.now() instead" +

+
+ +
+ 📝 Official Names: +
    +
  • The : part is called an "Initializer List"
  • +
  • The ?? operator is called the "Null-Coalescing Operator"
  • +
+
+
+
+ + +
+

Part 3️⃣: Factory Constructor (fromJson)

+ +
+
Line 25
+
+factory TabData.fromJson(Map<String, dynamic> json) => TabData(
+ +
🔍 Breaking Down Each Part:
+ +
+

factory

+

Keyword that makes this a factory constructor

+

What's special about factory? It can do logic/processing BEFORE creating the object. It can also return an existing object instead of creating a new one (though we don't do that here).

+
+ +
+

TabData.fromJson

+

This is a named constructor

+
    +
  • TabData = class name
  • +
  • . = dot separator
  • +
  • fromJson = name we chose for this constructor
  • +
+

Why "fromJson"? Because this constructor creates a TabData object FROM JSON data. You can have multiple constructors with different names!

+
+ +
+

Map<String, dynamic> json

+

This is the parameter - a Map (like a dictionary)

+
    +
  • Map = data type (key-value pairs)
  • +
  • <String, dynamic> = keys are Strings, values can be anything
  • +
  • json = parameter name
  • +
+
+ +
+

=> TabData(

+

=> is called an "arrow function" or "expression body"

+

Meaning: "return the result of TabData(...)"

+

This factory constructor calls the regular constructor (Part 2) and returns the result!

+
+ +
+ 📝 Official Name: "Factory Constructor" or "Named Factory Constructor" +
+
+ +
+
Lines 26-37
+

Inside the Factory - Extracting Data from JSON

+ +

Each line extracts data from the json Map and passes it to the regular constructor:

+ +
+id: json['id'].toString(), // Line 26 +title: json['title'] ?? 'Untitled', // Line 27 +url: json['url'] ?? '', \ No newline at end of file diff --git a/Study/tabdata-visual-explanation.html b/Study/tabdata-visual-explanation.html new file mode 100644 index 0000000..e49e4ac --- /dev/null +++ b/Study/tabdata-visual-explanation.html @@ -0,0 +1,960 @@ + + + + + + TabData Visual Explanation + + + +
+

📊 TabData Class - Visual Guide

+ + +
+

🎯 What is TabData?

+

+ TabData is a blueprint (or template) that defines how we store information about tabs, bookmarks, and history items. +

+ +
+
+ 🔖 + Example: A YouTube Tab +
+
+
id: "tab_123"
+
title: "Flutter - YouTube"
+
url: "https://youtube.com"
+
favicon: "https://youtube.com/favicon.ico"
+
lastAccessed: 2025-10-20 14:30:00
+
isPinned: true
+
type: "tab"
+
visitCount: 47
+
folder: null
+
+
+ +

+ Every tab, bookmark, or history item in your app is stored as a TabData object containing this information. +

+
+ + +
+

📋 The Properties (Variables)

+

These are the pieces of information each TabData object holds:

+ +
+
+ String id + Text + Unique identifier (like a social security number for the tab) + REQUIRED +
+ +
+ String title + Text + The page title (e.g., "Google") + REQUIRED +
+ +
+ String url + Text + Web address (e.g., "https://google.com") + REQUIRED +
+ +
+ String favicon + Text + The small icon for the website (defaults to empty '') + OPTIONAL +
+ +
+ DateTime lastAccessed + Date/Time + When you last opened this (defaults to now) + OPTIONAL +
+ +
+ bool isPinned + True/False + Is the tab pinned? (defaults to false) + OPTIONAL +
+ +
+ String type + Text + What kind: 'tab', 'bookmark', or 'history' (defaults to 'tab') + OPTIONAL +
+ +
+ int? visitCount + Number? + How many times visited (can be null) + OPTIONAL +
+ +
+ String? folder + Text? + Bookmark folder name (can be null) + OPTIONAL +
+
+ +
+ 💡 Note: The ? after a type (like int? or String?) means the value can be null (empty/missing). +
+
+ + +
+

🏗️ The Constructor - Creating TabData Objects

+

The constructor is like a factory that builds TabData objects.

+ +
+

How to Create a TabData:

+ +
+// Creating a new TabData object +final myTab = TabData( + id: 'tab_456', // REQUIRED ✅ + title: 'Flutter Docs', // REQUIRED ✅ + url: 'https://flutter.dev', // REQUIRED ✅ + favicon: 'https://flutter.dev/icon.png', // Optional + isPinned: true, // Optional (defaults to false) + visitCount: 23, // Optional (can be null) +); +
+
+ +
+
+ 📝 You provide data +
+ id, title, url
+ + optional fields +
+
+ +
+ +
+ 🏭 Constructor runs +
+ Fills in defaults
+ Sets lastAccessed +
+
+ +
+ +
+ ✨ TabData object created +
+ Ready to use! +
+
+
+ +
+ 🎯 Key Feature: The line : lastAccessed = lastAccessed ?? DateTime.now() means: +

+ "If lastAccessed is provided, use it. Otherwise (??), use the current time (DateTime.now())." +
+
+ + +
+

🔄 The fromJson Factory - Converting JSON to TabData

+

+ When we get data from the browser API, it comes as JSON (a text format). + The fromJson method converts that JSON into a TabData object. +

+ +
+
+
❌ JSON (from browser)
+
+{ + "id": "tab_789", + "title": "GitHub", + "url": "https://github.com", + "favIconUrl": "...", + "lastVisitTime": "2025-10-20T..." +} +
+

+ This is just text - we can't use it directly! +

+
+ +
+
✅ TabData Object (in our app)
+
+TabData { + id: "tab_789" + title: "GitHub" + url: "https://github.com" + favicon: "..." + lastAccessed: DateTime + isPinned: false + ... +} +
+

+ This is a proper object we can work with! +

+
+
+ +
+

How fromJson Works:

+ +
+// Usage: +final jsonData = {'id': '123', 'title': 'Test', 'url': '...'}; +final tab = TabData.fromJson(jsonData); + +// The method does this: +id: json['id'].toString() // Get 'id' from JSON +title: json['title'] ?? 'Untitled' // Get 'title', or use 'Untitled' if missing +
+
+
+ + +
+

📅 Smart Date Handling

+

+ Different browser APIs send dates with different field names. + Our fromJson method is smart enough to handle all of them! +

+ +
+// The code checks multiple possible date fields: + +lastAccessed: json['lastAccessed'] != null + ? DateTime.parse(json['lastAccessed']) // Try 'lastAccessed' first + : (json['lastVisitTime'] != null + ? DateTime.parse(json['lastVisitTime']) // If not, try 'lastVisitTime' + : (json['dateAdded'] != null + ? DateTime.parse(json['dateAdded']) // If not, try 'dateAdded' + : (json['timestamp'] != null + ? DateTime.parse(json['timestamp']) // If not, try 'timestamp' + : DateTime.now()))) // If none exist, use current time +
+ +
+
+ Try 1:
+ lastAccessed? +
+
+
+ Try 2:
+ lastVisitTime? +
+
+
+ Try 3:
+ dateAdded? +
+
+
+ Try 4:
+ timestamp? +
+
+
+ Fallback:
+ Use now! +
+
+ +
+ 💡 Why? Different browsers and APIs use different names for the same thing. + This ensures our app works with all of them! +
+
+ + +
+

🛡️ The ?? Operator - Providing Defaults

+

+ The ?? operator means: "if null (missing), use this default value instead" +

+ +
+
+
Without ?? (Would Crash)
+
+title: json['title'] +// If 'title' is missing → ERROR! 💥 +
+
+ +
+
With ?? (Safe)
+
+title: json['title'] ?? 'Untitled' +// If 'title' is missing → use 'Untitled' ✅ +
+
+
+ +
+

All the defaults in fromJson:

+
+ + json['title'] ?? 'Untitled' + + → If no title, use "Untitled" +
+
+ + json['url'] ?? '' + + → If no URL, use empty string +
+
+ + json['favicon'] ?? json['favIconUrl'] ?? '' + + → Try 'favicon', then 'favIconUrl', then empty +
+
+ + json['isPinned'] ?? false + + → If not specified, assume not pinned +
+
+ + json['type'] ?? 'tab' + + → If no type, assume it's a tab +
+
+
+ + +
+

🌍 Real-World Example

+ +
+

📱 What happens when you open a tab:

+ +
+ Step 1: Browser sends JSON data +
+{ + "id": "tab_999", + "title": "Claude AI", + "url": "https://claude.ai", + "favIconUrl": "https://claude.ai/icon.png", + "lastVisitTime": "2025-10-20T14:30:00", + "visitCount": 15 +} +
+
+ +
⬇️
+ +
+ Step 2: fromJson converts it +
+final tab = TabData.fromJson(browserData); +
+
+ +
⬇️
+ +
+ Step 3: We have a usable TabData object! +
+ tab.id → "tab_999"
+ tab.title → "Claude AI"
+ tab.url → "https://claude.ai"
+ tab.favicon → "https://claude.ai/icon.png"
+ tab.lastAccessed → DateTime(2025, 10, 20, 14, 30)
+ tab.isPinned → false (default)
+ tab.type → "tab" (default)
+ tab.visitCount → 15
+ tab.folder → null +
+
+ +
⬇️
+ +
+ Step 4: Display in UI +
+ Now we can show: tab.title, tab.url, tab.favicon, etc. in our card widgets! +
+
+
+
+ + +
+

🤔 Why Is TabData Designed This Way?

+ +
+
+

✅ Type Safety

+

Each property has a specific type (String, bool, DateTime). This prevents errors like putting a number where text should be.

+
+ +
+

🛡️ Error Handling

+

The ?? operators and multiple date field checks ensure the app doesn't crash if data is missing or in a different format.

+
+ +
+

🔄 Flexibility

+

Works with different browser APIs (Chrome, Firefox, Edge) because it handles different field names and formats.

+
+ +
+

📦 Organization

+

All tab/bookmark/history data is in one place, making it easy to work with throughout the app.

+
+ +
+

🔍 Readability

+

Instead of messy JSON strings everywhere, we have clean objects: tab.title, tab.url, etc.

+
+ +
+

⚡ Efficiency

+

Creating TabData objects is fast, and we can easily create lists of them: List<TabData>.

+
+
+
+ + +
+

🔗 How TabData Is Used in main.dart

+ +
+

The Complete Data Flow:

+ +
+
+ 1. Browser API +
+ Returns JSON data
+ about tabs/bookmarks +
+
+ +
+ +
+ 2. fromJson() +
+ Converts JSON
+ to TabData objects +
+
+ +
+ +
+ 3. State Lists +
+ Stored in allItems[]
+ and filteredItems[] +
+
+ +
+ +
+ 4. UI Display +
+ ItemCard widgets
+ show the data +
+
+
+ +
+// 1. Get JSON from browser +final result = await _callBrowserAPI('getTabs'); +final List<dynamic> data = json.decode(result); + +// 2. Convert each JSON item to TabData +return data.map((item) { + item['type'] = 'tab'; + return TabData.fromJson(item); // ← Using fromJson here! +}).toList(); + +// 3. Store in state +setState(() { + allItems = [...tabs, ...bookmarks, ...history]; // All TabData objects +}); + +// 4. Display in UI +return ItemCard( + item: filteredItems[index], // ← Pass TabData to card + icon: _getIcon(filteredItems[index]), +); +
+
+
+ + +
+

📝 Statement-by-Statement Breakdown

+ +
+
+ class TabData { + + STATEMENT: Declare a new class (blueprint) named TabData + +
+ +
+ String id; + + STATEMENT: Declare a variable named 'id' that holds text (String) + +
+ +
+ int? visitCount; + + STATEMENT: Declare a variable that holds a number, but can also be null (empty) + +
+ +
+ TabData({...}) + + STATEMENT: Define the constructor - how to create TabData objects + +
+ +
+ required this.id + + STATEMENT: This parameter MUST be provided when creating a TabData + +
+ +
+ this.favicon = '' + + STATEMENT: This parameter is optional, defaults to empty string if not provided + +
+ +
+ : lastAccessed = lastAccessed ?? DateTime.now() + + STATEMENT: Initializer list - set lastAccessed to the provided value, or current time if null + +
+ +
+ factory TabData.fromJson(...) + + STATEMENT: Define a factory method - an alternative way to create TabData from JSON + +
+ +
+ json['id'].toString() + + STATEMENT: Get the 'id' field from JSON and convert it to a string + +
+ +
+ json['title'] ?? 'Untitled' + + STATEMENT: Get 'title' from JSON, or use 'Untitled' if it's null/missing + +
+ +
+ DateTime.parse(json['lastAccessed']) + + STATEMENT: Convert the date string from JSON into a DateTime object + +
+
+
+ + +
+

⚙️ Common Operations with TabData

+ +
+

Creating a new TabData:

+
+final newTab = TabData( + id: 'tab_001', + title: 'My Website', + url: 'https://example.com', +); +
+ +

Converting JSON to TabData:

+
+final jsonData = {'id': '123', 'title': 'Test', 'url': '...'}; +final tab = TabData.fromJson(jsonData); +
+ +

Accessing properties:

+
+print(tab.title); // Print the title +print(tab.url); // Print the URL +if (tab.isPinned) { // Check if pinned + print('This tab is pinned!'); +} +
+ +

Creating a list of TabData:

+
+List<TabData> allTabs = []; // Empty list + +// Add tabs to the list +allTabs.add(newTab); + +// Convert multiple JSON items to TabData list +final tabs = jsonArray.map((item) => TabData.fromJson(item)).toList(); +
+ +

Modifying properties:

+
+tab.title = 'New Title'; // Change the title +tab.isPinned = true; // Pin the tab +tab.lastAccessed = DateTime.now(); // Update last access time +
+
+
+ + +
+

📚 Summary

+ +
+

TabData is the foundation of the entire app!

+ +
    +
  • + 🎯 + Blueprint: Defines how we store tab/bookmark/history information +
  • +
  • + 📋 + 9 Properties: id, title, url, favicon, lastAccessed, isPinned, type, visitCount, folder +
  • +
  • + 🏗️ + Constructor: Creates new TabData objects with required and optional fields +
  • +
  • + 🔄 + fromJson: Converts browser API JSON data into TabData objects +
  • +
  • + 🛡️ + Error Handling: Uses ?? operator and multiple checks to handle missing data +
  • +
  • + 📅 + Smart Dates: Handles 4 different date field names from different browsers +
  • +
  • + + Clean Code: Makes working with browser data easy and type-safe +
  • +
+
+ +
+

💡 Remember

+

+ Every tab, bookmark, and history item you see in the app is a TabData object. +

+ Without TabData, we'd just have messy JSON strings everywhere! +

+
+
+
+ + \ No newline at end of file diff --git a/dist/browser-tab-manager.tar.gz b/dist/browser-tab-manager.tar.gz deleted file mode 100644 index 0903211..0000000 Binary files a/dist/browser-tab-manager.tar.gz and /dev/null differ diff --git a/dist/browser-tab-manager/Dockerfile b/dist/browser-tab-manager/Dockerfile deleted file mode 100644 index daf3f3f..0000000 --- a/dist/browser-tab-manager/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -FROM debian:bullseye-slim AS builder - -# Install dependencies -RUN apt-get update && apt-get install -y \ - curl \ - git \ - unzip \ - xz-utils \ - && rm -rf /var/lib/apt/lists/* - -# Create flutter user -RUN useradd -m -u 1000 flutter - -# Switch to flutter user -USER flutter - -# Install Flutter -RUN git clone https://github.com/flutter/flutter.git /home/flutter/flutter -b stable --depth 1 -ENV PATH="/home/flutter/flutter/bin:${PATH}" - -# Configure Flutter -RUN flutter config --no-analytics && \ - flutter config --enable-web && \ - flutter precache --web - -# Set working directory -WORKDIR /home/flutter/app - -# Copy project files -COPY --chown=flutter:flutter . . - -# Enable web for this project -RUN flutter create . --platforms web - -# Get dependencies -RUN flutter pub get - -# Build web app -RUN flutter build web --release - -# Production stage -FROM nginx:alpine - -# Copy built web app -COPY --from=builder /home/flutter/app/build/web /usr/share/nginx/html - -# Copy nginx config -COPY nginx.conf /etc/nginx/conf.d/default.conf - -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/dist/browser-tab-manager/deploy.sh b/dist/browser-tab-manager/deploy.sh deleted file mode 100755 index 26ad299..0000000 --- a/dist/browser-tab-manager/deploy.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -# Server Deployment Script -set -e - -echo "🚀 Deploying Browser Tab Manager on Server..." - -# Ensure we're in the right directory -if [ ! -f "Dockerfile" ]; then - echo "❌ Error: Dockerfile not found. Are you in the right directory?" - exit 1 -fi - -# Create web/manifest.json if missing -mkdir -p web -if [ ! -f "web/manifest.json" ]; then - cat > web/manifest.json << 'EOF' -{ - "name": "Browser Tab Manager", - "short_name": "TabManager", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "Manage browser tabs in a grid view", - "orientation": "portrait-primary", - "prefer_related_applications": false -} -EOF -fi - -echo "✅ Project structure ready" - -# Build the container -echo "🔨 Building Podman container..." -podman build -t browser-tab-manager . - -# Stop and remove existing container if running -podman stop browser-tab-manager 2>/dev/null || true -podman rm browser-tab-manager 2>/dev/null || true - -# Run the container on port 8081 -echo "🚢 Starting container..." -podman run -d \ - --name browser-tab-manager \ - -p 8081:80 \ - --restart unless-stopped \ - browser-tab-manager - -echo "✅ Container started successfully!" -echo "" -echo "🌐 Your Browser Tab Manager is now running at:" -SERVER_IP=$(hostname -I | awk '{print $1}' 2>/dev/null || echo "your-server-ip") -echo " http://${SERVER_IP}:8081" -echo "" -echo "📝 Useful commands:" -echo " View logs: podman logs -f browser-tab-manager" -echo " Stop: podman stop browser-tab-manager" -echo " Start: podman start browser-tab-manager" -echo " Restart: podman restart browser-tab-manager" -echo " Remove: podman rm -f browser-tab-manager" diff --git a/dist/browser-tab-manager/lib/main.dart b/dist/browser-tab-manager/lib/main.dart deleted file mode 100644 index 13cbe4f..0000000 --- a/dist/browser-tab-manager/lib/main.dart +++ /dev/null @@ -1,890 +0,0 @@ -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; - } - } -} \ No newline at end of file diff --git a/dist/browser-tab-manager/nginx.conf b/dist/browser-tab-manager/nginx.conf deleted file mode 100644 index f3cd537..0000000 --- a/dist/browser-tab-manager/nginx.conf +++ /dev/null @@ -1,20 +0,0 @@ -server { - listen 80; - server_name _; - root /usr/share/nginx/html; - index index.html; - - # Enable gzip - gzip on; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; - - location / { - try_files $uri $uri/ /index.html; - } - - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } -} \ No newline at end of file diff --git a/dist/browser-tab-manager/pubspec.yaml b/dist/browser-tab-manager/pubspec.yaml deleted file mode 100644 index de94260..0000000 --- a/dist/browser-tab-manager/pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: browser_tab_manager -description: A Flutter web app for managing browser tabs, bookmarks, and history -publish_to: 'none' -version: 1.0.0+1 - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - flutter: - sdk: flutter - web: ^1.1.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^3.0.0 - -flutter: - uses-material-design: true \ No newline at end of file diff --git a/dist/browser-tab-manager/web/manifest.json b/dist/browser-tab-manager/web/manifest.json deleted file mode 100644 index 8059dd7..0000000 --- a/dist/browser-tab-manager/web/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "Browser Tab Manager", - "short_name": "TabManager", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "Manage browser tabs in a grid view", - "orientation": "portrait-primary", - "prefer_related_applications": false -} diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index 07806d7..5e33698 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -50,9 +50,7 @@ class AppConstants { } -// This is our centralized configuration file where all fixed values are stored in one place. -// -// It contains settings used throughout the app. +// centralized configuration fixed values stored one place. // // Instead of hardcoding values // @@ -63,5 +61,4 @@ class AppConstants { // All user-facing messages are stored here making translation and text updates simple. // // Grid layout values are defined here so we can adjust the card view globally. -// -// This approach keeps our code clean, consistent, and easy to maintain. \ No newline at end of file +// Easy maintain \ No newline at end of file diff --git a/lib/models/tab_data.dart b/lib/models/tab_data.dart index 7506376..ca352b7 100644 --- a/lib/models/tab_data.dart +++ b/lib/models/tab_data.dart @@ -1,4 +1,14 @@ + + +// Class creating objects "Class Declaration" The Blueprint +// ? means can be null or nothing "Operators" +// ?? means if null, use this instead + class TabData { + + // FIELDS for storing data "Fields" + // What we will be uding in other parts of the code + String id; String title; String url; @@ -7,9 +17,12 @@ class TabData { bool isPinned; String type; int? visitCount; - String? folder; + String? folder; + // Constructor Declaration "Constructor Declaration" The Assembly Line TabData({ + // Parameters saved from the data coming in + // "this." current object required this.id, required this.title, required this.url, @@ -19,9 +32,16 @@ class TabData { this.type = 'tab', this.visitCount, this.folder, - }) : lastAccessed = lastAccessed ?? DateTime.now(); + }) + // initializer logic, AFTER parameters, BEFORE Constructor + // ?? IF left is null, use right + // left side is field, right side is parameter the new saved value + : lastAccessed = lastAccessed ?? DateTime.now(); + // factory logic before constructor, from json so we don't have to extract later. factory TabData.fromJson(Map json) => TabData( + // Extract data pass to regular constructor + id: json['id'].toString(), title: json['title'] ?? 'Untitled', url: json['url'] ?? '', @@ -42,22 +62,20 @@ class TabData { ); } -// This is our data model that represents a single tab, bookmark, or history item. +// Data model single tab, bookmark, or history item. // -// It holds all the information we need about each item: title, URL, favicon, when it was accessed, etc. +// Blueprint storing browser item information. // -// Think of it as a blueprint or template for storing browser item information. +// Every tab ,bookmark is stored as a TabData object. // -// Every tab, bookmark, or history entry in our app is stored as a TabData object. +// Constructor creates new TabData objects. // -// The constructor creates new TabData objects with required fields like id, title, and url. +// Favicon defaults to empty string +// +// isPinned defaults to false. // -// Optional fields have default values, like favicon defaults to empty string and isPinned defaults to false. +// FromJson factory data browser API into TabData object. // -// The fromJson factory method converts JSON data from the browser API into a TabData object. +// Missing data, default values ??. // -// It handles different date field names from different sources like lastAccessed, lastVisitTime, dateAdded, or timestamp. -// -// It also handles missing data gracefully by using default values with the ?? operator. -// -// This class is the foundation of our app since everything revolves around displaying and managing these items. \ No newline at end of file +// Foundation: managing/storing tabs. \ No newline at end of file diff --git a/lib/screens/tab_manager_home.dart b/lib/screens/tab_manager_home.dart index 9ee4aeb..5d92692 100644 --- a/lib/screens/tab_manager_home.dart +++ b/lib/screens/tab_manager_home.dart @@ -9,6 +9,9 @@ import '../widgets/search_bar.dart' as custom; import '../widgets/filter_chips.dart'; import '../widgets/app_bar_actions.dart'; + +// StatefulWidget + class TabManagerHome extends StatefulWidget { const TabManagerHome({super.key}); diff --git a/lib/services/browser_api_service.dart b/lib/services/browser_api_service.dart index 5eed489..a145a33 100644 --- a/lib/services/browser_api_service.dart +++ b/lib/services/browser_api_service.dart @@ -3,6 +3,17 @@ import 'dart:convert'; import 'dart:js_util' as js_util; import '../models/tab_data.dart'; + +// communication with the browser API +// list of TabData objects +// results LATER +// API takes time so future +// ASYNC WAIT +// return empty if fail +// final cannot be changed + + + class BrowserApiService { Future> getTabs() async { try { @@ -94,20 +105,18 @@ class BrowserApiService { } } -// This service acts as a bridge between our Flutter app and the browser extension. +// Bridge Flutter app and the browser extension. // -// It provides clean methods to interact with browser APIs for tabs, bookmarks, and history. +// Methods fetching from browser, convert to TabData objects. // -// The getTabs, getBookmarks, and getHistory methods fetch data from the browser and convert it to TabData objects. +// SwitchToTab, openTab, closeTab allow control of tabs programmatically. // -// Action methods like switchToTab, openTab, closeTab allow us to control browser tabs programmatically. +// _callBrowserAPI low level JavaScript communication. // -// The private _callBrowserAPI method handles the low-level JavaScript communication. +// dart:js_util call JavaScript functions exposed by browser extension through window.BrowserAPI. // -// It uses dart:js_util to call JavaScript functions exposed by the browser extension through window.BrowserAPI. +// Empty results if the API is unavailable. // -// All methods handle errors gracefully and return empty results if the API is unavailable. +// Browser specific code separate from UI logic, app easier to maintain. // -// This abstraction keeps browser-specific code separate from our UI logic making the app easier to maintain. -// -// When running in development without the extension, it prints debug messages instead of crashing. \ No newline at end of file +// Without extension, prints debug messages instead of crashing. \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart index 00d4b0f..52332c4 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,30 +1,17 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:browser_tab_manager/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('App loads successfully', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const BrowserTabManagerApp()); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + // Verify that the app bar title is present. + expect(find.text('Browser Tab Manager'), findsOneWidget); + + // Verify that the search field is present. + expect(find.byType(TextField), findsOneWidget); }); -} +} \ No newline at end of file