This commit is contained in:
jsnk 2025-10-23 03:49:28 +02:00
parent 58d9d1f27b
commit f0354e706e
15 changed files with 1768 additions and 1103 deletions

View file

@ -0,0 +1,149 @@
<svg viewBox="0 0 1400 900" xmlns="http://www.w3.org/2000/svg">
<!-- Background -->
<rect width="1400" height="900" fill="#1a1a1a"/>
<!-- Screenshot representation -->
<rect x="50" y="50" width="600" height="400" fill="#2d2d2d" stroke="#444" stroke-width="2"/>
<!-- App Bar -->
<rect x="50" y="50" width="600" height="40" fill="#1e1e1e" stroke="#0175C2" stroke-width="2"/>
<text x="60" y="75" fill="#fff" font-size="14" font-family="Arial">Browser Tab Manager</text>
<text x="530" y="75" fill="#fff" font-size="12">▶ Track Tabs ⟳ ☰</text>
<!-- Search Bar -->
<rect x="60" y="100" width="580" height="35" fill="#333" stroke="#555" stroke-width="1" rx="5"/>
<text x="70" y="122" fill="#888" font-size="12">🔍 Search tabs, bookmarks, and history...</text>
<!-- Filter Chips -->
<rect x="60" y="145" width="80" height="25" fill="#0175C2" rx="12"/>
<text x="75" y="162" fill="#fff" font-size="11">All (0)</text>
<rect x="150" y="145" width="80" height="25" fill="#444" rx="12"/>
<text x="160" y="162" fill="#aaa" font-size="11">Tabs (0)</text>
<rect x="240" y="145" width="110" height="25" fill="#444" rx="12"/>
<text x="245" y="162" fill="#aaa" font-size="11">Bookmarks (0)</text>
<rect x="360" y="145" width="95" height="25" fill="#444" rx="12"/>
<text x="370" y="162" fill="#aaa" font-size="11">History (0)</text>
<!-- Empty State -->
<g transform="translate(250, 270)">
<circle cx="100" cy="0" r="40" fill="none" stroke="#0175C280" stroke-width="3"/>
<line x1="120" y1="15" x2="140" y2="35" stroke="#0175C280" stroke-width="3" stroke-linecap="round"/>
<line x1="115" y1="5" x2="125" y2="15" stroke="#0175C280" stroke-width="2"/>
<text x="35" y="60" fill="#fff" font-size="16" font-weight="bold">No items found</text>
<text x="15" y="80" fill="#aaa" font-size="12">Try a different search or filter</text>
</g>
<!-- Code snippets with arrows -->
<!-- AppBar code -->
<rect x="700" y="50" width="650" height="80" fill="#1e1e1e" stroke="#0175C2" stroke-width="1" rx="5"/>
<text x="710" y="70" fill="#569cd6" font-size="11" font-family="monospace">appBar: AppBar(</text>
<text x="720" y="85" fill="#fff" font-size="11" font-family="monospace">title: Row(children: [</text>
<text x="730" y="100" fill="#ce9178" font-size="11" font-family="monospace">const Text('Browser Tab Manager'),</text>
<text x="720" y="115" fill="#fff" font-size="11" font-family="monospace">]),</text>
<!-- Arrow from AppBar code to UI -->
<path d="M 700 90 L 650 70" stroke="#0175C2" stroke-width="2" fill="none" marker-end="url(#arrowblue)"/>
<!-- Search field code -->
<rect x="700" y="150" width="650" height="95" fill="#1e1e1e" stroke="#4CAF50" stroke-width="1" rx="5"/>
<text x="710" y="170" fill="#569cd6" font-size="11" font-family="monospace">TextField(</text>
<text x="720" y="185" fill="#fff" font-size="11" font-family="monospace">controller: searchController,</text>
<text x="720" y="200" fill="#fff" font-size="11" font-family="monospace">decoration: InputDecoration(</text>
<text x="730" y="215" fill="#ce9178" font-size="11" font-family="monospace">hintText: 'Search tabs, bookmarks...</text>
<text x="720" y="230" fill="#fff" font-size="11" font-family="monospace">),</text>
<!-- Arrow from Search code to UI -->
<path d="M 700 195 L 640 115" stroke="#4CAF50" stroke-width="2" fill="none" marker-end="url(#arrowgreen)"/>
<!-- Filter chips code -->
<rect x="700" y="265" width="650" height="110" fill="#1e1e1e" stroke="#FF9800" stroke-width="1" rx="5"/>
<text x="710" y="285" fill="#569cd6" font-size="11" font-family="monospace">FilterChip(</text>
<text x="720" y="300" fill="#ce9178" font-size="11" font-family="monospace">label: Text('All (${allItems.length})'),</text>
<text x="720" y="315" fill="#fff" font-size="11" font-family="monospace">selected: filterType == 'all',</text>
<text x="720" y="330" fill="#fff" font-size="11" font-family="monospace">onSelected: (selected) {</text>
<text x="730" y="345" fill="#dcdcaa" font-size="11" font-family="monospace">setState(() { filterType = 'all'; });</text>
<text x="720" y="360" fill="#fff" font-size="11" font-family="monospace">},</text>
<!-- Arrow from Filter code to UI -->
<path d="M 700 310 L 460 155" stroke="#FF9800" stroke-width="2" fill="none" marker-end="url(#arroworange)"/>
<!-- Empty state code -->
<rect x="700" y="395" width="650" height="125" fill="#1e1e1e" stroke="#9C27B0" stroke-width="1" rx="5"/>
<text x="710" y="415" fill="#569cd6" font-size="11" font-family="monospace">filteredItems.isEmpty</text>
<text x="720" y="430" fill="#fff" font-size="11" font-family="monospace">? Center(child: Column(</text>
<text x="730" y="445" fill="#fff" font-size="11" font-family="monospace">children: [</text>
<text x="740" y="460" fill="#fff" font-size="11" font-family="monospace">Icon(Icons.search_off, size: 80),</text>
<text x="740" y="475" fill="#ce9178" font-size="11" font-family="monospace">Text('No items found'),</text>
<text x="740" y="490" fill="#ce9178" font-size="11" font-family="monospace">Text('Try a different search...'),</text>
<text x="730" y="505" fill="#fff" font-size="11" font-family="monospace">],</text>
<!-- Arrow from Empty state code to UI -->
<path d="M 700 450 L 460 330" stroke="#9C27B0" stroke-width="2" fill="none" marker-end="url(#arrowpurple)"/>
<!-- State variables -->
<rect x="700" y="540" width="650" height="150" fill="#1e1e1e" stroke="#F44336" stroke-width="1" rx="5"/>
<text x="710" y="560" fill="#6A9955" font-size="11" font-family="monospace">// 🔴 STATE variables in _TabManagerHomeState:</text>
<text x="710" y="580" fill="#fff" font-size="11" font-family="monospace">List&lt;TabData&gt; allItems = [];</text>
<text x="710" y="595" fill="#fff" font-size="11" font-family="monospace">List&lt;TabData&gt; filteredItems = [];</text>
<text x="710" y="610" fill="#fff" font-size="11" font-family="monospace">TextEditingController searchController;</text>
<text x="710" y="625" fill="#fff" font-size="11" font-family="monospace">bool isGridView = true;</text>
<text x="710" y="640" fill="#fff" font-size="11" font-family="monospace">String sortBy = 'recent';</text>
<text x="710" y="655" fill="#fff" font-size="11" font-family="monospace">String filterType = 'all';</text>
<text x="710" y="670" fill="#fff" font-size="11" font-family="monospace">bool isLoading = true;</text>
<!-- Legend -->
<text x="50" y="480" fill="#fff" font-size="14" font-weight="bold">UI Elements:</text>
<line x1="50" y1="495" x2="100" y2="495" stroke="#0175C2" stroke-width="3"/>
<text x="110" y="500" fill="#fff" font-size="12">AppBar</text>
<line x1="50" y1="515" x2="100" y2="515" stroke="#4CAF50" stroke-width="3"/>
<text x="110" y="520" fill="#fff" font-size="12">Search Field</text>
<line x1="50" y1="535" x2="100" y2="535" stroke="#FF9800" stroke-width="3"/>
<text x="110" y="540" fill="#fff" font-size="12">Filter Chips</text>
<line x1="50" y1="555" x2="100" y2="555" stroke="#9C27B0" stroke-width="3"/>
<text x="110" y="560" fill="#fff" font-size="12">Empty State</text>
<line x1="50" y1="575" x2="100" y2="575" stroke="#F44336" stroke-width="3"/>
<text x="110" y="580" fill="#fff" font-size="12">State Variables</text>
<!-- Key concepts -->
<rect x="50" y="610" width="600" height="250" fill="#2d2d2d" stroke="#555" stroke-width="1" rx="5"/>
<text x="60" y="635" fill="#fff" font-size="14" font-weight="bold">Key Concepts:</text>
<text x="60" y="660" fill="#4CAF50" font-size="12" font-weight="bold">setState(() { ... })</text>
<text x="70" y="677" fill="#ddd" font-size="11">• Updates STATE variables</text>
<text x="70" y="692" fill="#ddd" font-size="11">• Triggers UI rebuild automatically</text>
<text x="60" y="717" fill="#FF9800" font-size="12" font-weight="bold">StatefulWidget</text>
<text x="70" y="734" fill="#ddd" font-size="11">• _TabManagerHomeState holds all STATE</text>
<text x="70" y="749" fill="#ddd" font-size="11">• When STATE changes, UI updates</text>
<text x="60" y="774" fill="#9C27B0" font-size="12" font-weight="bold">StatelessWidget</text>
<text x="70" y="791" fill="#ddd" font-size="11">• ItemCard and ItemListTile receive data</text>
<text x="70" y="806" fill="#ddd" font-size="11">• Display data but don't manage STATE</text>
<text x="60" y="831" fill="#0175C2" font-size="12" font-weight="bold">Data Flow</text>
<text x="70" y="848" fill="#ddd" font-size="11">• User action → function called → setState → STATE changes → UI rebuilds</text>
<!-- Arrow markers -->
<defs>
<marker id="arrowblue" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#0175C2"/>
</marker>
<marker id="arrowgreen" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#4CAF50"/>
</marker>
<marker id="arroworange" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#FF9800"/>
</marker>
<marker id="arrowpurple" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#9C27B0"/>
</marker>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -0,0 +1,595 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TabData Complete Breakdown</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
padding: 20px;
line-height: 1.8;
font-size: 17px;
}
.container {
max-width: 1300px;
margin: 0 auto;
}
h1 {
text-align: center;
color: white;
font-size: 2.5em;
margin-bottom: 30px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.section {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 25px;
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
.section-title {
font-size: 2em;
color: #0175C2;
margin-bottom: 20px;
border-bottom: 4px solid #0175C2;
padding-bottom: 10px;
}
.code {
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
border-radius: 8px;
font-family: 'Courier New', monospace;
overflow-x: auto;
margin: 20px 0;
line-height: 1.6;
font-size: 15px;
}
.keyword { color: #569cd6; font-weight: bold; }
.string { color: #ce9178; }
.comment { color: #6a9955; }
.class { color: #4ec9b0; }
.function { color: #dcdcaa; }
.number { color: #b5cea8; }
.line-box {
background: #f8f9fa;
border-left: 6px solid #0175C2;
padding: 25px;
margin: 25px 0;
border-radius: 8px;
}
.line-number {
background: #0175C2;
color: white;
padding: 6px 15px;
border-radius: 20px;
font-weight: bold;
display: inline-block;
margin-bottom: 15px;
font-size: 16px;
}
.part-title {
font-size: 1.5em;
color: #0175C2;
margin: 20px 0 15px 0;
font-weight: bold;
}
.info-box {
background: #e3f2fd;
border-left: 5px solid #2196f3;
padding: 20px;
margin: 20px 0;
border-radius: 6px;
}
.important-box {
background: #fff3cd;
border-left: 5px solid #ffc107;
padding: 20px;
margin: 20px 0;
border-radius: 6px;
}
.connection-diagram {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 12px;
margin: 30px 0;
}
.arrow {
text-align: center;
font-size: 2.5em;
color: #0175C2;
margin: 15px 0;
font-weight: bold;
}
.example-box {
background: #e8f5e9;
border: 2px solid #4caf50;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.inline-code {
background: #f5f5f5;
padding: 3px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
color: #d63384;
font-size: 0.95em;
}
.three-parts {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
margin: 30px 0;
}
.part-box {
background: white;
border: 3px solid #0175C2;
padding: 20px;
border-radius: 12px;
text-align: center;
}
.part-box h3 {
color: #0175C2;
margin-bottom: 15px;
font-size: 1.3em;
}
.flow-step {
background: white;
border: 2px solid #0175C2;
border-radius: 8px;
padding: 20px;
margin: 15px 0;
}
.highlight {
background: #fff3cd;
padding: 3px 8px;
border-radius: 4px;
font-weight: bold;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th {
background: #0175C2;
color: white;
padding: 15px;
text-align: left;
}
td {
padding: 15px;
border-bottom: 1px solid #ddd;
background: white;
}
tr:hover td {
background: #f8f9fa;
}
@media (max-width: 900px) {
.three-parts {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>📚 Complete TabData Breakdown</h1>
<!-- SECTION 1: The Three Main Parts -->
<div class="section">
<h2 class="section-title">🎯 The Three Main Parts</h2>
<p style="font-size: 1.2em; margin-bottom: 25px;">
Your TabData class has <strong>THREE</strong> main sections. Let's understand what each one is called and what it does:
</p>
<div class="three-parts">
<div class="part-box" style="border-color: #f44336;">
<h3>Part 1</h3>
<h4 style="color: #f44336; margin: 15px 0;">CLASS DEFINITION</h4>
<div class="code" style="font-size: 13px; text-align: left;">
<span class="keyword">class</span> <span class="class">TabData</span> {
<span class="class">String</span> id;
<span class="class">String</span> title;
<span class="comment">// ... properties</span>
}</div>
<p style="margin-top: 15px;">Declares the class and its <strong>properties</strong> (variables)</p>
</div>
<div class="part-box" style="border-color: #4caf50;">
<h3>Part 2</h3>
<h4 style="color: #4caf50; margin: 15px 0;">REGULAR CONSTRUCTOR</h4>
<div class="code" style="font-size: 13px; text-align: left;">
<span class="class">TabData</span>({
<span class="keyword">required this</span>.id,
<span class="comment">// ... parameters</span>
})</div>
<p style="margin-top: 15px;">The <strong>default way</strong> to create TabData objects</p>
</div>
<div class="part-box" style="border-color: #2196f3;">
<h3>Part 3</h3>
<h4 style="color: #2196f3; margin: 15px 0;">FACTORY CONSTRUCTOR</h4>
<div class="code" style="font-size: 13px; text-align: left;">
<span class="keyword">factory</span> <span class="class">TabData</span>.<span class="function">fromJson</span>(
<span class="class">Map</span> json
) => <span class="class">TabData</span>(...)</div>
<p style="margin-top: 15px;">A <strong>special way</strong> to create TabData from JSON</p>
</div>
</div>
<div class="connection-diagram">
<h3 style="margin-bottom: 20px; font-size: 1.5em;">🔗 How They Connect:</h3>
<div class="flow-step">
<strong>1. Class Definition</strong> defines WHAT a TabData object IS
<div style="margin-top: 10px; padding-left: 20px;">
→ "A TabData has an id, title, url, etc."
</div>
</div>
<div class="arrow">⬇️</div>
<div class="flow-step">
<strong>2. Regular Constructor</strong> creates TabData objects manually
<div style="margin-top: 10px; padding-left: 20px;">
<span class="inline-code" style="background: #555;">TabData(id: '123', title: 'Test', ...)</span>
</div>
</div>
<div class="arrow">⬇️</div>
<div class="flow-step">
<strong>3. Factory Constructor</strong> ALSO creates TabData objects, but from JSON
<div style="margin-top: 10px; padding-left: 20px;">
<span class="inline-code" style="background: #555;">TabData.fromJson({'id': '123', 'title': 'Test'})</span>
</div>
<div style="margin-top: 10px; padding-left: 20px;">
→ Internally, it calls the Regular Constructor!
</div>
</div>
</div>
</div>
<!-- SECTION 2: Part 1 - Class Definition -->
<div class="section">
<h2 class="section-title">Part 1⃣: Class Definition & Properties</h2>
<div class="line-box">
<div class="line-number">Line 1</div>
<div class="code">
<span class="keyword">class</span> <span class="class">TabData</span> {</div>
<div class="part-title">🔍 Breaking It Down:</div>
<ul style="padding-left: 25px; margin-top: 10px;">
<li><span class="inline-code">class</span> - Keyword that declares a new class</li>
<li><span class="inline-code">TabData</span> - The name of our class</li>
<li><span class="inline-code">{</span> - Opening brace, starts the class body</li>
</ul>
<div class="info-box" style="margin-top: 20px;">
<strong>What is a class?</strong> A class is a <strong>blueprint</strong> or <strong>template</strong> for creating objects. Like a cookie cutter - the class is the cutter, and each TabData object is a cookie made from it.
</div>
<div class="example-box">
<strong>📝 Official Name:</strong> "Class Declaration"
</div>
</div>
<div class="line-box">
<div class="line-number">Lines 2-10</div>
<div class="code">
<span class="class">String</span> id;
<span class="class">String</span> title;
<span class="class">String</span> url;
<span class="class">String</span> favicon;
<span class="class">DateTime</span> lastAccessed;
<span class="keyword">bool</span> isPinned;
<span class="class">String</span> type;
<span class="keyword">int</span>? visitCount;
<span class="class">String</span>? folder;</div>
<div class="part-title">🔍 What These Are:</div>
<p>These are called <strong>properties</strong>, <strong>fields</strong>, or <strong>instance variables</strong>.</p>
<p style="margin-top: 15px;"><strong>They define what data each TabData object will store.</strong></p>
<table style="margin-top: 20px;">
<tr>
<th>Line</th>
<th>Property Name</th>
<th>Type</th>
<th>Can be null?</th>
</tr>
<tr>
<td>2</td>
<td><code>id</code></td>
<td>String</td>
<td>❌ No</td>
</tr>
<tr>
<td>3</td>
<td><code>title</code></td>
<td>String</td>
<td>❌ No</td>
</tr>
<tr>
<td>4</td>
<td><code>url</code></td>
<td>String</td>
<td>❌ No</td>
</tr>
<tr>
<td>5</td>
<td><code>favicon</code></td>
<td>String</td>
<td>❌ No</td>
</tr>
<tr>
<td>6</td>
<td><code>lastAccessed</code></td>
<td>DateTime</td>
<td>❌ No</td>
</tr>
<tr>
<td>7</td>
<td><code>isPinned</code></td>
<td>bool</td>
<td>❌ No</td>
</tr>
<tr>
<td>8</td>
<td><code>type</code></td>
<td>String</td>
<td>❌ No</td>
</tr>
<tr>
<td>9</td>
<td><code>visitCount</code></td>
<td>int<strong>?</strong></td>
<td>✅ Yes (the <strong>?</strong> means nullable)</td>
</tr>
<tr>
<td>10</td>
<td><code>folder</code></td>
<td>String<strong>?</strong></td>
<td>✅ Yes (the <strong>?</strong> means nullable)</td>
</tr>
</table>
<div class="example-box">
<strong>📝 Official Name:</strong> "Instance Variables" or "Properties" or "Fields"
</div>
</div>
</div>
<!-- SECTION 3: Part 2 - Regular Constructor -->
<div class="section">
<h2 class="section-title">Part 2⃣: Regular Constructor</h2>
<div class="line-box">
<div class="line-number">Line 12</div>
<div class="code">
<span class="class">TabData</span>({</div>
<div class="part-title">🔍 What This Is:</div>
<ul style="padding-left: 25px; margin-top: 10px;">
<li><span class="inline-code">TabData</span> - Constructor name (same as class name)</li>
<li><span class="inline-code">(</span> - Opens parameter list</li>
<li><span class="inline-code">{</span> - Curly brace means "named parameters"</li>
</ul>
<div class="important-box" style="margin-top: 20px;">
<strong>🎯 This is called:</strong> "Default Constructor" or "Regular Constructor" or "Named Parameter Constructor"
</div>
<div class="info-box">
<strong>What does a constructor do?</strong> A constructor is a special method that <strong>creates and initializes</strong> new objects. When you write <span class="inline-code">TabData(...)</span>, this constructor runs.
</div>
</div>
<div class="line-box">
<div class="line-number">Lines 13-21</div>
<div class="code">
<span class="keyword">required this</span>.id, <span class="comment">// Line 13</span>
<span class="keyword">required this</span>.title, <span class="comment">// Line 14</span>
<span class="keyword">required this</span>.url, <span class="comment">// Line 15</span>
<span class="keyword">this</span>.favicon = <span class="string">''</span>, <span class="comment">// Line 16</span>
<span class="class">DateTime</span>? lastAccessed, <span class="comment">// Line 17</span>
<span class="keyword">this</span>.isPinned = <span class="keyword">false</span>, <span class="comment">// Line 18</span>
<span class="keyword">this</span>.type = <span class="string">'tab'</span>, <span class="comment">// Line 19</span>
<span class="keyword">this</span>.visitCount, <span class="comment">// Line 20</span>
<span class="keyword">this</span>.folder, <span class="comment">// Line 21</span></div>
<div class="part-title">🔍 Each Line Explained:</div>
<div style="background: #ffebee; padding: 15px; border-radius: 6px; margin: 15px 0;">
<strong>Lines 13-15:</strong> <span class="inline-code">required this.id</span>
<ul style="padding-left: 25px; margin-top: 10px;">
<li><span class="inline-code">required</span> = MUST be provided</li>
<li><span class="inline-code">this.id</span> = assign parameter value to the <span class="inline-code">id</span> property</li>
</ul>
</div>
<div style="background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0;">
<strong>Line 16:</strong> <span class="inline-code">this.favicon = ''</span>
<ul style="padding-left: 25px; margin-top: 10px;">
<li>Optional parameter (no <span class="inline-code">required</span>)</li>
<li>Has default value: empty string <span class="inline-code">''</span></li>
<li>If not provided → uses <span class="inline-code">''</span></li>
</ul>
</div>
<div style="background: #fff3e0; padding: 15px; border-radius: 6px; margin: 15px 0;">
<strong>Line 17:</strong> <span class="inline-code">DateTime? lastAccessed</span>
<ul style="padding-left: 25px; margin-top: 10px;">
<li><strong>NOT</strong> <span class="inline-code">this.lastAccessed</span> - just the parameter name!</li>
<li>The <span class="inline-code">?</span> means it can be null</li>
<li>This is handled specially in the initializer list (line 22)</li>
</ul>
</div>
<div style="background: #e1f5fe; padding: 15px; border-radius: 6px; margin: 15px 0;">
<strong>Lines 18-19:</strong> <span class="inline-code">this.isPinned = false</span>
<ul style="padding-left: 25px; margin-top: 10px;">
<li>Optional with defaults</li>
<li><span class="inline-code">isPinned</span> defaults to <span class="inline-code">false</span></li>
<li><span class="inline-code">type</span> defaults to <span class="inline-code">'tab'</span></li>
</ul>
</div>
<div style="background: #f3e5f5; padding: 15px; border-radius: 6px; margin: 15px 0;">
<strong>Lines 20-21:</strong> <span class="inline-code">this.visitCount</span>
<ul style="padding-left: 25px; margin-top: 10px;">
<li>Optional, no default value</li>
<li>If not provided → becomes <span class="inline-code">null</span></li>
</ul>
</div>
<div class="example-box">
<strong>📝 Official Name:</strong> "Constructor Parameters"
</div>
</div>
<div class="line-box" style="background: #fff3e0; border-color: #ff9800;">
<div class="line-number" style="background: #ff9800;">Line 22</div>
<div class="code">
}) : lastAccessed = lastAccessed ?? <span class="class">DateTime</span>.<span class="function">now</span>();</div>
<div class="part-title" style="color: #e65100;">🔍 The Most Important Line!</div>
<p style="font-size: 1.1em; margin: 15px 0;">Let's break this into 4 parts:</p>
<div style="background: white; padding: 20px; border-radius: 8px; margin: 15px 0;">
<h4 style="color: #ff9800;">Part 1: <span class="inline-code">}</span></h4>
<p>Closes the parameter list</p>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; margin: 15px 0;">
<h4 style="color: #ff9800;">Part 2: <span class="inline-code">:</span></h4>
<p>Starts the <strong>initializer list</strong></p>
<p style="margin-top: 10px;">The initializer list runs <strong>BEFORE</strong> the object is created. It's for special initialization logic.</p>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; margin: 15px 0;">
<h4 style="color: #ff9800;">Part 3: <span class="inline-code">lastAccessed = lastAccessed</span></h4>
<p>Left side: <span class="inline-code">lastAccessed</span> → the property (same as <span class="inline-code">this.lastAccessed</span>)</p>
<p>Right side: <span class="inline-code">lastAccessed</span> → the parameter from line 17</p>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; margin: 15px 0;">
<h4 style="color: #ff9800;">Part 4: <span class="inline-code">?? DateTime.now()</span></h4>
<p><span class="inline-code">??</span> = "if null, use this instead"</p>
<p><span class="inline-code">DateTime.now()</span> = current date/time</p>
</div>
<div class="important-box" style="margin-top: 20px;">
<h4>🎯 In Plain English:</h4>
<p style="font-size: 1.1em; line-height: 2; margin-top: 10px;">
"Set <span class="inline-code">this.lastAccessed</span> (the property) to the value of <span class="inline-code">lastAccessed</span> (the parameter),
BUT if the parameter is null, use <span class="inline-code">DateTime.now()</span> instead"
</p>
</div>
<div class="example-box">
<strong>📝 Official Names:</strong>
<ul style="padding-left: 25px; margin-top: 10px;">
<li>The <span class="inline-code">:</span> part is called an <strong>"Initializer List"</strong></li>
<li>The <span class="inline-code">??</span> operator is called the <strong>"Null-Coalescing Operator"</strong></li>
</ul>
</div>
</div>
</div>
<!-- SECTION 4: Part 3 - Factory Constructor -->
<div class="section">
<h2 class="section-title">Part 3⃣: Factory Constructor (fromJson)</h2>
<div class="line-box">
<div class="line-number">Line 25</div>
<div class="code">
<span class="keyword">factory</span> <span class="class">TabData</span>.<span class="function">fromJson</span>(<span class="class">Map</span>&lt;<span class="class">String</span>, <span class="keyword">dynamic</span>&gt; json) => <span class="class">TabData</span>(</div>
<div class="part-title">🔍 Breaking Down Each Part:</div>
<div style="background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0;">
<h4><span class="inline-code">factory</span></h4>
<p>Keyword that makes this a <strong>factory constructor</strong></p>
<p style="margin-top: 10px;"><strong>What's special about factory?</strong> 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).</p>
</div>
<div style="background: #e3f2fd; padding: 15px; border-radius: 6px; margin: 15px 0;">
<h4><span class="inline-code">TabData.fromJson</span></h4>
<p>This is a <strong>named constructor</strong></p>
<ul style="padding-left: 25px; margin-top: 10px;">
<li><span class="inline-code">TabData</span> = class name</li>
<li><span class="inline-code">.</span> = dot separator</li>
<li><span class="inline-code">fromJson</span> = name we chose for this constructor</li>
</ul>
<p style="margin-top: 15px;"><strong>Why "fromJson"?</strong> Because this constructor creates a TabData object FROM JSON data. You can have multiple constructors with different names!</p>
</div>
<div style="background: #fff3e0; padding: 15px; border-radius: 6px; margin: 15px 0;">
<h4><span class="inline-code">Map&lt;String, dynamic&gt; json</span></h4>
<p>This is the parameter - a Map (like a dictionary)</p>
<ul style="padding-left: 25px; margin-top: 10px;">
<li><span class="inline-code">Map</span> = data type (key-value pairs)</li>
<li><span class="inline-code">&lt;String, dynamic&gt;</span> = keys are Strings, values can be anything</li>
<li><span class="inline-code">json</span> = parameter name</li>
</ul>
</div>
<div style="background: #f3e5f5; padding: 15px; border-radius: 6px; margin: 15px 0;">
<h4><span class="inline-code">=> TabData(</span></h4>
<p><span class="inline-code">=></span> is called an "arrow function" or "expression body"</p>
<p style="margin-top: 10px;"><strong>Meaning:</strong> "return the result of <span class="inline-code">TabData(...)</span>"</p>
<p style="margin-top: 10px;">This factory constructor calls the regular constructor (Part 2) and returns the result!</p>
</div>
<div class="example-box">
<strong>📝 Official Name:</strong> "Factory Constructor" or "Named Factory Constructor"
</div>
</div>
<div class="line-box">
<div class="line-number">Lines 26-37</div>
<h3 style="margin-bottom: 15px;">Inside the Factory - Extracting Data from JSON</h3>
<p>Each line extracts data from the <span class="inline-code">json</span> Map and passes it to the regular constructor:</p>
<div class="code">
id: json[<span class="string">'id'</span>].<span class="function">toString</span>(), <span class="comment">// Line 26</span>
title: json[<span class="string">'title'</span>] ?? <span class="string">'Untitled'</span>, <span class="comment">// Line 27</span>
url: json[<span class="string">'url'</span>] ?? <span class="string">''</span>,

View file

@ -0,0 +1,960 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TabData Visual Explanation</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: #333;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
text-align: center;
color: white;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.section {
background: white;
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
.section-title {
font-size: 1.8em;
color: #0175C2;
margin-bottom: 20px;
border-bottom: 3px solid #0175C2;
padding-bottom: 10px;
}
.blueprint {
background: #f8f9fa;
border: 2px dashed #0175C2;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.property {
display: flex;
align-items: center;
margin: 12px 0;
padding: 12px;
background: white;
border-radius: 6px;
border-left: 4px solid #0175C2;
transition: transform 0.2s;
}
.property:hover {
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(1,117,194,0.2);
}
.property-name {
font-weight: bold;
color: #0175C2;
min-width: 140px;
font-family: 'Courier New', monospace;
}
.property-type {
background: #e3f2fd;
color: #1976d2;
padding: 4px 10px;
border-radius: 4px;
font-size: 0.85em;
font-weight: bold;
margin: 0 10px;
}
.property-desc {
color: #555;
flex: 1;
}
.example-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 15px;
font-size: 1.2em;
font-weight: bold;
}
.card-icon {
font-size: 2em;
margin-right: 15px;
}
.card-data {
background: rgba(255,255,255,0.1);
border-radius: 6px;
padding: 15px;
margin-top: 10px;
}
.card-data-item {
margin: 8px 0;
font-family: 'Courier New', monospace;
}
.card-data-label {
color: #ffd700;
font-weight: bold;
}
.constructor-flow {
display: flex;
align-items: center;
justify-content: space-around;
margin: 20px 0;
flex-wrap: wrap;
}
.flow-step {
background: #f0f7ff;
border: 2px solid #0175C2;
border-radius: 8px;
padding: 15px 20px;
margin: 10px;
text-align: center;
position: relative;
min-width: 200px;
}
.flow-arrow {
font-size: 2em;
color: #0175C2;
margin: 0 10px;
}
.code-block {
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
border-radius: 8px;
font-family: 'Courier New', monospace;
overflow-x: auto;
margin: 15px 0;
position: relative;
}
.code-comment {
color: #6a9955;
}
.code-keyword {
color: #569cd6;
}
.code-string {
color: #ce9178;
}
.code-class {
color: #4ec9b0;
}
.json-example {
background: #2d2d2d;
padding: 20px;
border-radius: 8px;
color: #d4d4d4;
font-family: 'Courier New', monospace;
margin: 15px 0;
}
.json-key {
color: #9cdcfe;
}
.json-value {
color: #ce9178;
}
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin: 20px 0;
}
.comparison-item {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
}
.comparison-title {
font-weight: bold;
margin-bottom: 10px;
font-size: 1.1em;
}
.required-badge {
background: #f44336;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75em;
margin-left: 8px;
}
.optional-badge {
background: #4caf50;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75em;
margin-left: 8px;
}
.visual-flow {
background: linear-gradient(to right, #f8f9fa, white, #f8f9fa);
padding: 30px;
border-radius: 8px;
margin: 20px 0;
}
.highlight {
background: #fff3cd;
padding: 2px 6px;
border-radius: 3px;
font-weight: bold;
}
@media (max-width: 768px) {
.comparison {
grid-template-columns: 1fr;
}
.constructor-flow {
flex-direction: column;
}
.flow-arrow {
transform: rotate(90deg);
}
}
</style>
</head>
<body>
<div class="container">
<h1>📊 TabData Class - Visual Guide</h1>
<!-- SECTION 1: What is TabData? -->
<div class="section">
<h2 class="section-title">🎯 What is TabData?</h2>
<p style="font-size: 1.1em; margin-bottom: 20px;">
<strong>TabData</strong> is a <span class="highlight">blueprint</span> (or template) that defines how we store information about tabs, bookmarks, and history items.
</p>
<div class="example-card">
<div class="card-header">
<span class="card-icon">🔖</span>
<span>Example: A YouTube Tab</span>
</div>
<div class="card-data">
<div class="card-data-item"><span class="card-data-label">id:</span> "tab_123"</div>
<div class="card-data-item"><span class="card-data-label">title:</span> "Flutter - YouTube"</div>
<div class="card-data-item"><span class="card-data-label">url:</span> "https://youtube.com"</div>
<div class="card-data-item"><span class="card-data-label">favicon:</span> "https://youtube.com/favicon.ico"</div>
<div class="card-data-item"><span class="card-data-label">lastAccessed:</span> 2025-10-20 14:30:00</div>
<div class="card-data-item"><span class="card-data-label">isPinned:</span> true</div>
<div class="card-data-item"><span class="card-data-label">type:</span> "tab"</div>
<div class="card-data-item"><span class="card-data-label">visitCount:</span> 47</div>
<div class="card-data-item"><span class="card-data-label">folder:</span> null</div>
</div>
</div>
<p style="margin-top: 20px;">
Every tab, bookmark, or history item in your app is stored as a <strong>TabData object</strong> containing this information.
</p>
</div>
<!-- SECTION 2: The Properties -->
<div class="section">
<h2 class="section-title">📋 The Properties (Variables)</h2>
<p style="margin-bottom: 15px;">These are the pieces of information each TabData object holds:</p>
<div class="blueprint">
<div class="property">
<span class="property-name">String id</span>
<span class="property-type">Text</span>
<span class="property-desc">Unique identifier (like a social security number for the tab)</span>
<span class="required-badge">REQUIRED</span>
</div>
<div class="property">
<span class="property-name">String title</span>
<span class="property-type">Text</span>
<span class="property-desc">The page title (e.g., "Google")</span>
<span class="required-badge">REQUIRED</span>
</div>
<div class="property">
<span class="property-name">String url</span>
<span class="property-type">Text</span>
<span class="property-desc">Web address (e.g., "https://google.com")</span>
<span class="required-badge">REQUIRED</span>
</div>
<div class="property">
<span class="property-name">String favicon</span>
<span class="property-type">Text</span>
<span class="property-desc">The small icon for the website (defaults to empty '')</span>
<span class="optional-badge">OPTIONAL</span>
</div>
<div class="property">
<span class="property-name">DateTime lastAccessed</span>
<span class="property-type">Date/Time</span>
<span class="property-desc">When you last opened this (defaults to now)</span>
<span class="optional-badge">OPTIONAL</span>
</div>
<div class="property">
<span class="property-name">bool isPinned</span>
<span class="property-type">True/False</span>
<span class="property-desc">Is the tab pinned? (defaults to false)</span>
<span class="optional-badge">OPTIONAL</span>
</div>
<div class="property">
<span class="property-name">String type</span>
<span class="property-type">Text</span>
<span class="property-desc">What kind: 'tab', 'bookmark', or 'history' (defaults to 'tab')</span>
<span class="optional-badge">OPTIONAL</span>
</div>
<div class="property">
<span class="property-name">int? visitCount</span>
<span class="property-type">Number?</span>
<span class="property-desc">How many times visited (can be null)</span>
<span class="optional-badge">OPTIONAL</span>
</div>
<div class="property">
<span class="property-name">String? folder</span>
<span class="property-type">Text?</span>
<span class="property-desc">Bookmark folder name (can be null)</span>
<span class="optional-badge">OPTIONAL</span>
</div>
</div>
<div style="background: #e3f2fd; padding: 15px; border-radius: 6px; margin-top: 20px;">
<strong>💡 Note:</strong> The <code>?</code> after a type (like <code>int?</code> or <code>String?</code>) means the value can be <strong>null</strong> (empty/missing).
</div>
</div>
<!-- SECTION 3: The Constructor -->
<div class="section">
<h2 class="section-title">🏗️ The Constructor - Creating TabData Objects</h2>
<p style="margin-bottom: 15px;">The constructor is like a <strong>factory that builds TabData objects</strong>.</p>
<div class="visual-flow">
<h3 style="margin-bottom: 15px;">How to Create a TabData:</h3>
<div class="code-block">
<span class="code-comment">// Creating a new TabData object</span>
<span class="code-keyword">final</span> myTab = <span class="code-class">TabData</span>(
id: <span class="code-string">'tab_456'</span>, <span class="code-comment">// REQUIRED ✅</span>
title: <span class="code-string">'Flutter Docs'</span>, <span class="code-comment">// REQUIRED ✅</span>
url: <span class="code-string">'https://flutter.dev'</span>, <span class="code-comment">// REQUIRED ✅</span>
favicon: <span class="code-string">'https://flutter.dev/icon.png'</span>, <span class="code-comment">// Optional</span>
isPinned: <span class="code-keyword">true</span>, <span class="code-comment">// Optional (defaults to false)</span>
visitCount: <span class="code-string">23</span>, <span class="code-comment">// Optional (can be null)</span>
);
</div>
</div>
<div class="constructor-flow">
<div class="flow-step">
<strong>📝 You provide data</strong>
<div style="margin-top: 10px; font-size: 0.9em;">
id, title, url<br/>
+ optional fields
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<strong>🏭 Constructor runs</strong>
<div style="margin-top: 10px; font-size: 0.9em;">
Fills in defaults<br/>
Sets lastAccessed
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<strong>✨ TabData object created</strong>
<div style="margin-top: 10px; font-size: 0.9em;">
Ready to use!
</div>
</div>
</div>
<div style="background: #fff3cd; padding: 15px; border-radius: 6px; margin-top: 20px;">
<strong>🎯 Key Feature:</strong> The line <code>: lastAccessed = lastAccessed ?? DateTime.now()</code> means:
<br/><br/>
"If lastAccessed is provided, use it. Otherwise (<code>??</code>), use the current time (<code>DateTime.now()</code>)."
</div>
</div>
<!-- SECTION 4: The fromJson Factory -->
<div class="section">
<h2 class="section-title">🔄 The fromJson Factory - Converting JSON to TabData</h2>
<p style="margin-bottom: 15px;">
When we get data from the browser API, it comes as <strong>JSON</strong> (a text format).
The <code>fromJson</code> method converts that JSON into a TabData object.
</p>
<div class="comparison">
<div class="comparison-item">
<div class="comparison-title" style="color: #f44336;">❌ JSON (from browser)</div>
<div class="json-example">
{
<span class="json-key">"id"</span>: <span class="json-value">"tab_789"</span>,
<span class="json-key">"title"</span>: <span class="json-value">"GitHub"</span>,
<span class="json-key">"url"</span>: <span class="json-value">"https://github.com"</span>,
<span class="json-key">"favIconUrl"</span>: <span class="json-value">"..."</span>,
<span class="json-key">"lastVisitTime"</span>: <span class="json-value">"2025-10-20T..."</span>
}
</div>
<p style="margin-top: 10px; font-size: 0.9em;">
This is just text - we can't use it directly!
</p>
</div>
<div class="comparison-item">
<div class="comparison-title" style="color: #4caf50;">✅ TabData Object (in our app)</div>
<div class="code-block">
<span class="code-class">TabData</span> {
id: <span class="code-string">"tab_789"</span>
title: <span class="code-string">"GitHub"</span>
url: <span class="code-string">"https://github.com"</span>
favicon: <span class="code-string">"..."</span>
lastAccessed: DateTime
isPinned: <span class="code-keyword">false</span>
...
}
</div>
<p style="margin-top: 10px; font-size: 0.9em;">
This is a proper object we can work with!
</p>
</div>
</div>
<div class="visual-flow" style="margin-top: 30px;">
<h3>How fromJson Works:</h3>
<div class="code-block">
<span class="code-comment">// Usage:</span>
<span class="code-keyword">final</span> jsonData = {<span class="code-string">'id'</span>: <span class="code-string">'123'</span>, <span class="code-string">'title'</span>: <span class="code-string">'Test'</span>, <span class="code-string">'url'</span>: <span class="code-string">'...'</span>};
<span class="code-keyword">final</span> tab = <span class="code-class">TabData</span>.fromJson(jsonData);
<span class="code-comment">// The method does this:</span>
id: json[<span class="code-string">'id'</span>].toString() <span class="code-comment">// Get 'id' from JSON</span>
title: json[<span class="code-string">'title'</span>] ?? <span class="code-string">'Untitled'</span> <span class="code-comment">// Get 'title', or use 'Untitled' if missing</span>
</div>
</div>
</div>
<!-- SECTION 5: Handling Different Date Fields -->
<div class="section">
<h2 class="section-title">📅 Smart Date Handling</h2>
<p style="margin-bottom: 15px;">
Different browser APIs send dates with <strong>different field names</strong>.
Our fromJson method is smart enough to handle all of them!
</p>
<div class="code-block">
<span class="code-comment">// The code checks multiple possible date fields:</span>
lastAccessed: json[<span class="code-string">'lastAccessed'</span>] != <span class="code-keyword">null</span>
? DateTime.parse(json[<span class="code-string">'lastAccessed'</span>]) <span class="code-comment">// Try 'lastAccessed' first</span>
: (json[<span class="code-string">'lastVisitTime'</span>] != <span class="code-keyword">null</span>
? DateTime.parse(json[<span class="code-string">'lastVisitTime'</span>]) <span class="code-comment">// If not, try 'lastVisitTime'</span>
: (json[<span class="code-string">'dateAdded'</span>] != <span class="code-keyword">null</span>
? DateTime.parse(json[<span class="code-string">'dateAdded'</span>]) <span class="code-comment">// If not, try 'dateAdded'</span>
: (json[<span class="code-string">'timestamp'</span>] != <span class="code-keyword">null</span>
? DateTime.parse(json[<span class="code-string">'timestamp'</span>]) <span class="code-comment">// If not, try 'timestamp'</span>
: DateTime.now()))) <span class="code-comment">// If none exist, use current time</span>
</div>
<div class="constructor-flow" style="margin-top: 30px;">
<div class="flow-step" style="background: #e8f5e9;">
<strong>Try 1:</strong><br/>
lastAccessed?
</div>
<div class="flow-arrow"></div>
<div class="flow-step" style="background: #fff3cd;">
<strong>Try 2:</strong><br/>
lastVisitTime?
</div>
<div class="flow-arrow"></div>
<div class="flow-step" style="background: #ffe0b2;">
<strong>Try 3:</strong><br/>
dateAdded?
</div>
<div class="flow-arrow"></div>
<div class="flow-step" style="background: #ffcdd2;">
<strong>Try 4:</strong><br/>
timestamp?
</div>
<div class="flow-arrow"></div>
<div class="flow-step" style="background: #f3e5f5;">
<strong>Fallback:</strong><br/>
Use now!
</div>
</div>
<div style="background: #e3f2fd; padding: 15px; border-radius: 6px; margin-top: 20px;">
<strong>💡 Why?</strong> Different browsers and APIs use different names for the same thing.
This ensures our app works with all of them!
</div>
</div>
<!-- SECTION 6: The ?? Operator -->
<div class="section">
<h2 class="section-title">🛡️ The ?? Operator - Providing Defaults</h2>
<p style="margin-bottom: 15px;">
The <code>??</code> operator means: <strong>"if null (missing), use this default value instead"</strong>
</p>
<div class="comparison">
<div class="comparison-item">
<div class="comparison-title">Without ?? (Would Crash)</div>
<div class="code-block">
title: json[<span class="code-string">'title'</span>]
<span class="code-comment">// If 'title' is missing → ERROR! 💥</span>
</div>
</div>
<div class="comparison-item">
<div class="comparison-title">With ?? (Safe)</div>
<div class="code-block">
title: json[<span class="code-string">'title'</span>] ?? <span class="code-string">'Untitled'</span>
<span class="code-comment">// If 'title' is missing → use 'Untitled' ✅</span>
</div>
</div>
</div>
<div style="margin-top: 20px;">
<h3 style="margin-bottom: 15px;">All the defaults in fromJson:</h3>
<div class="property">
<code style="background: #f5f5f5; padding: 4px 8px; border-radius: 4px;">
json['title'] ?? 'Untitled'
</code>
<span style="margin-left: 15px;">→ If no title, use "Untitled"</span>
</div>
<div class="property">
<code style="background: #f5f5f5; padding: 4px 8px; border-radius: 4px;">
json['url'] ?? ''
</code>
<span style="margin-left: 15px;">→ If no URL, use empty string</span>
</div>
<div class="property">
<code style="background: #f5f5f5; padding: 4px 8px; border-radius: 4px;">
json['favicon'] ?? json['favIconUrl'] ?? ''
</code>
<span style="margin-left: 15px;">→ Try 'favicon', then 'favIconUrl', then empty</span>
</div>
<div class="property">
<code style="background: #f5f5f5; padding: 4px 8px; border-radius: 4px;">
json['isPinned'] ?? false
</code>
<span style="margin-left: 15px;">→ If not specified, assume not pinned</span>
</div>
<div class="property">
<code style="background: #f5f5f5; padding: 4px 8px; border-radius: 4px;">
json['type'] ?? 'tab'
</code>
<span style="margin-left: 15px;">→ If no type, assume it's a tab</span>
</div>
</div>
</div>
<!-- SECTION 7: Real-World Example -->
<div class="section">
<h2 class="section-title">🌍 Real-World Example</h2>
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 12px; color: white;">
<h3 style="margin-bottom: 20px;">📱 What happens when you open a tab:</h3>
<div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 8px; margin: 15px 0;">
<strong>Step 1:</strong> Browser sends JSON data
<div class="json-example" style="margin-top: 10px;">
{
<span class="json-key">"id"</span>: <span class="json-value">"tab_999"</span>,
<span class="json-key">"title"</span>: <span class="json-value">"Claude AI"</span>,
<span class="json-key">"url"</span>: <span class="json-value">"https://claude.ai"</span>,
<span class="json-key">"favIconUrl"</span>: <span class="json-value">"https://claude.ai/icon.png"</span>,
<span class="json-key">"lastVisitTime"</span>: <span class="json-value">"2025-10-20T14:30:00"</span>,
<span class="json-key">"visitCount"</span>: <span class="json-value">15</span>
}
</div>
</div>
<div style="text-align: center; font-size: 2em; margin: 20px 0;">⬇️</div>
<div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 8px; margin: 15px 0;">
<strong>Step 2:</strong> fromJson converts it
<div class="code-block" style="margin-top: 10px;">
<span class="code-keyword">final</span> tab = <span class="code-class">TabData</span>.fromJson(browserData);
</div>
</div>
<div style="text-align: center; font-size: 2em; margin: 20px 0;">⬇️</div>
<div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 8px;">
<strong>Step 3:</strong> We have a usable TabData object!
<div style="margin-top: 15px; font-family: 'Courier New', monospace;">
tab.id → "tab_999"<br/>
tab.title → "Claude AI"<br/>
tab.url → "https://claude.ai"<br/>
tab.favicon → "https://claude.ai/icon.png"<br/>
tab.lastAccessed → DateTime(2025, 10, 20, 14, 30)<br/>
tab.isPinned → false (default)<br/>
tab.type → "tab" (default)<br/>
tab.visitCount → 15<br/>
tab.folder → null
</div>
</div>
<div style="text-align: center; font-size: 2em; margin: 20px 0;">⬇️</div>
<div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 8px;">
<strong>Step 4:</strong> Display in UI
<div style="margin-top: 15px;">
Now we can show: tab.title, tab.url, tab.favicon, etc. in our card widgets!
</div>
</div>
</div>
</div>
<!-- SECTION 8: Why This Design? -->
<div class="section">
<h2 class="section-title">🤔 Why Is TabData Designed This Way?</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 20px;">
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; border-left: 4px solid #4caf50;">
<h3 style="color: #2e7d32; margin-bottom: 10px;">✅ Type Safety</h3>
<p>Each property has a specific type (String, bool, DateTime). This prevents errors like putting a number where text should be.</p>
</div>
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; border-left: 4px solid #2196f3;">
<h3 style="color: #1565c0; margin-bottom: 10px;">🛡️ Error Handling</h3>
<p>The ?? operators and multiple date field checks ensure the app doesn't crash if data is missing or in a different format.</p>
</div>
<div style="background: #fff3e0; padding: 20px; border-radius: 8px; border-left: 4px solid #ff9800;">
<h3 style="color: #e65100; margin-bottom: 10px;">🔄 Flexibility</h3>
<p>Works with different browser APIs (Chrome, Firefox, Edge) because it handles different field names and formats.</p>
</div>
<div style="background: #f3e5f5; padding: 20px; border-radius: 8px; border-left: 4px solid #9c27b0;">
<h3 style="color: #6a1b9a; margin-bottom: 10px;">📦 Organization</h3>
<p>All tab/bookmark/history data is in one place, making it easy to work with throughout the app.</p>
</div>
<div style="background: #fce4ec; padding: 20px; border-radius: 8px; border-left: 4px solid #e91e63;">
<h3 style="color: #880e4f; margin-bottom: 10px;">🔍 Readability</h3>
<p>Instead of messy JSON strings everywhere, we have clean objects: tab.title, tab.url, etc.</p>
</div>
<div style="background: #e0f2f1; padding: 20px; border-radius: 8px; border-left: 4px solid #009688;">
<h3 style="color: #00695c; margin-bottom: 10px;">⚡ Efficiency</h3>
<p>Creating TabData objects is fast, and we can easily create lists of them: List&lt;TabData&gt;.</p>
</div>
</div>
</div>
<!-- SECTION 9: How It's Used in main.dart -->
<div class="section">
<h2 class="section-title">🔗 How TabData Is Used in main.dart</h2>
<div class="visual-flow">
<h3 style="margin-bottom: 15px;">The Complete Data Flow:</h3>
<div class="constructor-flow" style="margin: 30px 0;">
<div class="flow-step" style="background: #ffebee;">
<strong>1. Browser API</strong>
<div style="margin-top: 10px; font-size: 0.9em;">
Returns JSON data<br/>
about tabs/bookmarks
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" style="background: #fff3e0;">
<strong>2. fromJson()</strong>
<div style="margin-top: 10px; font-size: 0.9em;">
Converts JSON<br/>
to TabData objects
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" style="background: #e8f5e9;">
<strong>3. State Lists</strong>
<div style="margin-top: 10px; font-size: 0.9em;">
Stored in allItems[]<br/>
and filteredItems[]
</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" style="background: #e3f2fd;">
<strong>4. UI Display</strong>
<div style="margin-top: 10px; font-size: 0.9em;">
ItemCard widgets<br/>
show the data
</div>
</div>
</div>
<div class="code-block">
<span class="code-comment">// 1. Get JSON from browser</span>
<span class="code-keyword">final</span> result = <span class="code-keyword">await</span> _callBrowserAPI(<span class="code-string">'getTabs'</span>);
<span class="code-keyword">final</span> List&lt;<span class="code-keyword">dynamic</span>&gt; data = json.decode(result);
<span class="code-comment">// 2. Convert each JSON item to TabData</span>
<span class="code-keyword">return</span> data.map((item) {
item[<span class="code-string">'type'</span>] = <span class="code-string">'tab'</span>;
<span class="code-keyword">return</span> <span class="code-class">TabData</span>.fromJson(item); <span class="code-comment">// ← Using fromJson here!</span>
}).toList();
<span class="code-comment">// 3. Store in state</span>
setState(() {
allItems = [...tabs, ...bookmarks, ...history]; <span class="code-comment">// All TabData objects</span>
});
<span class="code-comment">// 4. Display in UI</span>
<span class="code-keyword">return</span> ItemCard(
item: filteredItems[index], <span class="code-comment">// ← Pass TabData to card</span>
icon: _getIcon(filteredItems[index]),
);
</div>
</div>
</div>
<!-- SECTION 10: Key Statements Breakdown -->
<div class="section">
<h2 class="section-title">📝 Statement-by-Statement Breakdown</h2>
<div style="margin-top: 20px;">
<div class="property">
<span class="property-name">class TabData {</span>
<span class="property-desc">
<strong>STATEMENT:</strong> Declare a new class (blueprint) named TabData
</span>
</div>
<div class="property">
<span class="property-name">String id;</span>
<span class="property-desc">
<strong>STATEMENT:</strong> Declare a variable named 'id' that holds text (String)
</span>
</div>
<div class="property">
<span class="property-name">int? visitCount;</span>
<span class="property-desc">
<strong>STATEMENT:</strong> Declare a variable that holds a number, but can also be null (empty)
</span>
</div>
<div class="property">
<span class="property-name">TabData({...})</span>
<span class="property-desc">
<strong>STATEMENT:</strong> Define the constructor - how to create TabData objects
</span>
</div>
<div class="property">
<span class="property-name">required this.id</span>
<span class="property-desc">
<strong>STATEMENT:</strong> This parameter MUST be provided when creating a TabData
</span>
</div>
<div class="property">
<span class="property-name">this.favicon = ''</span>
<span class="property-desc">
<strong>STATEMENT:</strong> This parameter is optional, defaults to empty string if not provided
</span>
</div>
<div class="property">
<span class="property-name">: lastAccessed = lastAccessed ?? DateTime.now()</span>
<span class="property-desc">
<strong>STATEMENT:</strong> Initializer list - set lastAccessed to the provided value, or current time if null
</span>
</div>
<div class="property">
<span class="property-name">factory TabData.fromJson(...)</span>
<span class="property-desc">
<strong>STATEMENT:</strong> Define a factory method - an alternative way to create TabData from JSON
</span>
</div>
<div class="property">
<span class="property-name">json['id'].toString()</span>
<span class="property-desc">
<strong>STATEMENT:</strong> Get the 'id' field from JSON and convert it to a string
</span>
</div>
<div class="property">
<span class="property-name">json['title'] ?? 'Untitled'</span>
<span class="property-desc">
<strong>STATEMENT:</strong> Get 'title' from JSON, or use 'Untitled' if it's null/missing
</span>
</div>
<div class="property">
<span class="property-name">DateTime.parse(json['lastAccessed'])</span>
<span class="property-desc">
<strong>STATEMENT:</strong> Convert the date string from JSON into a DateTime object
</span>
</div>
</div>
</div>
<!-- SECTION 11: Common Operations -->
<div class="section">
<h2 class="section-title">⚙️ Common Operations with TabData</h2>
<div style="margin-top: 20px;">
<h3 style="margin-bottom: 15px;">Creating a new TabData:</h3>
<div class="code-block">
<span class="code-keyword">final</span> newTab = <span class="code-class">TabData</span>(
id: <span class="code-string">'tab_001'</span>,
title: <span class="code-string">'My Website'</span>,
url: <span class="code-string">'https://example.com'</span>,
);
</div>
<h3 style="margin: 30px 0 15px 0;">Converting JSON to TabData:</h3>
<div class="code-block">
<span class="code-keyword">final</span> jsonData = {<span class="code-string">'id'</span>: <span class="code-string">'123'</span>, <span class="code-string">'title'</span>: <span class="code-string">'Test'</span>, <span class="code-string">'url'</span>: <span class="code-string">'...'</span>};
<span class="code-keyword">final</span> tab = <span class="code-class">TabData</span>.fromJson(jsonData);
</div>
<h3 style="margin: 30px 0 15px 0;">Accessing properties:</h3>
<div class="code-block">
print(tab.title); <span class="code-comment">// Print the title</span>
print(tab.url); <span class="code-comment">// Print the URL</span>
<span class="code-keyword">if</span> (tab.isPinned) { <span class="code-comment">// Check if pinned</span>
print(<span class="code-string">'This tab is pinned!'</span>);
}
</div>
<h3 style="margin: 30px 0 15px 0;">Creating a list of TabData:</h3>
<div class="code-block">
List&lt;<span class="code-class">TabData</span>&gt; allTabs = []; <span class="code-comment">// Empty list</span>
<span class="code-comment">// Add tabs to the list</span>
allTabs.add(newTab);
<span class="code-comment">// Convert multiple JSON items to TabData list</span>
<span class="code-keyword">final</span> tabs = jsonArray.map((item) => <span class="code-class">TabData</span>.fromJson(item)).toList();
</div>
<h3 style="margin: 30px 0 15px 0;">Modifying properties:</h3>
<div class="code-block">
tab.title = <span class="code-string">'New Title'</span>; <span class="code-comment">// Change the title</span>
tab.isPinned = <span class="code-keyword">true</span>; <span class="code-comment">// Pin the tab</span>
tab.lastAccessed = DateTime.now(); <span class="code-comment">// Update last access time</span>
</div>
</div>
</div>
<!-- SECTION 12: Summary -->
<div class="section" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<h2 style="color: white; border-color: white;">📚 Summary</h2>
<div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 8px; margin-top: 20px;">
<h3 style="color: #ffd700; margin-bottom: 15px;">TabData is the foundation of the entire app!</h3>
<ul style="list-style: none; padding: 0; margin-top: 20px;">
<li style="margin: 15px 0; padding-left: 25px; position: relative;">
<span style="position: absolute; left: 0;">🎯</span>
<strong>Blueprint:</strong> Defines how we store tab/bookmark/history information
</li>
<li style="margin: 15px 0; padding-left: 25px; position: relative;">
<span style="position: absolute; left: 0;">📋</span>
<strong>9 Properties:</strong> id, title, url, favicon, lastAccessed, isPinned, type, visitCount, folder
</li>
<li style="margin: 15px 0; padding-left: 25px; position: relative;">
<span style="position: absolute; left: 0;">🏗️</span>
<strong>Constructor:</strong> Creates new TabData objects with required and optional fields
</li>
<li style="margin: 15px 0; padding-left: 25px; position: relative;">
<span style="position: absolute; left: 0;">🔄</span>
<strong>fromJson:</strong> Converts browser API JSON data into TabData objects
</li>
<li style="margin: 15px 0; padding-left: 25px; position: relative;">
<span style="position: absolute; left: 0;">🛡️</span>
<strong>Error Handling:</strong> Uses ?? operator and multiple checks to handle missing data
</li>
<li style="margin: 15px 0; padding-left: 25px; position: relative;">
<span style="position: absolute; left: 0;">📅</span>
<strong>Smart Dates:</strong> Handles 4 different date field names from different browsers
</li>
<li style="margin: 15px 0; padding-left: 25px; position: relative;">
<span style="position: absolute; left: 0;"></span>
<strong>Clean Code:</strong> Makes working with browser data easy and type-safe
</li>
</ul>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 8px; margin-top: 20px; text-align: center;">
<h3 style="color: #ffd700; margin-bottom: 10px;">💡 Remember</h3>
<p style="font-size: 1.1em;">
Every tab, bookmark, and history item you see in the app is a <strong>TabData object</strong>.
<br/><br/>
Without TabData, we'd just have messy JSON strings everywhere!
</p>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

View file

@ -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;"]

View file

@ -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"

View file

@ -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<String, dynamic> 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<TabManagerHome> createState() => _TabManagerHomeState();
}
class _TabManagerHomeState extends State<TabManagerHome> {
List<TabData> allItems = [];
List<TabData> 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<String, dynamic>.from(data));
}
});
}
// Handle messages from extension
void _handleExtensionMessage(Map<String, dynamic> 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<String, dynamic> 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<void> _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<List<TabData>> _getTabs() async {
try {
final result = await _callBrowserAPI('getTabs');
if (result != null) {
final List<dynamic> 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<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 {
// 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<void> _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<void> _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<void> _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<String>(
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;
}
}
}

View file

@ -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";
}
}

View file

@ -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

View file

@ -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
}

View file

@ -50,9 +50,7 @@ class AppConstants {
} }
// This is our centralized configuration file where all fixed values are stored in one place. // centralized configuration fixed values stored one place.
//
// It contains settings used throughout the app.
// //
// Instead of hardcoding values // Instead of hardcoding values
// //
@ -63,5 +61,4 @@ class AppConstants {
// All user-facing messages are stored here making translation and text updates simple. // 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. // Grid layout values are defined here so we can adjust the card view globally.
// // Easy maintain
// This approach keeps our code clean, consistent, and easy to maintain.

View file

@ -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 { class TabData {
// FIELDS for storing data "Fields"
// What we will be uding in other parts of the code
String id; String id;
String title; String title;
String url; String url;
@ -7,9 +17,12 @@ class TabData {
bool isPinned; bool isPinned;
String type; String type;
int? visitCount; int? visitCount;
String? folder; String? folder;
// Constructor Declaration "Constructor Declaration" The Assembly Line
TabData({ TabData({
// Parameters saved from the data coming in
// "this." current object
required this.id, required this.id,
required this.title, required this.title,
required this.url, required this.url,
@ -19,9 +32,16 @@ class TabData {
this.type = 'tab', this.type = 'tab',
this.visitCount, this.visitCount,
this.folder, 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<String, dynamic> json) => TabData( factory TabData.fromJson(Map<String, dynamic> json) => TabData(
// Extract data pass to regular constructor
id: json['id'].toString(), id: json['id'].toString(),
title: json['title'] ?? 'Untitled', title: json['title'] ?? 'Untitled',
url: json['url'] ?? '', 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. // Foundation: managing/storing tabs.
//
// 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.

View file

@ -9,6 +9,9 @@ import '../widgets/search_bar.dart' as custom;
import '../widgets/filter_chips.dart'; import '../widgets/filter_chips.dart';
import '../widgets/app_bar_actions.dart'; import '../widgets/app_bar_actions.dart';
// StatefulWidget
class TabManagerHome extends StatefulWidget { class TabManagerHome extends StatefulWidget {
const TabManagerHome({super.key}); const TabManagerHome({super.key});

View file

@ -3,6 +3,17 @@ import 'dart:convert';
import 'dart:js_util' as js_util; import 'dart:js_util' as js_util;
import '../models/tab_data.dart'; 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 { class BrowserApiService {
Future<List<TabData>> getTabs() async { Future<List<TabData>> getTabs() async {
try { 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. // Without extension, prints debug messages instead of crashing.
//
// When running in development without the extension, it prints debug messages instead of crashing.

View file

@ -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/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:browser_tab_manager/main.dart'; import 'package:browser_tab_manager/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('App loads successfully', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(const MyApp()); await tester.pumpWidget(const BrowserTabManagerApp());
// Verify that our counter starts at 0. // Verify that the app bar title is present.
expect(find.text('0'), findsOneWidget); expect(find.text('Browser Tab Manager'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Verify that the search field is present.
// Tap the '+' icon and trigger a frame. expect(find.byType(TextField), findsOneWidget);
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);
}); });
} }