mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add About screen

+535 -568
+146 -289
docs/designs/settings.html
··· 35 35 border-bottom: 1px solid var(--border); 36 36 } 37 37 38 - .theme-selector { 38 + .segmented-control { 39 + display: flex; 39 40 padding: 16px; 40 - display: grid; 41 - grid-template-columns: repeat(4, 1fr); 42 - gap: 8px; 43 - justify-items: center; 44 41 } 45 42 46 - .theme-option { 43 + .segmented-control-inner { 47 44 display: flex; 48 - flex-direction: column; 49 - align-items: center; 50 - gap: 8px; 45 + width: 100%; 46 + border: 1px solid var(--border); 47 + border-radius: 12px; 48 + overflow: hidden; 49 + } 50 + 51 + .segment-btn { 52 + flex: 1; 53 + padding: 10px 0; 54 + text-align: center; 55 + font-size: 14px; 56 + font-weight: 500; 57 + color: var(--text-secondary); 51 58 cursor: pointer; 52 - padding: 12px; 53 - border-radius: 12px; 54 - transition: background-color 0.2s ease; 59 + transition: all 0.2s ease; 60 + border: none; 61 + background: transparent; 62 + font-family: inherit; 55 63 } 56 64 57 - .theme-option:hover { 65 + .segment-btn:not(:last-child) { 66 + border-right: 1px solid var(--border); 67 + } 68 + 69 + .segment-btn:hover { 58 70 background-color: var(--surface-variant); 59 71 } 60 72 61 - .theme-option.active { 62 - background-color: var(--surface-variant); 73 + .segment-btn.active { 74 + background-color: var(--accent-primary); 75 + color: var(--bg-primary); 76 + font-weight: 600; 63 77 } 64 78 65 - .theme-section-label { 66 - grid-column: 1 / -1; 79 + .theme-list-label { 80 + padding: 16px 16px 8px; 67 81 font-size: 12px; 68 82 font-weight: 600; 69 83 color: var(--text-muted); 70 84 text-transform: uppercase; 71 85 letter-spacing: 0.5px; 72 - padding: 8px 0 0; 73 - width: 100%; 74 - text-align: left; 75 86 } 76 87 77 - .theme-section-label:first-child { 78 - padding-top: 0; 79 - } 80 - 81 - .theme-option-preview { 82 - width: 64px; 83 - height: 64px; 84 - border-radius: 12px; 85 - border: 2px solid var(--border); 86 - overflow: hidden; 87 - position: relative; 88 - transition: all 0.2s ease; 89 - } 90 - 91 - .theme-option:hover .theme-option-preview, 92 - .theme-option.active .theme-option-preview { 93 - border-color: var(--accent-primary); 94 - transform: scale(1.02); 95 - } 96 - 97 - .theme-preview-light { 98 - background: linear-gradient(135deg, #ffffff 50%, #f4f4f4 50%); 99 - } 100 - 101 - .theme-preview-dark { 102 - background: linear-gradient(135deg, #161616 50%, #262626 50%); 103 - } 104 - 105 - .theme-option-check { 106 - position: absolute; 107 - bottom: 4px; 108 - right: 4px; 109 - width: 20px; 110 - height: 20px; 111 - background-color: var(--accent-primary); 112 - border-radius: 50%; 88 + .theme-palette-row { 113 89 display: flex; 114 90 align-items: center; 115 - justify-content: center; 116 - opacity: 0; 117 - transition: opacity 0.2s ease; 91 + padding: 14px 16px; 92 + cursor: pointer; 93 + transition: background-color 0.2s ease; 118 94 } 119 95 120 - .theme-option.active .theme-option-check { 121 - opacity: 1; 96 + .theme-palette-row:hover { 97 + background-color: var(--surface-variant); 122 98 } 123 99 124 - .theme-option-check svg { 125 - width: 12px; 126 - height: 12px; 127 - color: white; 128 - } 129 - 130 - .theme-option-label { 131 - font-size: 14px; 100 + .theme-palette-name { 101 + flex: 1; 102 + font-size: 15px; 132 103 font-weight: 500; 133 104 color: var(--text-primary); 134 105 } 135 106 136 - .auto-theme-toggle { 137 - padding: 16px; 138 - border-top: 1px solid var(--border); 107 + .theme-palette-swatches { 139 108 display: flex; 140 - align-items: center; 141 - justify-content: space-between; 109 + gap: 4px; 110 + margin-right: 4px; 142 111 } 143 112 144 - .auto-theme-info { 145 - display: flex; 146 - flex-direction: column; 147 - gap: 4px; 113 + .theme-swatch { 114 + width: 20px; 115 + height: 20px; 116 + border-radius: 4px; 148 117 } 149 118 150 - .auto-theme-title { 151 - font-size: 15px; 152 - font-weight: 500; 153 - color: var(--text-primary); 119 + .theme-palette-check { 120 + width: 20px; 121 + height: 20px; 122 + margin-left: 8px; 123 + color: var(--accent-primary); 124 + opacity: 0; 125 + transition: opacity 0.2s ease; 154 126 } 155 127 156 - .auto-theme-subtitle { 157 - font-size: 13px; 158 - color: var(--text-secondary); 128 + .theme-palette-row.active .theme-palette-check { 129 + opacity: 1; 159 130 } 160 131 161 132 .settings-item-danger { ··· 255 226 <div class="settings-section-title">Appearance</div> 256 227 257 228 <div class="settings-group"> 258 - <!-- Theme Selector --> 259 - <div class="theme-selector"> 260 - <!-- Oxocarbon --> 261 - <div class="theme-section-label">Oxocarbon</div> 262 - <div class="theme-option active" data-theme="light" onclick="setTheme('light')"> 263 - <div class="theme-option-preview theme-preview-light"> 264 - <div class="theme-option-check"> 265 - <svg 266 - viewBox="0 0 24 24" 267 - fill="none" 268 - stroke="currentColor" 269 - stroke-width="3" 270 - stroke-linecap="round" 271 - stroke-linejoin="round"> 272 - <polyline points="20 6 9 17 4 12" /> 273 - </svg> 274 - </div> 275 - </div> 276 - <span class="theme-option-label">Light</span> 229 + <!-- Appearance Mode Segmented Control --> 230 + <div class="segmented-control"> 231 + <div class="segmented-control-inner"> 232 + <button class="segment-btn" data-mode="system" onclick="setMode('system')">System</button> 233 + <button class="segment-btn" data-mode="light" onclick="setMode('light')">Light</button> 234 + <button class="segment-btn active" data-mode="dark" onclick="setMode('dark')">Dark</button> 277 235 </div> 278 - <div class="theme-option" data-theme="dark" onclick="setTheme('dark')"> 279 - <div class="theme-option-preview theme-preview-dark"> 280 - <div class="theme-option-check"> 281 - <svg 282 - viewBox="0 0 24 24" 283 - fill="none" 284 - stroke="currentColor" 285 - stroke-width="3" 286 - stroke-linecap="round" 287 - stroke-linejoin="round"> 288 - <polyline points="20 6 9 17 4 12" /> 289 - </svg> 290 - </div> 291 - </div> 292 - <span class="theme-option-label">Dark</span> 293 - </div> 294 - <!-- spacers for 4-col grid --> 295 - <div></div> 296 - <div></div> 236 + </div> 237 + 238 + <div style="border-top: 1px solid var(--border)"></div> 239 + 240 + <!-- Theme Palette List --> 241 + <div class="theme-list-label">Theme</div> 297 242 298 - <!-- Catppuccin --> 299 - <div class="theme-section-label">Catppuccin</div> 300 - <div class="theme-option" data-theme="catppuccin-latte" onclick="setTheme('catppuccin-latte')"> 301 - <div class="theme-option-preview theme-preview-catppuccin-latte"> 302 - <div class="theme-option-check"> 303 - <svg 304 - viewBox="0 0 24 24" 305 - fill="none" 306 - stroke="currentColor" 307 - stroke-width="3" 308 - stroke-linecap="round" 309 - stroke-linejoin="round"> 310 - <polyline points="20 6 9 17 4 12" /> 311 - </svg> 312 - </div> 313 - </div> 314 - <span class="theme-option-label">Latte</span> 243 + <div class="theme-palette-row active" data-palette="oxocarbon" onclick="setPalette('oxocarbon')"> 244 + <span class="theme-palette-name">Oxocarbon</span> 245 + <div class="theme-palette-swatches"> 246 + <div class="theme-swatch" style="background-color: #78a9ff"></div> 247 + <div class="theme-swatch" style="background-color: #be95ff"></div> 248 + <div class="theme-swatch" style="background-color: #08bdba"></div> 249 + <div class="theme-swatch" style="background-color: #ee5396"></div> 315 250 </div> 316 - <div class="theme-option" data-theme="catppuccin-mocha" onclick="setTheme('catppuccin-mocha')"> 317 - <div class="theme-option-preview theme-preview-catppuccin-mocha"> 318 - <div class="theme-option-check"> 319 - <svg 320 - viewBox="0 0 24 24" 321 - fill="none" 322 - stroke="currentColor" 323 - stroke-width="3" 324 - stroke-linecap="round" 325 - stroke-linejoin="round"> 326 - <polyline points="20 6 9 17 4 12" /> 327 - </svg> 328 - </div> 329 - </div> 330 - <span class="theme-option-label">Mocha</span> 331 - </div> 332 - <div></div> 333 - <div></div> 251 + <svg class="theme-palette-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> 252 + <polyline points="20 6 9 17 4 12" /> 253 + </svg> 254 + </div> 334 255 335 - <!-- Nord --> 336 - <div class="theme-section-label">Nord</div> 337 - <div class="theme-option" data-theme="nord-light" onclick="setTheme('nord-light')"> 338 - <div class="theme-option-preview theme-preview-nord-light"> 339 - <div class="theme-option-check"> 340 - <svg 341 - viewBox="0 0 24 24" 342 - fill="none" 343 - stroke="currentColor" 344 - stroke-width="3" 345 - stroke-linecap="round" 346 - stroke-linejoin="round"> 347 - <polyline points="20 6 9 17 4 12" /> 348 - </svg> 349 - </div> 350 - </div> 351 - <span class="theme-option-label">Snow</span> 256 + <div class="theme-palette-row" data-palette="catppuccin" onclick="setPalette('catppuccin')"> 257 + <span class="theme-palette-name">Catppuccin</span> 258 + <div class="theme-palette-swatches"> 259 + <div class="theme-swatch" style="background-color: #b4befe"></div> 260 + <div class="theme-swatch" style="background-color: #cba6f7"></div> 261 + <div class="theme-swatch" style="background-color: #74c7ec"></div> 262 + <div class="theme-swatch" style="background-color: #f5c2e7"></div> 352 263 </div> 353 - <div class="theme-option" data-theme="nord-dark" onclick="setTheme('nord-dark')"> 354 - <div class="theme-option-preview theme-preview-nord-dark"> 355 - <div class="theme-option-check"> 356 - <svg 357 - viewBox="0 0 24 24" 358 - fill="none" 359 - stroke="currentColor" 360 - stroke-width="3" 361 - stroke-linecap="round" 362 - stroke-linejoin="round"> 363 - <polyline points="20 6 9 17 4 12" /> 364 - </svg> 365 - </div> 366 - </div> 367 - <span class="theme-option-label">Polar</span> 368 - </div> 369 - <div></div> 370 - <div></div> 264 + <svg class="theme-palette-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> 265 + <polyline points="20 6 9 17 4 12" /> 266 + </svg> 267 + </div> 371 268 372 - <!-- Rosé Pine --> 373 - <div class="theme-section-label">Rosé Pine</div> 374 - <div class="theme-option" data-theme="rose-pine-dawn" onclick="setTheme('rose-pine-dawn')"> 375 - <div class="theme-option-preview theme-preview-rose-pine-dawn"> 376 - <div class="theme-option-check"> 377 - <svg 378 - viewBox="0 0 24 24" 379 - fill="none" 380 - stroke="currentColor" 381 - stroke-width="3" 382 - stroke-linecap="round" 383 - stroke-linejoin="round"> 384 - <polyline points="20 6 9 17 4 12" /> 385 - </svg> 386 - </div> 387 - </div> 388 - <span class="theme-option-label">Dawn</span> 269 + <div class="theme-palette-row" data-palette="nord" onclick="setPalette('nord')"> 270 + <span class="theme-palette-name">Nord</span> 271 + <div class="theme-palette-swatches"> 272 + <div class="theme-swatch" style="background-color: #88c0d0"></div> 273 + <div class="theme-swatch" style="background-color: #a3be8c"></div> 274 + <div class="theme-swatch" style="background-color: #ebcb8b"></div> 275 + <div class="theme-swatch" style="background-color: #b48ead"></div> 389 276 </div> 390 - <div class="theme-option" data-theme="rose-pine-moon" onclick="setTheme('rose-pine-moon')"> 391 - <div class="theme-option-preview theme-preview-rose-pine-moon"> 392 - <div class="theme-option-check"> 393 - <svg 394 - viewBox="0 0 24 24" 395 - fill="none" 396 - stroke="currentColor" 397 - stroke-width="3" 398 - stroke-linecap="round" 399 - stroke-linejoin="round"> 400 - <polyline points="20 6 9 17 4 12" /> 401 - </svg> 402 - </div> 403 - </div> 404 - <span class="theme-option-label">Moon</span> 405 - </div> 406 - <div></div> 407 - <div></div> 277 + <svg class="theme-palette-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> 278 + <polyline points="20 6 9 17 4 12" /> 279 + </svg> 408 280 </div> 409 281 410 - <!-- Auto Theme Toggle --> 411 - <div class="auto-theme-toggle"> 412 - <div class="auto-theme-info"> 413 - <div class="auto-theme-title">Auto</div> 414 - <div class="auto-theme-subtitle">Follow system theme</div> 415 - </div> 416 - <div class="toggle" id="auto-theme-toggle" onclick="toggleAutoTheme()"> 417 - <div class="toggle-thumb"></div> 282 + <div class="theme-palette-row" data-palette="rosepine" onclick="setPalette('rosepine')"> 283 + <span class="theme-palette-name">Rosé Pine</span> 284 + <div class="theme-palette-swatches"> 285 + <div class="theme-swatch" style="background-color: #ebbcba"></div> 286 + <div class="theme-swatch" style="background-color: #c4a7e7"></div> 287 + <div class="theme-swatch" style="background-color: #9ccfd8"></div> 288 + <div class="theme-swatch" style="background-color: #f6c177"></div> 418 289 </div> 290 + <svg class="theme-palette-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> 291 + <polyline points="20 6 9 17 4 12" /> 292 + </svg> 419 293 </div> 294 + 295 + <div style="height: 8px"></div> 420 296 </div> 421 297 </div> 422 298 ··· 768 644 </div> 769 645 770 646 <script> 771 - const ALL_THEMES = [ 772 - "light", 773 - "dark", 774 - "catppuccin-latte", 775 - "catppuccin-mocha", 776 - "nord-light", 777 - "nord-dark", 778 - "rose-pine-dawn", 779 - "rose-pine-moon", 780 - ]; 781 - 782 - function setTheme(theme) { 783 - document.querySelectorAll(".theme-option").forEach((el) => { 784 - el.classList.remove("active"); 785 - }); 786 - const target = document.querySelector(`.theme-option[data-theme="${theme}"]`); 647 + function setMode(mode) { 648 + document.querySelectorAll(".segment-btn").forEach((el) => el.classList.remove("active")); 649 + const target = document.querySelector(`.segment-btn[data-mode="${mode}"]`); 787 650 if (target) target.classList.add("active"); 788 651 789 - if (theme === "light") { 790 - document.documentElement.removeAttribute("data-theme"); 652 + localStorage.setItem("appearance-mode", mode); 653 + 654 + if (mode === "system") { 655 + const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; 656 + applyVariant(prefersDark ? "dark" : "light"); 791 657 } else { 792 - document.documentElement.setAttribute("data-theme", theme); 658 + applyVariant(mode); 793 659 } 794 - localStorage.setItem("theme", theme); 795 - 796 - document.getElementById("auto-theme-toggle").classList.remove("active"); 797 - localStorage.removeItem("auto-theme"); 798 660 } 799 661 800 - function toggleAutoTheme() { 801 - const toggle = document.getElementById("auto-theme-toggle"); 802 - toggle.classList.toggle("active"); 662 + function setPalette(palette) { 663 + document.querySelectorAll(".theme-palette-row").forEach((el) => el.classList.remove("active")); 664 + const target = document.querySelector(`.theme-palette-row[data-palette="${palette}"]`); 665 + if (target) target.classList.add("active"); 666 + localStorage.setItem("palette", palette); 667 + } 803 668 804 - if (toggle.classList.contains("active")) { 805 - localStorage.setItem("auto-theme", "true"); 806 - 807 - if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { 808 - document.documentElement.setAttribute("data-theme", "dark"); 809 - localStorage.setItem("theme", "dark"); 810 - } else { 811 - document.documentElement.removeAttribute("data-theme"); 812 - localStorage.setItem("theme", "light"); 813 - } 669 + function applyVariant(variant) { 670 + if (variant === "light") { 671 + document.documentElement.removeAttribute("data-theme"); 814 672 } else { 815 - localStorage.removeItem("auto-theme"); 673 + document.documentElement.setAttribute("data-theme", "dark"); 816 674 } 817 675 } 818 676 819 677 function logout() { 820 678 if (confirm("Are you sure you want to log out?")) { 821 - localStorage.removeItem("theme"); 822 - localStorage.removeItem("auto-theme"); 679 + localStorage.clear(); 823 680 window.location.href = "login.html"; 824 681 } 825 682 } 826 683 827 684 (function () { 828 - const savedTheme = localStorage.getItem("theme") || "light"; 829 - const autoTheme = localStorage.getItem("auto-theme"); 685 + const mode = localStorage.getItem("appearance-mode") || "dark"; 686 + const palette = localStorage.getItem("palette") || "oxocarbon"; 830 687 831 - if (autoTheme === "true") { 832 - document.getElementById("auto-theme-toggle").classList.add("active"); 833 - } 688 + // Restore mode 689 + document.querySelectorAll(".segment-btn").forEach((el) => el.classList.remove("active")); 690 + const modeBtn = document.querySelector(`.segment-btn[data-mode="${mode}"]`); 691 + if (modeBtn) modeBtn.classList.add("active"); 834 692 835 - document.querySelectorAll(".theme-option").forEach((el) => { 836 - el.classList.remove("active"); 837 - }); 838 - 839 - if (savedTheme === "light") { 840 - document.documentElement.removeAttribute("data-theme"); 693 + if (mode === "system") { 694 + const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; 695 + applyVariant(prefersDark ? "dark" : "light"); 841 696 } else { 842 - document.documentElement.setAttribute("data-theme", savedTheme); 697 + applyVariant(mode); 843 698 } 844 699 845 - const active = document.querySelector(`.theme-option[data-theme="${savedTheme}"]`); 846 - if (active) active.classList.add("active"); 700 + // Restore palette 701 + document.querySelectorAll(".theme-palette-row").forEach((el) => el.classList.remove("active")); 702 + const paletteRow = document.querySelector(`.theme-palette-row[data-palette="${palette}"]`); 703 + if (paletteRow) paletteRow.classList.add("active"); 847 704 })(); 848 705 </script> 849 706 </body>
+10 -9
docs/tasks/phase-2.md
··· 34 34 - [ ] Typeahead autocomplete via `searchActorsTypeahead` 35 35 - [ ] Drift migration: add `search_history` table (query, type, searched_at, account_did) 36 36 - [ ] Persisted search history — display recent queries, tap to re-execute, swipe to delete, cap at 50 per account 37 + - [ ] Search with `@` should autocomplete with avatars + handles (debounced) 37 38 38 39 ## M7 — Dev Tools (PDS Explorer) 39 40 40 - - [ ] `DevToolsCubit` with request/response state for stateless exploration 41 - - [ ] Handle / DID input with resolution via `resolveHandle` 42 - - [ ] Repository overview via `describeRepo` — list collections with record counts 43 - - [ ] Collection browser via `listRecords` — paginated record list per collection 44 - - [ ] Record inspector via `getRecord` — pretty-printed JSON with syntax highlighting 45 - - [ ] AT-URI input — paste `at://` URI to jump directly to a record 46 - - [ ] Add Dev Tools entry in Settings screen, navigable by all users 47 - - [ ] Include link to <https://pds.ls> as inspiration (pdsls) 48 - - [ ] Construct <https://aturi.to> links from AT-URI. 41 + - [x] `DevToolsCubit` with request/response state for stateless exploration 42 + - [x] Handle / DID input with resolution via `resolveHandle` 43 + - [x] Repository overview via `describeRepo` — list collections with record counts 44 + - [x] Collection browser via `listRecords` — paginated record list per collection 45 + - [x] Record inspector via `getRecord` — pretty-printed JSON with syntax highlighting 46 + - [x] AT-URI input — paste `at://` URI to jump directly to a record 47 + - [x] Add Dev Tools entry in Settings screen, navigable by all users 48 + - [x] Include link to <https://pds.ls> as inspiration (pdsls) 49 + - [x] Construct <https://aturi.to> links from AT-URI. 49 50 - ex. `at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.feed.post/3m6mwoadjbp2d` becomes 50 51 <https://aturi.to/did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.feed.post/3m6mwoadjbp2d>
+2
lib/core/router/app_router.dart
··· 10 10 import 'package:lazurite/features/feed/presentation/home_feed_screen.dart'; 11 11 import 'package:lazurite/features/logs/presentation/logs_screen.dart'; 12 12 import 'package:lazurite/features/profile/presentation/profile_screen.dart'; 13 + import 'package:lazurite/features/settings/presentation/about_screen.dart'; 13 14 import 'package:lazurite/features/settings/presentation/settings_screen.dart'; 14 15 15 16 class AppRouter { ··· 77 78 path: '/settings', 78 79 builder: (context, state) => const SettingsScreen(), 79 80 routes: [ 81 + GoRoute(path: 'about', builder: (context, state) => const AboutScreen()), 80 82 GoRoute(path: 'logs', builder: (context, state) => const LogsScreen()), 81 83 GoRoute(path: 'devtools', builder: (context, state) => const DevToolsScreen()), 82 84 ],
+9 -9
lib/core/router/app_shell.dart
··· 11 11 return Scaffold( 12 12 body: navigationShell, 13 13 bottomNavigationBar: NavigationBar( 14 + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, 14 15 selectedIndex: navigationShell.currentIndex, 15 16 onDestinationSelected: (index) { 16 17 navigationShell.goBranch(index, initialLocation: index == navigationShell.currentIndex); 17 18 }, 18 - destinations: const [ 19 - NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'), 20 - NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profile'), 21 - NavigationDestination( 22 - icon: Icon(Icons.settings_outlined), 23 - selectedIcon: Icon(Icons.settings), 24 - label: 'Settings', 25 - ), 26 - ], 19 + labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, 20 + destinations: _destinations, 27 21 ), 28 22 ); 29 23 } 24 + 25 + List<Widget> get _destinations => const [ 26 + NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'), 27 + NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profile'), 28 + NavigationDestination(icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: 'Settings'), 29 + ]; 30 30 }
+13
lib/core/theme/app_theme.dart
··· 93 93 return 'dark'; 94 94 } 95 95 } 96 + 97 + static List<Color> getSwatchColors(AppThemePalette palette) { 98 + switch (palette) { 99 + case AppThemePalette.oxocarbon: 100 + return const [Color(0xFF78a9ff), Color(0xFFbe95ff), Color(0xFF08bdba), Color(0xFFee5396)]; 101 + case AppThemePalette.catppuccin: 102 + return const [Color(0xFFb4befe), Color(0xFFcba6f7), Color(0xFF74c7ec), Color(0xFFf5c2e7)]; 103 + case AppThemePalette.nord: 104 + return const [Color(0xFF88c0d0), Color(0xFFa3be8c), Color(0xFFebcb8b), Color(0xFFb48ead)]; 105 + case AppThemePalette.rosePine: 106 + return const [Color(0xFFebbcba), Color(0xFFc4a7e7), Color(0xFF9ccfd8), Color(0xFFf6c177)]; 107 + } 108 + } 96 109 }
+8 -1
lib/features/auth/presentation/login_screen.dart
··· 1 1 import 'package:flutter/foundation.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 + import 'package:flutter_svg/flutter_svg.dart'; 4 5 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 5 6 6 7 class LoginScreen extends StatefulWidget { ··· 232 233 BoxShadow(color: colorScheme.primary.withValues(alpha: 0.24), blurRadius: 28, offset: const Offset(0, 12)), 233 234 ], 234 235 ), 235 - child: const Icon(Icons.layers_outlined, color: Colors.white, size: 42), 236 + child: Padding( 237 + padding: const EdgeInsets.all(18), 238 + child: SvgPicture.asset( 239 + 'assets/logo.svg', 240 + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), 241 + ), 242 + ), 236 243 ), 237 244 ); 238 245 }
+46 -118
lib/features/devtools/presentation/dev_tools_screen.dart
··· 18 18 actions: [ 19 19 IconButton( 20 20 icon: const Icon(Icons.open_in_new), 21 - tooltip: 'Open pds.ls inspiration', 21 + tooltip: 'Go to pds.ls', 22 22 onPressed: () => _openExternalUrl('https://pds.ls'), 23 23 ), 24 24 ], ··· 263 263 @override 264 264 Widget build(BuildContext context) { 265 265 final totalRepoRecords = state.totalRepoRecords; 266 - 266 + final theme = Theme.of(context); 267 267 return ListView( 268 268 children: [ 269 269 Container( ··· 290 290 const SizedBox(height: 2), 291 291 Text( 292 292 state.did ?? '', 293 - style: Theme.of(context).textTheme.bodySmall?.copyWith( 294 - fontFamily: 'JetBrains Mono', 295 - color: Theme.of(context).colorScheme.outline, 296 - ), 293 + style: Theme.of( 294 + context, 295 + ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 297 296 overflow: TextOverflow.ellipsis, 298 297 ), 299 298 ], ··· 306 305 spacing: 16, 307 306 runSpacing: 4, 308 307 children: [ 309 - Text('${state.collections.length} collections', style: Theme.of(context).textTheme.bodySmall), 308 + Text( 309 + '${state.collections.length} collections', 310 + style: theme.textTheme.bodySmall!.copyWith(color: theme.colorScheme.onSurface), 311 + ), 310 312 Text( 311 313 totalRepoRecords == null 312 314 ? (state.isCollectionCountsLoading ? 'Counting records...' : 'Record counts unavailable') 313 315 : '$totalRepoRecords records', 314 - style: Theme.of(context).textTheme.bodySmall, 316 + style: Theme.of( 317 + context, 318 + ).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 315 319 ), 316 320 ], 317 321 ), ··· 346 350 borderRadius: BorderRadius.circular(6), 347 351 color: Theme.of(context).colorScheme.surfaceContainerHighest, 348 352 ), 349 - child: Icon(_getCollectionIcon(collection.name), size: 16, color: Theme.of(context).colorScheme.outline), 353 + child: Icon(_getCollectionIcon(collection.name), size: 16, color: Theme.of(context).colorScheme.primary), 350 354 ), 351 355 title: Text(collection.name, style: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 13)), 352 356 trailing: Row( ··· 358 362 color: Theme.of(context).colorScheme.surfaceContainerHighest, 359 363 borderRadius: BorderRadius.circular(999), 360 364 ), 361 - child: Text(collection.countLabel, style: Theme.of(context).textTheme.labelSmall), 365 + child: Text( 366 + collection.countLabel, 367 + style: Theme.of( 368 + context, 369 + ).textTheme.labelSmall!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 370 + ), 362 371 ), 363 372 const SizedBox(width: 4), 364 373 const Icon(Icons.chevron_right), ··· 528 537 children: [ 529 538 Text( 530 539 record.rkey, 531 - style: Theme.of(context).textTheme.titleSmall?.copyWith( 532 - fontFamily: 'JetBrains Mono', 533 - color: Theme.of(context).colorScheme.primary, 534 - ), 540 + style: Theme.of(context).textTheme.titleSmall?.copyWith(color: Theme.of(context).colorScheme.primary), 535 541 overflow: TextOverflow.ellipsis, 536 542 ), 537 543 const SizedBox(height: 4), 538 544 Text( 539 545 record.uri, 540 - style: Theme.of(context).textTheme.bodySmall?.copyWith( 541 - fontFamily: 'JetBrains Mono', 542 - color: Theme.of(context).colorScheme.outline, 543 - ), 546 + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.tertiary), 544 547 overflow: TextOverflow.ellipsis, 545 548 ), 546 549 if (record.cid != null) ...[ 547 550 const SizedBox(height: 2), 548 551 Text( 549 552 'CID: ${record.cid!}', 550 - style: Theme.of(context).textTheme.bodySmall?.copyWith( 551 - fontFamily: 'JetBrains Mono', 552 - color: Theme.of(context).colorScheme.outline, 553 - ), 553 + style: Theme.of( 554 + context, 555 + ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.secondary), 554 556 ), 555 557 ], 556 558 const SizedBox(height: 8), ··· 606 608 final theme = Theme.of(context); 607 609 final primaryColor = theme.colorScheme.primary; 608 610 final surfaceVariant = theme.colorScheme.onSurfaceVariant; 611 + final keyStyle = theme.textTheme.bodySmall!.copyWith(color: surfaceVariant); 612 + final valueStyle = theme.textTheme.bodySmall!.copyWith(color: primaryColor); 613 + final strStyle = theme.textTheme.bodySmall!.copyWith(color: theme.colorScheme.secondary); 614 + final numStyle = theme.textTheme.bodySmall!.copyWith(color: theme.colorScheme.tertiary); 615 + final boolStyle = theme.textTheme.bodySmall!.copyWith(color: theme.colorScheme.primary); 616 + final nullStyle = theme.textTheme.bodySmall!.copyWith(color: theme.colorScheme.error); 609 617 610 618 if (value is Map<String, dynamic>) { 611 - final spans = <TextSpan>[ 612 - TextSpan( 613 - text: '{\n', 614 - style: TextStyle(color: surfaceVariant), 615 - ), 616 - ]; 619 + final spans = <TextSpan>[TextSpan(text: '{\n', style: keyStyle)]; 617 620 final entries = value.entries.toList(); 618 621 for (var i = 0; i < entries.length; i++) { 619 622 final entry = entries[i]; 620 - spans.add( 621 - TextSpan( 622 - text: ' ' * (indent + 1), 623 - style: TextStyle(color: surfaceVariant), 624 - ), 625 - ); 626 - spans.add( 627 - TextSpan( 628 - text: '"${entry.key}"', 629 - style: TextStyle(color: primaryColor), 630 - ), 631 - ); 632 - spans.add( 633 - TextSpan( 634 - text: ': ', 635 - style: TextStyle(color: surfaceVariant), 636 - ), 637 - ); 623 + spans.add(TextSpan(text: ' ' * (indent + 1), style: keyStyle)); 624 + spans.add(TextSpan(text: '"${entry.key}"', style: valueStyle)); 625 + spans.add(TextSpan(text: ': ', style: keyStyle)); 638 626 spans.addAll(_buildSpans(context, entry.value, indent + 1)); 639 627 if (i < entries.length - 1) { 640 - spans.add( 641 - TextSpan( 642 - text: ',', 643 - style: TextStyle(color: surfaceVariant), 644 - ), 645 - ); 628 + spans.add(TextSpan(text: ',', style: keyStyle)); 646 629 } 647 - spans.add( 648 - TextSpan( 649 - text: '\n', 650 - style: TextStyle(color: surfaceVariant), 651 - ), 652 - ); 630 + spans.add(TextSpan(text: '\n', style: keyStyle)); 653 631 } 654 - spans.add( 655 - TextSpan( 656 - text: ' ' * indent + '}', 657 - style: TextStyle(color: surfaceVariant), 658 - ), 659 - ); 632 + spans.add(TextSpan(text: ' ' * indent + '}', style: keyStyle)); 660 633 return spans; 661 634 } 662 635 663 636 if (value is List) { 664 - final spans = <TextSpan>[ 665 - TextSpan( 666 - text: '[\n', 667 - style: TextStyle(color: surfaceVariant), 668 - ), 669 - ]; 637 + final spans = <TextSpan>[TextSpan(text: '[\n', style: keyStyle)]; 670 638 for (var i = 0; i < value.length; i++) { 671 - spans.add( 672 - TextSpan( 673 - text: ' ' * (indent + 1), 674 - style: TextStyle(color: surfaceVariant), 675 - ), 676 - ); 639 + spans.add(TextSpan(text: ' ' * (indent + 1), style: keyStyle)); 677 640 spans.addAll(_buildSpans(context, value[i], indent + 1)); 678 641 if (i < value.length - 1) { 679 - spans.add( 680 - TextSpan( 681 - text: ',', 682 - style: TextStyle(color: surfaceVariant), 683 - ), 684 - ); 642 + spans.add(TextSpan(text: ',', style: keyStyle)); 685 643 } 686 - spans.add( 687 - TextSpan( 688 - text: '\n', 689 - style: TextStyle(color: surfaceVariant), 690 - ), 691 - ); 644 + spans.add(TextSpan(text: '\n', style: keyStyle)); 692 645 } 693 - spans.add( 694 - TextSpan( 695 - text: ' ' * indent + ']', 696 - style: TextStyle(color: surfaceVariant), 697 - ), 698 - ); 646 + spans.add(TextSpan(text: ' ' * indent + ']', style: keyStyle)); 699 647 return spans; 700 648 } 701 649 702 650 if (value is String) { 703 - return [ 704 - TextSpan( 705 - text: '"$value"', 706 - style: TextStyle(color: theme.colorScheme.primaryContainer), 707 - ), 708 - ]; 651 + return [TextSpan(text: '"$value"', style: strStyle)]; 709 652 } 710 653 711 654 if (value is num) { 712 - return [ 713 - TextSpan( 714 - text: value.toString(), 715 - style: TextStyle(color: theme.colorScheme.tertiary), 716 - ), 717 - ]; 655 + return [TextSpan(text: value.toString(), style: numStyle)]; 718 656 } 719 657 720 658 if (value is bool) { 721 - return [ 722 - TextSpan( 723 - text: value.toString(), 724 - style: TextStyle(color: theme.colorScheme.secondary), 725 - ), 726 - ]; 659 + return [TextSpan(text: value.toString(), style: boolStyle)]; 727 660 } 728 661 729 662 if (value == null) { 730 - return [ 731 - TextSpan( 732 - text: 'null', 733 - style: TextStyle(color: surfaceVariant), 734 - ), 735 - ]; 663 + return [TextSpan(text: 'null', style: nullStyle)]; 736 664 } 737 665 738 666 return [TextSpan(text: value.toString())];
+118
lib/features/settings/presentation/about_screen.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:flutter_svg/flutter_svg.dart'; 3 + import 'package:url_launcher/url_launcher.dart'; 4 + 5 + class AboutScreen extends StatelessWidget { 6 + const AboutScreen({super.key}); 7 + 8 + static const _linkedInUrl = 'https://linkedin.com/in/owais-jamil'; 9 + static const _githubUrl = 'https://github.com/stormlightlabs/lazurite'; 10 + static const _tangledUrl = 'https://tangled.org/desertthunder.dev/lazurite'; 11 + static const _emailUrl = 'mailto:info@stormlightlabs.org'; 12 + 13 + Future<void> _launch(String url) async { 14 + await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); 15 + } 16 + 17 + @override 18 + Widget build(BuildContext context) { 19 + final theme = Theme.of(context); 20 + 21 + return Scaffold( 22 + appBar: AppBar(title: const Text('About')), 23 + body: ListView( 24 + padding: const EdgeInsets.all(24), 25 + children: [ 26 + Center( 27 + child: SvgPicture.asset( 28 + 'assets/logo.svg', 29 + width: 64, 30 + height: 64, 31 + colorFilter: ColorFilter.mode(theme.colorScheme.primary, BlendMode.srcIn), 32 + ), 33 + ), 34 + const SizedBox(height: 16), 35 + Text( 36 + 'Lazurite', 37 + textAlign: TextAlign.center, 38 + style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700), 39 + ), 40 + const SizedBox(height: 16), 41 + Text('Lazurite is made at Stormlight Labs, which is just me:', style: theme.textTheme.bodyLarge), 42 + const SizedBox(height: 8), 43 + GestureDetector( 44 + onTap: () => _launch(_linkedInUrl), 45 + child: Text( 46 + 'Owais', 47 + style: theme.textTheme.bodyLarge?.copyWith( 48 + color: theme.colorScheme.primary, 49 + decoration: TextDecoration.underline, 50 + decorationColor: theme.colorScheme.primary, 51 + ), 52 + ), 53 + ), 54 + const SizedBox(height: 24), 55 + Text( 56 + 'Stormlight Labs makes high quality, free and open source software. ' 57 + 'Check out our other projects on GitHub.', 58 + style: theme.textTheme.bodyMedium, 59 + ), 60 + const SizedBox(height: 8), 61 + Text( 62 + 'You can view or contribute to this project on Tangled or GitHub. ' 63 + 'Feature requests and bug reports are welcome!', 64 + style: theme.textTheme.bodyMedium, 65 + ), 66 + const SizedBox(height: 32), 67 + Row( 68 + mainAxisAlignment: MainAxisAlignment.center, 69 + children: [ 70 + _LinkIcon( 71 + onTap: () => _launch(_githubUrl), 72 + child: SvgPicture.asset( 73 + 'assets/gh.svg', 74 + width: 28, 75 + height: 28, 76 + colorFilter: ColorFilter.mode(theme.colorScheme.onSurface, BlendMode.srcIn), 77 + ), 78 + ), 79 + const SizedBox(width: 24), 80 + _LinkIcon( 81 + onTap: () => _launch(_tangledUrl), 82 + child: SvgPicture.asset( 83 + 'assets/tangled.svg', 84 + width: 28, 85 + height: 28, 86 + colorFilter: ColorFilter.mode(theme.colorScheme.onSurface, BlendMode.srcIn), 87 + ), 88 + ), 89 + const SizedBox(width: 24), 90 + _LinkIcon( 91 + onTap: () => _launch(_emailUrl), 92 + child: Icon(Icons.email_outlined, size: 28, color: theme.colorScheme.onSurface), 93 + ), 94 + ], 95 + ), 96 + const SizedBox(height: 32), 97 + Center(child: Text('Lazurite v1.0.0', style: theme.textTheme.bodySmall)), 98 + ], 99 + ), 100 + ); 101 + } 102 + } 103 + 104 + class _LinkIcon extends StatelessWidget { 105 + const _LinkIcon({required this.onTap, required this.child}); 106 + 107 + final VoidCallback onTap; 108 + final Widget child; 109 + 110 + @override 111 + Widget build(BuildContext context) { 112 + return InkWell( 113 + onTap: onTap, 114 + borderRadius: BorderRadius.circular(12), 115 + child: Padding(padding: const EdgeInsets.all(8), child: child), 116 + ); 117 + } 118 + }
+94 -134
lib/features/settings/presentation/settings_screen.dart
··· 82 82 onTap: () => context.push('/settings/logs'), 83 83 ), 84 84 _SettingsTile(icon: Icons.help_outline, title: 'Help & Support', onTap: () {}), 85 - _SettingsTile(icon: Icons.security_outlined, title: 'Privacy Policy', onTap: () {}), 85 + _SettingsTile( 86 + icon: Icons.info_outline, 87 + title: 'About', 88 + subtitle: 'Stormlight Labs', 89 + onTap: () => context.push('/settings/about'), 90 + ), 86 91 const SizedBox(height: 24), 87 92 _buildSectionHeader(context, 'Danger Zone'), 88 93 _SettingsTile( ··· 95 100 }, 96 101 ), 97 102 const SizedBox(height: 24), 98 - Center(child: Text('Lazurite v1.0.0 (Phase 2)', style: Theme.of(context).textTheme.bodySmall)), 103 + Center(child: Text('Lazurite v1.0.0', style: Theme.of(context).textTheme.bodySmall)), 99 104 const SizedBox(height: 24), 100 105 ], 101 106 ), ··· 115 120 Widget _buildThemeSelector(BuildContext context) { 116 121 final settingsCubit = context.read<SettingsCubit>(); 117 122 118 - return Container( 119 - decoration: BoxDecoration( 120 - border: Border( 121 - top: BorderSide(color: Theme.of(context).dividerColor), 122 - bottom: BorderSide(color: Theme.of(context).dividerColor), 123 - ), 124 - color: Theme.of(context).cardColor, 125 - ), 126 - child: Column( 127 - children: [ 128 - Padding(padding: const EdgeInsets.all(16), child: _buildThemeGrid(context, settingsCubit)), 129 - const Divider(height: 1), 130 - BlocBuilder<SettingsCubit, SettingsState>( 131 - buildWhen: (prev, curr) => prev.useSystemTheme != curr.useSystemTheme, 132 - builder: (context, state) { 133 - return _SettingsTile( 134 - title: 'Auto', 135 - subtitle: 'Follow system theme', 136 - trailing: Switch( 137 - value: state.useSystemTheme, 138 - onChanged: (value) => settingsCubit.setUseSystemTheme(value), 139 - ), 140 - ); 141 - }, 142 - ), 143 - ], 144 - ), 145 - ); 146 - } 147 - 148 - Widget _buildThemeGrid(BuildContext context, SettingsCubit settingsCubit) { 149 123 return BlocBuilder<SettingsCubit, SettingsState>( 150 124 builder: (context, state) { 151 - return Column( 152 - children: [ 153 - _buildThemeRow(context, settingsCubit, 'Oxocarbon', AppThemePalette.oxocarbon, state), 154 - const SizedBox(height: 16), 155 - _buildThemeRow(context, settingsCubit, 'Catppuccin', AppThemePalette.catppuccin, state), 156 - const SizedBox(height: 16), 157 - _buildThemeRow(context, settingsCubit, 'Nord', AppThemePalette.nord, state), 158 - const SizedBox(height: 16), 159 - _buildThemeRow(context, settingsCubit, 'Rosé Pine', AppThemePalette.rosePine, state), 160 - ], 125 + return Container( 126 + decoration: BoxDecoration( 127 + border: Border( 128 + top: BorderSide(color: Theme.of(context).dividerColor), 129 + bottom: BorderSide(color: Theme.of(context).dividerColor), 130 + ), 131 + color: Theme.of(context).cardColor, 132 + ), 133 + child: Column( 134 + children: [ 135 + Padding( 136 + padding: const EdgeInsets.all(16), 137 + child: SizedBox( 138 + width: double.infinity, 139 + child: SegmentedButton<_AppearanceMode>( 140 + segments: const [ 141 + ButtonSegment(value: _AppearanceMode.system, label: Text('System')), 142 + ButtonSegment(value: _AppearanceMode.light, label: Text('Light')), 143 + ButtonSegment(value: _AppearanceMode.dark, label: Text('Dark')), 144 + ], 145 + selected: {_AppearanceMode.fromState(state)}, 146 + onSelectionChanged: (selected) { 147 + final mode = selected.first; 148 + switch (mode) { 149 + case _AppearanceMode.system: 150 + settingsCubit.setUseSystemTheme(true); 151 + case _AppearanceMode.light: 152 + settingsCubit.setUseSystemTheme(false); 153 + settingsCubit.setThemeVariant(AppThemeVariant.light); 154 + case _AppearanceMode.dark: 155 + settingsCubit.setUseSystemTheme(false); 156 + settingsCubit.setThemeVariant(AppThemeVariant.dark); 157 + } 158 + }, 159 + ), 160 + ), 161 + ), 162 + const Divider(height: 1), 163 + Padding( 164 + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 165 + child: Align( 166 + alignment: Alignment.centerLeft, 167 + child: Text( 168 + 'THEME', 169 + style: Theme.of( 170 + context, 171 + ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 172 + ), 173 + ), 174 + ), 175 + for (final palette in AppThemePalette.values) 176 + _ThemePaletteRow( 177 + palette: palette, 178 + isSelected: state.themePalette == palette, 179 + onTap: () => settingsCubit.setThemePalette(palette), 180 + ), 181 + const SizedBox(height: 8), 182 + ], 183 + ), 161 184 ); 162 185 }, 163 186 ); 164 187 } 188 + } 165 189 166 - Widget _buildThemeRow( 167 - BuildContext context, 168 - SettingsCubit settingsCubit, 169 - String label, 170 - AppThemePalette palette, 171 - SettingsState state, 172 - ) { 173 - return Column( 174 - crossAxisAlignment: CrossAxisAlignment.start, 175 - children: [ 176 - Text( 177 - label.toUpperCase(), 178 - style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 179 - ), 180 - const SizedBox(height: 8), 181 - Row( 182 - children: [ 183 - Expanded( 184 - child: _ThemeOption( 185 - palette: palette, 186 - variant: AppThemeVariant.light, 187 - label: 'Light', 188 - isSelected: state.themePalette == palette && state.themeVariant == AppThemeVariant.light, 189 - onTap: () => settingsCubit.setTheme(palette, AppThemeVariant.light), 190 - ), 191 - ), 192 - const SizedBox(width: 8), 193 - Expanded( 194 - child: _ThemeOption( 195 - palette: palette, 196 - variant: AppThemeVariant.dark, 197 - label: 'Dark', 198 - isSelected: state.themePalette == palette && state.themeVariant == AppThemeVariant.dark, 199 - onTap: () => settingsCubit.setTheme(palette, AppThemeVariant.dark), 200 - ), 201 - ), 202 - ], 203 - ), 204 - ], 205 - ); 190 + enum _AppearanceMode { 191 + system, 192 + light, 193 + dark; 194 + 195 + static _AppearanceMode fromState(SettingsState state) { 196 + if (state.useSystemTheme) return system; 197 + return state.themeVariant == AppThemeVariant.light ? light : dark; 206 198 } 207 199 } 208 200 209 - class _ThemeOption extends StatelessWidget { 210 - const _ThemeOption({ 211 - required this.palette, 212 - required this.variant, 213 - required this.label, 214 - required this.isSelected, 215 - required this.onTap, 216 - }); 201 + class _ThemePaletteRow extends StatelessWidget { 202 + const _ThemePaletteRow({required this.palette, required this.isSelected, required this.onTap}); 217 203 218 204 final AppThemePalette palette; 219 - final AppThemeVariant variant; 220 - final String label; 221 205 final bool isSelected; 222 206 final VoidCallback onTap; 223 207 224 208 @override 225 209 Widget build(BuildContext context) { 226 - final theme = AppTheme.getTheme(palette, variant); 210 + final swatches = AppTheme.getSwatchColors(palette); 227 211 228 - return InkWell( 212 + return ListTile( 229 213 onTap: onTap, 230 - borderRadius: BorderRadius.circular(12), 231 - child: Container( 232 - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), 233 - decoration: BoxDecoration( 234 - borderRadius: BorderRadius.circular(12), 235 - color: isSelected ? Theme.of(context).colorScheme.surfaceContainerHighest : null, 236 - ), 237 - child: Column( 238 - children: [ 239 - Container( 240 - width: 64, 241 - height: 64, 242 - decoration: BoxDecoration( 243 - borderRadius: BorderRadius.circular(12), 244 - border: Border.all( 245 - color: isSelected 246 - ? Theme.of(context).colorScheme.primary 247 - : Theme.of(context).colorScheme.outlineVariant, 248 - width: isSelected ? 2 : 1, 249 - ), 250 - color: theme.scaffoldBackgroundColor, 251 - ), 252 - child: Stack( 253 - children: [ 254 - if (isSelected) 255 - Positioned( 256 - bottom: 4, 257 - right: 4, 258 - child: Container( 259 - width: 20, 260 - height: 20, 261 - decoration: BoxDecoration(color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle), 262 - child: Icon(Icons.check, size: 12, color: Theme.of(context).colorScheme.onPrimary), 263 - ), 264 - ), 265 - ], 214 + title: Text(AppTheme.getPaletteName(palette)), 215 + trailing: Row( 216 + mainAxisSize: MainAxisSize.min, 217 + children: [ 218 + for (final color in swatches) 219 + Padding( 220 + padding: const EdgeInsets.only(left: 4), 221 + child: Container( 222 + width: 20, 223 + height: 20, 224 + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)), 266 225 ), 267 226 ), 268 - const SizedBox(height: 8), 269 - Text(label, style: Theme.of(context).textTheme.labelMedium), 227 + if (isSelected) ...[ 228 + const SizedBox(width: 12), 229 + Icon(Icons.check, color: Theme.of(context).colorScheme.primary, size: 20), 270 230 ], 271 - ), 231 + ], 272 232 ), 273 233 ); 274 234 }
+29 -8
lib/main.dart
··· 1 1 import 'package:bluesky/bluesky.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:flutter_bloc/flutter_bloc.dart'; 4 + import 'package:go_router/go_router.dart'; 4 5 import 'package:lazurite/core/database/app_database.dart'; 5 6 import 'package:lazurite/core/logging/app_logger.dart'; 6 7 import 'package:lazurite/core/logging/logging_bloc_observer.dart'; ··· 43 44 runApp(LazuriteApp(authBloc: authBloc, database: database, settingsCubit: settingsCubit)); 44 45 } 45 46 46 - class LazuriteApp extends StatelessWidget { 47 + class LazuriteApp extends StatefulWidget { 47 48 const LazuriteApp({super.key, required this.authBloc, required this.database, required this.settingsCubit}); 48 49 49 50 final AuthBloc authBloc; 50 51 final AppDatabase database; 51 52 final SettingsCubit settingsCubit; 52 53 54 + @override 55 + State<LazuriteApp> createState() => _LazuriteAppState(); 56 + } 57 + 58 + class _LazuriteAppState extends State<LazuriteApp> { 53 59 static final _navigatorObserver = LoggingNavigatorObserver(); 60 + late final GoRouter _router; 61 + 62 + @override 63 + void initState() { 64 + super.initState(); 65 + _router = AppRouter(authBloc: widget.authBloc, navigatorObserver: _navigatorObserver).router; 66 + } 67 + 68 + @override 69 + void dispose() { 70 + _router.dispose(); 71 + super.dispose(); 72 + } 54 73 55 74 Bluesky? _createBluesky(AuthState state) { 56 75 if (!state.isAuthenticated) { ··· 64 83 Widget build(BuildContext context) { 65 84 return MultiBlocProvider( 66 85 providers: [ 67 - BlocProvider.value(value: authBloc), 68 - BlocProvider.value(value: settingsCubit), 86 + BlocProvider.value(value: widget.authBloc), 87 + BlocProvider.value(value: widget.settingsCubit), 69 88 ], 70 89 child: BlocBuilder<AuthBloc, AuthState>( 71 90 builder: (context, authState) { ··· 85 104 theme: lightTheme, 86 105 darkTheme: darkTheme, 87 106 themeMode: themeMode, 88 - routerConfig: AppRouter(authBloc: authBloc, navigatorObserver: _navigatorObserver).router, 107 + routerConfig: _router, 89 108 ); 90 109 }, 91 110 ); ··· 101 120 providers: [ 102 121 BlocProvider( 103 122 create: (_) => ProfileBloc( 104 - profileRepository: ProfileRepository(database: database, bluesky: bluesky), 123 + profileRepository: ProfileRepository(database: widget.database, bluesky: bluesky), 105 124 ), 106 125 ), 107 126 BlocProvider(create: (_) => FeedBloc(feedRepository: feedRepository)), 108 127 BlocProvider( 109 - create: (_) => 110 - FeedPreferencesCubit(feedRepository: feedRepository, database: database, accountDid: accountDid) 111 - ..loadPreferences(), 128 + create: (_) => FeedPreferencesCubit( 129 + feedRepository: feedRepository, 130 + database: widget.database, 131 + accountDid: accountDid, 132 + )..loadPreferences(), 112 133 ), 113 134 BlocProvider(create: (_) => DevToolsCubit(atproto: bluesky.atproto)), 114 135 RepositoryProvider.value(value: feedRepository),
+56
pubspec.lock
··· 390 390 url: "https://pub.dev" 391 391 source: hosted 392 392 version: "6.0.0" 393 + flutter_svg: 394 + dependency: "direct main" 395 + description: 396 + name: flutter_svg 397 + sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" 398 + url: "https://pub.dev" 399 + source: hosted 400 + version: "2.2.4" 393 401 flutter_test: 394 402 dependency: "direct dev" 395 403 description: flutter ··· 664 672 url: "https://pub.dev" 665 673 source: hosted 666 674 version: "1.9.1" 675 + path_parsing: 676 + dependency: transitive 677 + description: 678 + name: path_parsing 679 + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" 680 + url: "https://pub.dev" 681 + source: hosted 682 + version: "1.1.0" 667 683 path_provider: 668 684 dependency: "direct main" 669 685 description: ··· 712 728 url: "https://pub.dev" 713 729 source: hosted 714 730 version: "2.3.0" 731 + petitparser: 732 + dependency: transitive 733 + description: 734 + name: petitparser 735 + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" 736 + url: "https://pub.dev" 737 + source: hosted 738 + version: "7.0.2" 715 739 platform: 716 740 dependency: transitive 717 741 description: ··· 1037 1061 url: "https://pub.dev" 1038 1062 source: hosted 1039 1063 version: "4.5.3" 1064 + vector_graphics: 1065 + dependency: transitive 1066 + description: 1067 + name: vector_graphics 1068 + sha256: "7076216a10d5c390315fbe536a30f1254c341e7543e6c4c8a815e591307772b1" 1069 + url: "https://pub.dev" 1070 + source: hosted 1071 + version: "1.1.20" 1072 + vector_graphics_codec: 1073 + dependency: transitive 1074 + description: 1075 + name: vector_graphics_codec 1076 + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" 1077 + url: "https://pub.dev" 1078 + source: hosted 1079 + version: "1.1.13" 1080 + vector_graphics_compiler: 1081 + dependency: transitive 1082 + description: 1083 + name: vector_graphics_compiler 1084 + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" 1085 + url: "https://pub.dev" 1086 + source: hosted 1087 + version: "1.2.0" 1040 1088 vector_math: 1041 1089 dependency: transitive 1042 1090 description: ··· 1109 1157 url: "https://pub.dev" 1110 1158 source: hosted 1111 1159 version: "1.1.0" 1160 + xml: 1161 + dependency: transitive 1162 + description: 1163 + name: xml 1164 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" 1165 + url: "https://pub.dev" 1166 + source: hosted 1167 + version: "6.6.1" 1112 1168 xrpc: 1113 1169 dependency: transitive 1114 1170 description:
+4
pubspec.yaml
··· 32 32 share_plus: ^10.1.4 33 33 http: ^1.2.2 34 34 uuid: ^4.5.1 35 + flutter_svg: ^2.2.4 35 36 36 37 dev_dependencies: 37 38 flutter_test: ··· 46 47 47 48 flutter: 48 49 uses-material-design: true 50 + 51 + assets: 52 + - assets/