cc
This commit is contained in:
parent
58d9d1f27b
commit
f0354e706e
15 changed files with 1768 additions and 1103 deletions
149
Study/code-to-ui-mapping.svg
Normal file
149
Study/code-to-ui-mapping.svg
Normal 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<TabData> allItems = [];</text>
|
||||
<text x="710" y="595" fill="#fff" font-size="11" font-family="monospace">List<TabData> 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 |
595
Study/tabdata-complete-breakdown.html
Normal file
595
Study/tabdata-complete-breakdown.html
Normal 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><<span class="class">String</span>, <span class="keyword">dynamic</span>> 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<String, dynamic> 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"><String, dynamic></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>,
|
||||
960
Study/tabdata-visual-explanation.html
Normal file
960
Study/tabdata-visual-explanation.html
Normal 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<TabData>.</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<<span class="code-keyword">dynamic</span>> 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<<span class="code-class">TabData</span>> 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>
|
||||
BIN
dist/browser-tab-manager.tar.gz
vendored
BIN
dist/browser-tab-manager.tar.gz
vendored
Binary file not shown.
52
dist/browser-tab-manager/Dockerfile
vendored
52
dist/browser-tab-manager/Dockerfile
vendored
|
|
@ -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;"]
|
||||
60
dist/browser-tab-manager/deploy.sh
vendored
60
dist/browser-tab-manager/deploy.sh
vendored
|
|
@ -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"
|
||||
890
dist/browser-tab-manager/lib/main.dart
vendored
890
dist/browser-tab-manager/lib/main.dart
vendored
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
dist/browser-tab-manager/nginx.conf
vendored
20
dist/browser-tab-manager/nginx.conf
vendored
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
20
dist/browser-tab-manager/pubspec.yaml
vendored
20
dist/browser-tab-manager/pubspec.yaml
vendored
|
|
@ -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
|
||||
11
dist/browser-tab-manager/web/manifest.json
vendored
11
dist/browser-tab-manager/web/manifest.json
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
// Easy maintain
|
||||
|
|
@ -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<String, dynamic> 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.
|
||||
// Foundation: managing/storing tabs.
|
||||
|
|
@ -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});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<List<TabData>> 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.
|
||||
// Without extension, prints debug messages instead of crashing.
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue