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: authentication, database integration and routing

+3816 -732
+606 -489
docs/designs/devtools.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 6 - <title>Dev Tools - Lazurite</title> 7 - <link rel="preconnect" href="https://fonts.googleapis.com"> 8 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 - <link href="https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> 10 - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/geist@1.2.2/dist/fonts/geist-sans/style.css"> 11 - <link rel="stylesheet" href="styles.css"> 12 - <style> 13 - .devtools-container { 14 - padding-bottom: 88px; 15 - } 16 - 17 - .devtools-search { 18 - padding: 16px; 19 - border-bottom: 1px solid var(--border); 20 - } 21 - 22 - .devtools-search-row { 23 - display: flex; 24 - gap: 8px; 25 - } 26 - 27 - .devtools-search-row .input { 28 - flex: 1; 29 - font-family: 'JetBrains Mono', monospace; 30 - font-size: 13px; 31 - } 32 - 33 - .devtools-search-btn { 34 - padding: 12px 16px; 35 - border-radius: 8px; 36 - border: none; 37 - background-color: var(--accent-primary); 38 - color: white; 39 - font-weight: 600; 40 - font-size: 14px; 41 - cursor: pointer; 42 - transition: background-color 0.2s ease; 43 - white-space: nowrap; 44 - } 45 - 46 - .devtools-search-btn:hover { 47 - background-color: var(--accent-primary-hover); 48 - } 49 - 50 - .devtools-tabs { 51 - display: flex; 52 - border-bottom: 1px solid var(--border); 53 - background-color: var(--bg); 54 - } 55 - 56 - .devtools-tab { 57 - flex: 1; 58 - padding: 12px; 59 - text-align: center; 60 - font-weight: 600; 61 - font-size: 13px; 62 - color: var(--text-secondary); 63 - cursor: pointer; 64 - border-bottom: 2px solid transparent; 65 - transition: all 0.2s ease; 66 - background: none; 67 - border-top: none; 68 - border-left: none; 69 - border-right: none; 70 - } 71 - 72 - .devtools-tab:hover { 73 - background-color: var(--surface); 74 - color: var(--text-primary); 75 - } 76 - 77 - .devtools-tab.active { 78 - color: var(--text-primary); 79 - border-bottom-color: var(--accent-primary); 80 - } 81 - 82 - /* Repo Overview */ 83 - .repo-header { 84 - padding: 16px; 85 - border-bottom: 1px solid var(--border); 86 - background-color: var(--surface); 87 - } 88 - 89 - .repo-identity { 90 - display: flex; 91 - align-items: center; 92 - gap: 12px; 93 - margin-bottom: 12px; 94 - } 95 - 96 - .repo-avatar { 97 - width: 40px; 98 - height: 40px; 99 - border-radius: 50%; 100 - background-color: var(--surface-variant); 101 - display: flex; 102 - align-items: center; 103 - justify-content: center; 104 - font-weight: 600; 105 - font-size: 14px; 106 - color: var(--text-secondary); 107 - flex-shrink: 0; 108 - } 109 - 110 - .repo-names { 111 - flex: 1; 112 - min-width: 0; 113 - } 114 - 115 - .repo-handle { 116 - font-weight: 600; 117 - font-size: 15px; 118 - color: var(--text-primary); 119 - } 120 - 121 - .repo-did { 122 - font-family: 'JetBrains Mono', monospace; 123 - font-size: 11px; 124 - color: var(--text-muted); 125 - overflow: hidden; 126 - text-overflow: ellipsis; 127 - white-space: nowrap; 128 - } 129 - 130 - .repo-stats { 131 - display: flex; 132 - gap: 16px; 133 - } 134 - 135 - .repo-stat { 136 - font-size: 13px; 137 - color: var(--text-secondary); 138 - } 139 - 140 - .repo-stat strong { 141 - color: var(--text-primary); 142 - font-weight: 600; 143 - } 144 - 145 - /* Collection List */ 146 - .collection-item { 147 - display: flex; 148 - align-items: center; 149 - justify-content: space-between; 150 - padding: 14px 16px; 151 - border-bottom: 1px solid var(--border); 152 - cursor: pointer; 153 - transition: background-color 0.2s ease; 154 - } 155 - 156 - .collection-item:hover { 157 - background-color: var(--surface); 158 - } 159 - 160 - .collection-item-left { 161 - display: flex; 162 - align-items: center; 163 - gap: 12px; 164 - min-width: 0; 165 - flex: 1; 166 - } 167 - 168 - .collection-icon { 169 - width: 32px; 170 - height: 32px; 171 - border-radius: 6px; 172 - background-color: var(--surface); 173 - border: 1px solid var(--border); 174 - display: flex; 175 - align-items: center; 176 - justify-content: center; 177 - flex-shrink: 0; 178 - } 179 - 180 - .collection-icon svg { 181 - width: 16px; 182 - height: 16px; 183 - color: var(--text-secondary); 184 - } 185 - 186 - .collection-name { 187 - font-family: 'JetBrains Mono', monospace; 188 - font-size: 13px; 189 - font-weight: 500; 190 - color: var(--text-primary); 191 - overflow: hidden; 192 - text-overflow: ellipsis; 193 - white-space: nowrap; 194 - } 195 - 196 - .collection-count { 197 - font-size: 12px; 198 - color: var(--text-muted); 199 - background-color: var(--surface); 200 - padding: 2px 8px; 201 - border-radius: 9999px; 202 - flex-shrink: 0; 203 - } 204 - 205 - .collection-chevron { 206 - color: var(--text-muted); 207 - flex-shrink: 0; 208 - margin-left: 8px; 209 - } 210 - 211 - .collection-chevron svg { 212 - width: 16px; 213 - height: 16px; 214 - } 215 - 216 - /* Record Inspector */ 217 - .record-header { 218 - padding: 12px 16px; 219 - background-color: var(--surface); 220 - border-bottom: 1px solid var(--border); 221 - display: flex; 222 - align-items: center; 223 - justify-content: space-between; 224 - } 225 - 226 - .record-breadcrumb { 227 - font-family: 'JetBrains Mono', monospace; 228 - font-size: 12px; 229 - color: var(--text-secondary); 230 - overflow: hidden; 231 - text-overflow: ellipsis; 232 - white-space: nowrap; 233 - } 234 - 235 - .record-breadcrumb span { 236 - color: var(--accent-primary); 237 - } 238 - 239 - .record-copy-btn { 240 - padding: 4px 10px; 241 - border-radius: 4px; 242 - border: 1px solid var(--border); 243 - background-color: var(--bg); 244 - color: var(--text-secondary); 245 - font-size: 11px; 246 - font-weight: 500; 247 - cursor: pointer; 248 - transition: all 0.2s ease; 249 - } 250 - 251 - .record-copy-btn:hover { 252 - background-color: var(--surface-variant); 253 - color: var(--text-primary); 254 - } 255 - 256 - .json-viewer { 257 - padding: 16px; 258 - font-family: 'JetBrains Mono', monospace; 259 - font-size: 12px; 260 - line-height: 1.8; 261 - overflow-x: auto; 262 - } 263 - 264 - .json-key { color: var(--accent-primary); } 265 - .json-string { color: var(--accent-success); } 266 - .json-number { color: var(--accent-secondary); } 267 - .json-bool { color: var(--accent-warning); } 268 - .json-null { color: var(--text-muted); } 269 - .json-brace { color: var(--text-secondary); } 270 - 271 - /* Section Label */ 272 - .section-label { 273 - padding: 8px 16px; 274 - font-size: 12px; 275 - font-weight: 600; 276 - color: var(--text-muted); 277 - text-transform: uppercase; 278 - letter-spacing: 0.5px; 279 - background-color: var(--surface); 280 - border-bottom: 1px solid var(--border); 281 - } 282 - </style> 283 - </head> 284 - <body> 285 - <div class="mobile-container"> 286 - 287 - <!-- Header --> 288 - <header class="header"> 289 - <button class="header-action">← Back</button> 290 - <h1 class="header-title">PDS Explorer</h1> 291 - <span class="debug-badge">Debug</span> 292 - </header> 293 - 294 - <!-- Search / AT-URI Input --> 295 - <div class="devtools-search"> 296 - <div class="devtools-search-row"> 297 - <input class="input" type="text" placeholder="Handle, DID, or at:// URI" value="alice.bsky.social"> 298 - <button class="devtools-search-btn">Resolve</button> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> 6 + <title>Dev Tools - Lazurite</title> 7 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 8 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 9 + <link 10 + href="https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" 11 + rel="stylesheet" /> 12 + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/geist@1.2.2/dist/fonts/geist-sans/style.css" /> 13 + <link rel="stylesheet" href="styles.css" /> 14 + <style> 15 + .devtools-container { 16 + padding-bottom: 88px; 17 + } 18 + 19 + .devtools-search { 20 + padding: 16px; 21 + border-bottom: 1px solid var(--border); 22 + } 23 + 24 + .devtools-search-row { 25 + display: flex; 26 + gap: 8px; 27 + } 28 + 29 + .devtools-search-row .input { 30 + flex: 1; 31 + font-family: "JetBrains Mono", monospace; 32 + font-size: 13px; 33 + } 34 + 35 + .devtools-search-btn { 36 + padding: 12px 16px; 37 + border-radius: 8px; 38 + border: none; 39 + background-color: var(--accent-primary); 40 + color: white; 41 + font-weight: 600; 42 + font-size: 14px; 43 + cursor: pointer; 44 + transition: background-color 0.2s ease; 45 + white-space: nowrap; 46 + } 47 + 48 + .devtools-search-btn:hover { 49 + background-color: var(--accent-primary-hover); 50 + } 51 + 52 + .devtools-tabs { 53 + display: flex; 54 + border-bottom: 1px solid var(--border); 55 + background-color: var(--bg); 56 + } 57 + 58 + .devtools-tab { 59 + flex: 1; 60 + padding: 12px; 61 + text-align: center; 62 + font-weight: 600; 63 + font-size: 13px; 64 + color: var(--text-secondary); 65 + cursor: pointer; 66 + border-bottom: 2px solid transparent; 67 + transition: all 0.2s ease; 68 + background: none; 69 + border-top: none; 70 + border-left: none; 71 + border-right: none; 72 + } 73 + 74 + .devtools-tab:hover { 75 + background-color: var(--surface); 76 + color: var(--text-primary); 77 + } 78 + 79 + .devtools-tab.active { 80 + color: var(--text-primary); 81 + border-bottom-color: var(--accent-primary); 82 + } 83 + 84 + /* Repo Overview */ 85 + .repo-header { 86 + padding: 16px; 87 + border-bottom: 1px solid var(--border); 88 + background-color: var(--surface); 89 + } 90 + 91 + .repo-identity { 92 + display: flex; 93 + align-items: center; 94 + gap: 12px; 95 + margin-bottom: 12px; 96 + } 97 + 98 + .repo-avatar { 99 + width: 40px; 100 + height: 40px; 101 + border-radius: 50%; 102 + background-color: var(--surface-variant); 103 + display: flex; 104 + align-items: center; 105 + justify-content: center; 106 + font-weight: 600; 107 + font-size: 14px; 108 + color: var(--text-secondary); 109 + flex-shrink: 0; 110 + } 111 + 112 + .repo-names { 113 + flex: 1; 114 + min-width: 0; 115 + } 116 + 117 + .repo-handle { 118 + font-weight: 600; 119 + font-size: 15px; 120 + color: var(--text-primary); 121 + } 122 + 123 + .repo-did { 124 + font-family: "JetBrains Mono", monospace; 125 + font-size: 11px; 126 + color: var(--text-muted); 127 + overflow: hidden; 128 + text-overflow: ellipsis; 129 + white-space: nowrap; 130 + } 131 + 132 + .repo-stats { 133 + display: flex; 134 + gap: 16px; 135 + } 136 + 137 + .repo-stat { 138 + font-size: 13px; 139 + color: var(--text-secondary); 140 + } 141 + 142 + .repo-stat strong { 143 + color: var(--text-primary); 144 + font-weight: 600; 145 + } 146 + 147 + /* Collection List */ 148 + .collection-item { 149 + display: flex; 150 + align-items: center; 151 + justify-content: space-between; 152 + padding: 14px 16px; 153 + border-bottom: 1px solid var(--border); 154 + cursor: pointer; 155 + transition: background-color 0.2s ease; 156 + } 157 + 158 + .collection-item:hover { 159 + background-color: var(--surface); 160 + } 161 + 162 + .collection-item-left { 163 + display: flex; 164 + align-items: center; 165 + gap: 12px; 166 + min-width: 0; 167 + flex: 1; 168 + } 169 + 170 + .collection-icon { 171 + width: 32px; 172 + height: 32px; 173 + border-radius: 6px; 174 + background-color: var(--surface); 175 + border: 1px solid var(--border); 176 + display: flex; 177 + align-items: center; 178 + justify-content: center; 179 + flex-shrink: 0; 180 + } 181 + 182 + .collection-icon svg { 183 + width: 16px; 184 + height: 16px; 185 + color: var(--text-secondary); 186 + } 187 + 188 + .collection-name { 189 + font-family: "JetBrains Mono", monospace; 190 + font-size: 13px; 191 + font-weight: 500; 192 + color: var(--text-primary); 193 + overflow: hidden; 194 + text-overflow: ellipsis; 195 + white-space: nowrap; 196 + } 197 + 198 + .collection-count { 199 + font-size: 12px; 200 + color: var(--text-muted); 201 + background-color: var(--surface); 202 + padding: 2px 8px; 203 + border-radius: 9999px; 204 + flex-shrink: 0; 205 + } 206 + 207 + .collection-chevron { 208 + color: var(--text-muted); 209 + flex-shrink: 0; 210 + margin-left: 8px; 211 + } 212 + 213 + .collection-chevron svg { 214 + width: 16px; 215 + height: 16px; 216 + } 217 + 218 + /* Record Inspector */ 219 + .record-header { 220 + padding: 12px 16px; 221 + background-color: var(--surface); 222 + border-bottom: 1px solid var(--border); 223 + display: flex; 224 + align-items: center; 225 + justify-content: space-between; 226 + } 227 + 228 + .record-breadcrumb { 229 + font-family: "JetBrains Mono", monospace; 230 + font-size: 12px; 231 + color: var(--text-secondary); 232 + overflow: hidden; 233 + text-overflow: ellipsis; 234 + white-space: nowrap; 235 + } 236 + 237 + .record-breadcrumb span { 238 + color: var(--accent-primary); 239 + } 240 + 241 + .record-copy-btn { 242 + padding: 4px 10px; 243 + border-radius: 4px; 244 + border: 1px solid var(--border); 245 + background-color: var(--bg); 246 + color: var(--text-secondary); 247 + font-size: 11px; 248 + font-weight: 500; 249 + cursor: pointer; 250 + transition: all 0.2s ease; 251 + } 252 + 253 + .record-copy-btn:hover { 254 + background-color: var(--surface-variant); 255 + color: var(--text-primary); 256 + } 257 + 258 + .json-viewer { 259 + padding: 16px; 260 + font-family: "JetBrains Mono", monospace; 261 + font-size: 12px; 262 + line-height: 1.8; 263 + overflow-x: auto; 264 + } 265 + 266 + .json-key { 267 + color: var(--accent-primary); 268 + } 269 + .json-string { 270 + color: var(--accent-success); 271 + } 272 + .json-number { 273 + color: var(--accent-secondary); 274 + } 275 + .json-bool { 276 + color: var(--accent-warning); 277 + } 278 + .json-null { 279 + color: var(--text-muted); 280 + } 281 + .json-brace { 282 + color: var(--text-secondary); 283 + } 284 + 285 + /* Section Label */ 286 + .section-label { 287 + padding: 8px 16px; 288 + font-size: 12px; 289 + font-weight: 600; 290 + color: var(--text-muted); 291 + text-transform: uppercase; 292 + letter-spacing: 0.5px; 293 + background-color: var(--surface); 294 + border-bottom: 1px solid var(--border); 295 + } 296 + </style> 297 + </head> 298 + <body> 299 + <div class="mobile-container"> 300 + <!-- Header --> 301 + <header class="header"> 302 + <button class="header-action">← Back</button> 303 + <h1 class="header-title">PDS Explorer</h1> 304 + <button class="header-action">⋯</button> 305 + </header> 306 + 307 + <!-- Search / AT-URI Input --> 308 + <div class="devtools-search"> 309 + <div class="devtools-search-row"> 310 + <input class="input" type="text" placeholder="Handle, DID, or at:// URI" value="alice.bsky.social" /> 311 + <button class="devtools-search-btn">Resolve</button> 312 + </div> 313 + </div> 314 + 315 + <!-- Tabs --> 316 + <div class="devtools-tabs"> 317 + <button class="devtools-tab active">Repo</button> 318 + <button class="devtools-tab">Records</button> 319 + <button class="devtools-tab">JSON</button> 299 320 </div> 300 - </div> 301 - 302 - <!-- Tabs --> 303 - <div class="devtools-tabs"> 304 - <button class="devtools-tab active">Repo</button> 305 - <button class="devtools-tab">Records</button> 306 - <button class="devtools-tab">JSON</button> 307 - </div> 308 - 309 - <div class="devtools-container"> 310 - 311 - <!-- Repo Overview --> 312 - <div class="repo-header"> 313 - <div class="repo-identity"> 314 - <div class="repo-avatar">AS</div> 315 - <div class="repo-names"> 316 - <div class="repo-handle">alice.bsky.social</div> 317 - <div class="repo-did">did:plc:z72i7hdynmk6r22z27h6tvur</div> 321 + 322 + <div class="devtools-container"> 323 + <!-- Repo Overview --> 324 + <div class="repo-header"> 325 + <div class="repo-identity"> 326 + <div class="repo-avatar">AS</div> 327 + <div class="repo-names"> 328 + <div class="repo-handle">alice.bsky.social</div> 329 + <div class="repo-did">did:plc:z72i7hdynmk6r22z27h6tvur</div> 330 + </div> 331 + </div> 332 + <div class="repo-stats"> 333 + <div class="repo-stat"><strong>12</strong> collections</div> 334 + <div class="repo-stat"><strong>1,847</strong> records</div> 318 335 </div> 319 336 </div> 320 - <div class="repo-stats"> 321 - <div class="repo-stat"><strong>12</strong> collections</div> 322 - <div class="repo-stat"><strong>1,847</strong> records</div> 323 - </div> 324 - </div> 325 - 326 - <!-- Collections --> 327 - <div class="section-label">Collections</div> 328 - 329 - <div class="collection-item"> 330 - <div class="collection-item-left"> 331 - <div class="collection-icon"> 332 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 333 - <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/> 337 + 338 + <!-- Collections --> 339 + <div class="section-label">Collections</div> 340 + 341 + <div class="collection-item"> 342 + <div class="collection-item-left"> 343 + <div class="collection-icon"> 344 + <svg 345 + viewBox="0 0 24 24" 346 + fill="none" 347 + stroke="currentColor" 348 + stroke-width="2" 349 + stroke-linecap="round" 350 + stroke-linejoin="round"> 351 + <path 352 + d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" /> 353 + </svg> 354 + </div> 355 + <div class="collection-name">app.bsky.feed.post</div> 356 + </div> 357 + <span class="collection-count">482</span> 358 + <div class="collection-chevron"> 359 + <svg 360 + viewBox="0 0 24 24" 361 + fill="none" 362 + stroke="currentColor" 363 + stroke-width="2" 364 + stroke-linecap="round" 365 + stroke-linejoin="round"> 366 + <polyline points="9 18 15 12 9 6" /> 334 367 </svg> 335 368 </div> 336 - <div class="collection-name">app.bsky.feed.post</div> 337 369 </div> 338 - <span class="collection-count">482</span> 339 - <div class="collection-chevron"> 340 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 341 - <polyline points="9 18 15 12 9 6"/> 342 - </svg> 343 - </div> 344 - </div> 345 - 346 - <div class="collection-item"> 347 - <div class="collection-item-left"> 348 - <div class="collection-icon"> 349 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 350 - <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/> 370 + 371 + <div class="collection-item"> 372 + <div class="collection-item-left"> 373 + <div class="collection-icon"> 374 + <svg 375 + viewBox="0 0 24 24" 376 + fill="none" 377 + stroke="currentColor" 378 + stroke-width="2" 379 + stroke-linecap="round" 380 + stroke-linejoin="round"> 381 + <path 382 + d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" /> 383 + </svg> 384 + </div> 385 + <div class="collection-name">app.bsky.feed.like</div> 386 + </div> 387 + <span class="collection-count">1,203</span> 388 + <div class="collection-chevron"> 389 + <svg 390 + viewBox="0 0 24 24" 391 + fill="none" 392 + stroke="currentColor" 393 + stroke-width="2" 394 + stroke-linecap="round" 395 + stroke-linejoin="round"> 396 + <polyline points="9 18 15 12 9 6" /> 351 397 </svg> 352 398 </div> 353 - <div class="collection-name">app.bsky.feed.like</div> 354 399 </div> 355 - <span class="collection-count">1,203</span> 356 - <div class="collection-chevron"> 357 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 358 - <polyline points="9 18 15 12 9 6"/> 359 - </svg> 360 - </div> 361 - </div> 362 - 363 - <div class="collection-item"> 364 - <div class="collection-item-left"> 365 - <div class="collection-icon"> 366 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 367 - <polyline points="17 1 21 5 17 9"/> 368 - <path d="M3 11V9a4 4 0 0 1 4-4h14"/> 369 - <polyline points="7 23 3 19 7 15"/> 370 - <path d="M21 13v2a4 4 0 0 1-4 4H3"/> 400 + 401 + <div class="collection-item"> 402 + <div class="collection-item-left"> 403 + <div class="collection-icon"> 404 + <svg 405 + viewBox="0 0 24 24" 406 + fill="none" 407 + stroke="currentColor" 408 + stroke-width="2" 409 + stroke-linecap="round" 410 + stroke-linejoin="round"> 411 + <polyline points="17 1 21 5 17 9" /> 412 + <path d="M3 11V9a4 4 0 0 1 4-4h14" /> 413 + <polyline points="7 23 3 19 7 15" /> 414 + <path d="M21 13v2a4 4 0 0 1-4 4H3" /> 415 + </svg> 416 + </div> 417 + <div class="collection-name">app.bsky.feed.repost</div> 418 + </div> 419 + <span class="collection-count">89</span> 420 + <div class="collection-chevron"> 421 + <svg 422 + viewBox="0 0 24 24" 423 + fill="none" 424 + stroke="currentColor" 425 + stroke-width="2" 426 + stroke-linecap="round" 427 + stroke-linejoin="round"> 428 + <polyline points="9 18 15 12 9 6" /> 371 429 </svg> 372 430 </div> 373 - <div class="collection-name">app.bsky.feed.repost</div> 374 431 </div> 375 - <span class="collection-count">89</span> 376 - <div class="collection-chevron"> 377 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 378 - <polyline points="9 18 15 12 9 6"/> 379 - </svg> 380 - </div> 381 - </div> 382 - 383 - <div class="collection-item"> 384 - <div class="collection-item-left"> 385 - <div class="collection-icon"> 386 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 387 - <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/> 388 - <circle cx="8.5" cy="7" r="4"/> 389 - <line x1="20" y1="8" x2="20" y2="14"/> 390 - <line x1="23" y1="11" x2="17" y2="11"/> 432 + 433 + <div class="collection-item"> 434 + <div class="collection-item-left"> 435 + <div class="collection-icon"> 436 + <svg 437 + viewBox="0 0 24 24" 438 + fill="none" 439 + stroke="currentColor" 440 + stroke-width="2" 441 + stroke-linecap="round" 442 + stroke-linejoin="round"> 443 + <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /> 444 + <circle cx="8.5" cy="7" r="4" /> 445 + <line x1="20" y1="8" x2="20" y2="14" /> 446 + <line x1="23" y1="11" x2="17" y2="11" /> 447 + </svg> 448 + </div> 449 + <div class="collection-name">app.bsky.graph.follow</div> 450 + </div> 451 + <span class="collection-count">256</span> 452 + <div class="collection-chevron"> 453 + <svg 454 + viewBox="0 0 24 24" 455 + fill="none" 456 + stroke="currentColor" 457 + stroke-width="2" 458 + stroke-linecap="round" 459 + stroke-linejoin="round"> 460 + <polyline points="9 18 15 12 9 6" /> 391 461 </svg> 392 462 </div> 393 - <div class="collection-name">app.bsky.graph.follow</div> 394 463 </div> 395 - <span class="collection-count">256</span> 396 - <div class="collection-chevron"> 397 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 398 - <polyline points="9 18 15 12 9 6"/> 399 - </svg> 400 - </div> 401 - </div> 402 - 403 - <div class="collection-item"> 404 - <div class="collection-item-left"> 405 - <div class="collection-icon"> 406 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 407 - <circle cx="12" cy="12" r="10"/> 408 - <line x1="2" y1="12" x2="22" y2="12"/> 409 - <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/> 464 + 465 + <div class="collection-item"> 466 + <div class="collection-item-left"> 467 + <div class="collection-icon"> 468 + <svg 469 + viewBox="0 0 24 24" 470 + fill="none" 471 + stroke="currentColor" 472 + stroke-width="2" 473 + stroke-linecap="round" 474 + stroke-linejoin="round"> 475 + <circle cx="12" cy="12" r="10" /> 476 + <line x1="2" y1="12" x2="22" y2="12" /> 477 + <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /> 478 + </svg> 479 + </div> 480 + <div class="collection-name">app.bsky.feed.generator</div> 481 + </div> 482 + <span class="collection-count">2</span> 483 + <div class="collection-chevron"> 484 + <svg 485 + viewBox="0 0 24 24" 486 + fill="none" 487 + stroke="currentColor" 488 + stroke-width="2" 489 + stroke-linecap="round" 490 + stroke-linejoin="round"> 491 + <polyline points="9 18 15 12 9 6" /> 410 492 </svg> 411 493 </div> 412 - <div class="collection-name">app.bsky.feed.generator</div> 413 494 </div> 414 - <span class="collection-count">2</span> 415 - <div class="collection-chevron"> 416 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 417 - <polyline points="9 18 15 12 9 6"/> 418 - </svg> 419 - </div> 420 - </div> 421 - 422 - <div class="collection-item"> 423 - <div class="collection-item-left"> 424 - <div class="collection-icon"> 425 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 426 - <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 427 - <circle cx="12" cy="7" r="4"/> 495 + 496 + <div class="collection-item"> 497 + <div class="collection-item-left"> 498 + <div class="collection-icon"> 499 + <svg 500 + viewBox="0 0 24 24" 501 + fill="none" 502 + stroke="currentColor" 503 + stroke-width="2" 504 + stroke-linecap="round" 505 + stroke-linejoin="round"> 506 + <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> 507 + <circle cx="12" cy="7" r="4" /> 508 + </svg> 509 + </div> 510 + <div class="collection-name">app.bsky.actor.profile</div> 511 + </div> 512 + <span class="collection-count">1</span> 513 + <div class="collection-chevron"> 514 + <svg 515 + viewBox="0 0 24 24" 516 + fill="none" 517 + stroke="currentColor" 518 + stroke-width="2" 519 + stroke-linecap="round" 520 + stroke-linejoin="round"> 521 + <polyline points="9 18 15 12 9 6" /> 428 522 </svg> 429 523 </div> 430 - <div class="collection-name">app.bsky.actor.profile</div> 431 524 </div> 432 - <span class="collection-count">1</span> 433 - <div class="collection-chevron"> 434 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 435 - <polyline points="9 18 15 12 9 6"/> 436 - </svg> 525 + 526 + <!-- Record Inspector Preview (shown when tapping a record) --> 527 + <div class="section-label">Record Preview</div> 528 + 529 + <div class="record-header"> 530 + <div class="record-breadcrumb"><span>app.bsky.feed.post</span> / 3lbr7yz2gfk2j</div> 531 + <button class="record-copy-btn">Copy JSON</button> 437 532 </div> 438 - </div> 439 - 440 - <!-- Record Inspector Preview (shown when tapping a record) --> 441 - <div class="section-label">Record Preview</div> 442 - 443 - <div class="record-header"> 444 - <div class="record-breadcrumb"> 445 - <span>app.bsky.feed.post</span> / 3lbr7yz2gfk2j 533 + 534 + <div class="json-viewer"> 535 + <span class="json-brace">{</span><br /> 536 + &nbsp;&nbsp;<span class="json-key">"$type"</span>: 537 + <span class="json-string">"app.bsky.feed.post"</span>,<br /> 538 + &nbsp;&nbsp;<span class="json-key">"text"</span>: 539 + <span class="json-string">"Just launched my new project! 🚀"</span>,<br /> 540 + &nbsp;&nbsp;<span class="json-key">"createdAt"</span>: 541 + <span class="json-string">"2024-03-15T14:30:00.000Z"</span>,<br /> 542 + &nbsp;&nbsp;<span class="json-key">"langs"</span>: <span class="json-brace">[</span 543 + ><span class="json-string">"en"</span><span class="json-brace">]</span>,<br /> 544 + &nbsp;&nbsp;<span class="json-key">"facets"</span>: <span class="json-brace">[</span><br /> 545 + &nbsp;&nbsp;&nbsp;&nbsp;<span class="json-brace">{</span><br /> 546 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"index"</span>: <span class="json-brace">{</span 547 + ><br /> 548 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"byteStart"</span>: 549 + <span class="json-number">0</span>,<br /> 550 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"byteEnd"</span>: 551 + <span class="json-number">34</span><br /> 552 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-brace">}</span>,<br /> 553 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"features"</span>: <span class="json-brace">[</span 554 + ><span class="json-brace">{</span><br /> 555 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"$type"</span>: 556 + <span class="json-string">"...facet#tag"</span>,<br /> 557 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"tag"</span>: 558 + <span class="json-string">"buildinpublic"</span><br /> 559 + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-brace">}</span><span class="json-brace">]</span><br /> 560 + &nbsp;&nbsp;&nbsp;&nbsp;<span class="json-brace">}</span><br /> 561 + &nbsp;&nbsp;<span class="json-brace">]</span><br /> 562 + <span class="json-brace">}</span> 446 563 </div> 447 - <button class="record-copy-btn">Copy JSON</button> 448 564 </div> 449 - 450 - <div class="json-viewer"> 451 - <span class="json-brace">{</span><br> 452 - &nbsp;&nbsp;<span class="json-key">"$type"</span>: <span class="json-string">"app.bsky.feed.post"</span>,<br> 453 - &nbsp;&nbsp;<span class="json-key">"text"</span>: <span class="json-string">"Just launched my new project! 🚀"</span>,<br> 454 - &nbsp;&nbsp;<span class="json-key">"createdAt"</span>: <span class="json-string">"2024-03-15T14:30:00.000Z"</span>,<br> 455 - &nbsp;&nbsp;<span class="json-key">"langs"</span>: <span class="json-brace">[</span><span class="json-string">"en"</span><span class="json-brace">]</span>,<br> 456 - &nbsp;&nbsp;<span class="json-key">"facets"</span>: <span class="json-brace">[</span><br> 457 - &nbsp;&nbsp;&nbsp;&nbsp;<span class="json-brace">{</span><br> 458 - &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"index"</span>: <span class="json-brace">{</span><br> 459 - &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"byteStart"</span>: <span class="json-number">0</span>,<br> 460 - &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"byteEnd"</span>: <span class="json-number">34</span><br> 461 - &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-brace">}</span>,<br> 462 - &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"features"</span>: <span class="json-brace">[</span><span class="json-brace">{</span><br> 463 - &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"$type"</span>: <span class="json-string">"...facet#tag"</span>,<br> 464 - &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-key">"tag"</span>: <span class="json-string">"buildinpublic"</span><br> 465 - &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="json-brace">}</span><span class="json-brace">]</span><br> 466 - &nbsp;&nbsp;&nbsp;&nbsp;<span class="json-brace">}</span><br> 467 - &nbsp;&nbsp;<span class="json-brace">]</span><br> 468 - <span class="json-brace">}</span> 469 - </div> 470 - 565 + 566 + <!-- Bottom Navigation --> 567 + <nav class="nav-bar"> 568 + <a href="home.html" class="nav-item"> 569 + <svg 570 + viewBox="0 0 24 24" 571 + fill="none" 572 + stroke="currentColor" 573 + stroke-width="2" 574 + stroke-linecap="round" 575 + stroke-linejoin="round"> 576 + <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /> 577 + <polyline points="9 22 9 12 15 12 15 22" /> 578 + </svg> 579 + <span>Home</span> 580 + </a> 581 + 582 + <a href="search.html" class="nav-item"> 583 + <svg 584 + viewBox="0 0 24 24" 585 + fill="none" 586 + stroke="currentColor" 587 + stroke-width="2" 588 + stroke-linecap="round" 589 + stroke-linejoin="round"> 590 + <circle cx="11" cy="11" r="8" /> 591 + <line x1="21" y1="21" x2="16.65" y2="16.65" /> 592 + </svg> 593 + <span>Search</span> 594 + </a> 595 + 596 + <a href="profile.html" class="nav-item"> 597 + <svg 598 + viewBox="0 0 24 24" 599 + fill="none" 600 + stroke="currentColor" 601 + stroke-width="2" 602 + stroke-linecap="round" 603 + stroke-linejoin="round"> 604 + <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> 605 + <circle cx="12" cy="7" r="4" /> 606 + </svg> 607 + <span>Profile</span> 608 + </a> 609 + 610 + <a href="settings.html" class="nav-item"> 611 + <svg 612 + viewBox="0 0 24 24" 613 + fill="none" 614 + stroke="currentColor" 615 + stroke-width="2" 616 + stroke-linecap="round" 617 + stroke-linejoin="round"> 618 + <circle cx="12" cy="12" r="3" /> 619 + <path 620 + d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" /> 621 + </svg> 622 + <span>Settings</span> 623 + </a> 624 + </nav> 471 625 </div> 472 - 473 - <!-- Bottom Navigation --> 474 - <nav class="nav-bar"> 475 - <a href="home.html" class="nav-item"> 476 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 477 - <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> 478 - <polyline points="9 22 9 12 15 12 15 22"/> 479 - </svg> 480 - <span>Home</span> 481 - </a> 482 - 483 - <a href="search.html" class="nav-item"> 484 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 485 - <circle cx="11" cy="11" r="8"/> 486 - <line x1="21" y1="21" x2="16.65" y2="16.65"/> 487 - </svg> 488 - <span>Search</span> 489 - </a> 490 - 491 - <a href="profile.html" class="nav-item"> 492 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 493 - <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 494 - <circle cx="12" cy="7" r="4"/> 495 - </svg> 496 - <span>Profile</span> 497 - </a> 498 - 499 - <a href="settings.html" class="nav-item"> 500 - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 501 - <circle cx="12" cy="12" r="3"/> 502 - <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/> 503 - </svg> 504 - <span>Settings</span> 505 - </a> 506 - </nav> 507 - 508 - </div> 509 - 510 - <script> 511 - if (localStorage.getItem('theme') === 'dark') { 512 - document.documentElement.setAttribute('data-theme', 'dark'); 513 - } 514 - </script> 515 - </body> 626 + 627 + <script> 628 + if (localStorage.getItem("theme") === "dark") { 629 + document.documentElement.setAttribute("data-theme", "dark"); 630 + } 631 + </script> 632 + </body> 516 633 </html>
+18
docs/designs/settings.html
··· 359 359 <div class="settings-section-title">About</div> 360 360 361 361 <div class="settings-group"> 362 + <a href="devtools.html" class="settings-item" style="text-decoration:none;"> 363 + <div class="settings-item-left"> 364 + <svg class="settings-item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 365 + <polyline points="16 18 22 12 16 6"/> 366 + <polyline points="8 6 2 12 8 18"/> 367 + </svg> 368 + <div class="settings-item-content"> 369 + <div class="settings-item-title">Dev Tools</div> 370 + <div class="settings-item-subtitle">PDS Explorer</div> 371 + </div> 372 + </div> 373 + <div class="settings-item-right"> 374 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 375 + <polyline points="9 18 15 12 9 6"/> 376 + </svg> 377 + </div> 378 + </a> 379 + 362 380 <div class="settings-item"> 363 381 <div class="settings-item-left"> 364 382 <svg class="settings-item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+6 -5
docs/specs/phase-2.md
··· 109 109 110 110 ### PDS Explorer (pdsls.dev replica) 111 111 112 - An in-app developer tool (debug builds) that replicates the core functionality 113 - of [pdsls.dev](https://pdsls.dev) — a client-side AT Protocol repository 114 - browser. 112 + An in-app developer tool accessible via Settings that replicates the core 113 + functionality of [pdsls.dev](https://pdsls.dev) — a client-side AT Protocol 114 + repository browser. 115 115 116 116 **Core features:** 117 117 ··· 132 132 | `com.atproto.repo.listRecords` | Collection → paginated record list | 133 133 | `com.atproto.repo.getRecord` | Collection + rkey → full record JSON | 134 134 135 - Guard behind `kDebugMode`. No separate Bloc needed — use a `DevToolsCubit` 136 - with simple request/response state, since this is a stateless exploration tool. 135 + Accessible via Settings → Dev Tools. No separate Bloc needed — use a 136 + `DevToolsCubit` with simple request/response state, since this is a stateless 137 + exploration tool.
+11 -10
docs/tasks/phase-1.md
··· 3 3 ## M0 — Project Scaffolding 4 4 5 5 - [x] Add dependencies (`bluesky`, `atproto_oauth`, `bluesky_text`, `flutter_bloc`, `drift`, `go_router`) 6 - - [ ] Set up feature-first folder structure (`core/`, `features/auth|profile|settings/`) 7 - - [ ] Configure Drift database with `accounts`, `cached_profiles`, `cached_posts`, `settings` tables 8 - - [ ] Configure `go_router` with initial route definitions (login, home, profile, settings) 6 + - [x] Set up feature-first folder structure (`core/`, `features/auth|profile|settings/`) 7 + - [x] Configure Drift database with `accounts`, `cached_profiles`, `cached_posts`, `settings` tables 8 + - [x] Configure `go_router` with initial route definitions (login, home, profile, settings) 9 9 10 10 ## M1 — Authentication 11 11 12 - - [ ] Implement App Password login (`createSession`) behind `kDebugMode` flag 13 - - [ ] Implement OAuth 2.0 flow (DPoP + PAR + PKCE) via `atproto_oauth` 14 - - [ ] Set up loopback redirect listener (`http://127.0.0.1/callback`) 15 - - [ ] Build `AuthBloc` — events: `LoginRequested`, `LogoutRequested`, `SessionRestored`; states: `Unauthenticated`, `Authenticating`, `Authenticated`, `AuthError` 16 - - [ ] Session persistence: store/restore tokens in Drift, silent refresh on launch 17 - - [ ] Build login screen (handle input, OAuth button, debug app-password form) 18 - - [ ] Logout: revoke tokens, clear Drift row, reset Bloc, navigate to login 12 + - [x] Implement App Password login (`createSession`) behind `kDebugMode` flag 13 + - [x] Implement OAuth 2.0 flow (DPoP + PAR + PKCE) via `atproto_oauth` 14 + - [x] Set up loopback redirect listener (`http://127.0.0.1/callback`) 15 + - [x] Build `AuthBloc` — events: `LoginRequested`, `LogoutRequested`, `SessionRestored`; states: `Unauthenticated`, `Authenticating`, `Authenticated`, `AuthError` 16 + - [x] Session persistence: store/restore tokens in Drift, silent refresh on launch 17 + - [x] Build login screen (handle input, OAuth button, debug app-password form) 18 + - [x] Logout: revoke tokens, clear Drift row, reset Bloc, navigate to login 19 19 20 20 ## M2 — Profile Rendering 21 21 ··· 35 35 36 36 ## M4 — Dev Scripts 37 37 38 + - [ ] Create `uv` project in directory `scripts` 38 39 - [ ] `scripts/fetch_profile.py` — pretty-print profile JSON 39 40 - [ ] `scripts/fetch_feed.py` — dump post + facet structures 40 41 - [ ] `scripts/resolve_handle.py` — resolve handle → DID
+1 -1
docs/tasks/phase-2.md
··· 28 28 - [ ] Collection browser via `listRecords` — paginated record list per collection 29 29 - [ ] Record inspector via `getRecord` — pretty-printed JSON with syntax highlighting 30 30 - [ ] AT-URI input — paste `at://` URI to jump directly to a record 31 - - [ ] Guard all dev tools screens behind `kDebugMode` 31 + - [ ] Add Dev Tools entry in Settings screen, navigable by all users
+1
ios/Flutter/Debug.xcconfig
··· 1 + #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 1 2 #include "Generated.xcconfig"
+1
ios/Flutter/Release.xcconfig
··· 1 + #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 1 2 #include "Generated.xcconfig"
+43
ios/Podfile
··· 1 + # Uncomment this line to define a global platform for your project 2 + # platform :ios, '13.0' 3 + 4 + # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 + ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 + 7 + project 'Runner', { 8 + 'Debug' => :debug, 9 + 'Profile' => :release, 10 + 'Release' => :release, 11 + } 12 + 13 + def flutter_root 14 + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 + unless File.exist?(generated_xcode_build_settings_path) 16 + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 + end 18 + 19 + File.foreach(generated_xcode_build_settings_path) do |line| 20 + matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 + return matches[1].strip if matches 22 + end 23 + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 + end 25 + 26 + require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 + 28 + flutter_ios_podfile_setup 29 + 30 + target 'Runner' do 31 + use_frameworks! 32 + 33 + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 34 + target 'RunnerTests' do 35 + inherit! :search_paths 36 + end 37 + end 38 + 39 + post_install do |installer| 40 + installer.pods_project.targets.each do |target| 41 + flutter_additional_ios_build_settings(target) 42 + end 43 + end
+67
lib/core/database/app_database.dart
··· 1 + import 'package:drift/drift.dart'; 2 + import 'package:drift_flutter/drift_flutter.dart'; 3 + import 'package:path_provider/path_provider.dart'; 4 + 5 + import 'tables.dart'; 6 + 7 + part 'app_database.g.dart'; 8 + 9 + @DriftDatabase(tables: [Accounts, Settings]) 10 + class AppDatabase extends _$AppDatabase { 11 + AppDatabase({QueryExecutor? executor}) : super(executor ?? _openConnection()); 12 + 13 + @override 14 + int get schemaVersion => 1; 15 + 16 + static QueryExecutor _openConnection() { 17 + return driftDatabase( 18 + name: 'lazurite_db', 19 + native: const DriftNativeOptions(databaseDirectory: getApplicationSupportDirectory), 20 + ); 21 + } 22 + 23 + Future<Account?> getAccount(String did) => (select(accounts)..where((a) => a.did.equals(did))).getSingleOrNull(); 24 + 25 + Future<Account?> getActiveAccount() async { 26 + final all = await select(accounts).get(); 27 + return all.isNotEmpty ? all.first : null; 28 + } 29 + 30 + Future<List<Account>> getAllAccounts() => select(accounts).get(); 31 + 32 + Future<int> insertAccount(AccountsCompanion account) => into(accounts).insert(account, mode: InsertMode.replace); 33 + 34 + Future<int> deleteAccount(String did) => (delete(accounts)..where((a) => a.did.equals(did))).go(); 35 + 36 + Future<int> deleteAllAccounts() => delete(accounts).go(); 37 + 38 + Future<bool> updateAccountTokens( 39 + String did, { 40 + required String accessToken, 41 + String? refreshToken, 42 + DateTime? expiresAt, 43 + }) async { 44 + final query = update(accounts)..where((a) => a.did.equals(did)); 45 + final rowsAffected = await query.write( 46 + AccountsCompanion( 47 + accessToken: Value(accessToken), 48 + refreshToken: refreshToken != null ? Value(refreshToken) : const Value.absent(), 49 + expiresAt: expiresAt != null ? Value(expiresAt) : const Value.absent(), 50 + updatedAt: Value(DateTime.now()), 51 + ), 52 + ); 53 + return rowsAffected > 0; 54 + } 55 + 56 + Future<String?> getSetting(String key) async { 57 + final setting = await (select(settings)..where((s) => s.key.equals(key))).getSingleOrNull(); 58 + return setting?.value; 59 + } 60 + 61 + Future<int> setSetting(String key, String value) => into(settings).insert( 62 + SettingsCompanion(key: Value(key), value: Value(value), updatedAt: Value(DateTime.now())), 63 + mode: InsertMode.replace, 64 + ); 65 + 66 + Future<int> deleteSetting(String key) => (delete(settings)..where((s) => s.key.equals(key))).go(); 67 + }
+1056
lib/core/database/app_database.g.dart
··· 1 + // GENERATED CODE - DO NOT MODIFY BY HAND 2 + 3 + part of 'app_database.dart'; 4 + 5 + // ignore_for_file: type=lint 6 + class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { 7 + @override 8 + final GeneratedDatabase attachedDatabase; 9 + final String? _alias; 10 + $AccountsTable(this.attachedDatabase, [this._alias]); 11 + static const VerificationMeta _didMeta = const VerificationMeta('did'); 12 + @override 13 + late final GeneratedColumn<String> did = GeneratedColumn<String>( 14 + 'did', 15 + aliasedName, 16 + false, 17 + type: DriftSqlType.string, 18 + requiredDuringInsert: true, 19 + ); 20 + static const VerificationMeta _handleMeta = const VerificationMeta('handle'); 21 + @override 22 + late final GeneratedColumn<String> handle = GeneratedColumn<String>( 23 + 'handle', 24 + aliasedName, 25 + false, 26 + type: DriftSqlType.string, 27 + requiredDuringInsert: true, 28 + ); 29 + static const VerificationMeta _displayNameMeta = const VerificationMeta('displayName'); 30 + @override 31 + late final GeneratedColumn<String> displayName = GeneratedColumn<String>( 32 + 'display_name', 33 + aliasedName, 34 + true, 35 + type: DriftSqlType.string, 36 + requiredDuringInsert: false, 37 + ); 38 + static const VerificationMeta _accessTokenMeta = const VerificationMeta('accessToken'); 39 + @override 40 + late final GeneratedColumn<String> accessToken = GeneratedColumn<String>( 41 + 'access_token', 42 + aliasedName, 43 + false, 44 + type: DriftSqlType.string, 45 + requiredDuringInsert: true, 46 + ); 47 + static const VerificationMeta _refreshTokenMeta = const VerificationMeta('refreshToken'); 48 + @override 49 + late final GeneratedColumn<String> refreshToken = GeneratedColumn<String>( 50 + 'refresh_token', 51 + aliasedName, 52 + true, 53 + type: DriftSqlType.string, 54 + requiredDuringInsert: false, 55 + ); 56 + static const VerificationMeta _dpopPrivateKeyMeta = const VerificationMeta('dpopPrivateKey'); 57 + @override 58 + late final GeneratedColumn<String> dpopPrivateKey = GeneratedColumn<String>( 59 + 'dpop_private_key', 60 + aliasedName, 61 + true, 62 + type: DriftSqlType.string, 63 + requiredDuringInsert: false, 64 + ); 65 + static const VerificationMeta _expiresAtMeta = const VerificationMeta('expiresAt'); 66 + @override 67 + late final GeneratedColumn<DateTime> expiresAt = GeneratedColumn<DateTime>( 68 + 'expires_at', 69 + aliasedName, 70 + true, 71 + type: DriftSqlType.dateTime, 72 + requiredDuringInsert: false, 73 + ); 74 + static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); 75 + @override 76 + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( 77 + 'created_at', 78 + aliasedName, 79 + false, 80 + type: DriftSqlType.dateTime, 81 + requiredDuringInsert: false, 82 + defaultValue: currentDateAndTime, 83 + ); 84 + static const VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); 85 + @override 86 + late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>( 87 + 'updated_at', 88 + aliasedName, 89 + false, 90 + type: DriftSqlType.dateTime, 91 + requiredDuringInsert: false, 92 + defaultValue: currentDateAndTime, 93 + ); 94 + @override 95 + List<GeneratedColumn> get $columns => [ 96 + did, 97 + handle, 98 + displayName, 99 + accessToken, 100 + refreshToken, 101 + dpopPrivateKey, 102 + expiresAt, 103 + createdAt, 104 + updatedAt, 105 + ]; 106 + @override 107 + String get aliasedName => _alias ?? actualTableName; 108 + @override 109 + String get actualTableName => $name; 110 + static const String $name = 'accounts'; 111 + @override 112 + VerificationContext validateIntegrity(Insertable<Account> instance, {bool isInserting = false}) { 113 + final context = VerificationContext(); 114 + final data = instance.toColumns(true); 115 + if (data.containsKey('did')) { 116 + context.handle(_didMeta, did.isAcceptableOrUnknown(data['did']!, _didMeta)); 117 + } else if (isInserting) { 118 + context.missing(_didMeta); 119 + } 120 + if (data.containsKey('handle')) { 121 + context.handle(_handleMeta, handle.isAcceptableOrUnknown(data['handle']!, _handleMeta)); 122 + } else if (isInserting) { 123 + context.missing(_handleMeta); 124 + } 125 + if (data.containsKey('display_name')) { 126 + context.handle(_displayNameMeta, displayName.isAcceptableOrUnknown(data['display_name']!, _displayNameMeta)); 127 + } 128 + if (data.containsKey('access_token')) { 129 + context.handle(_accessTokenMeta, accessToken.isAcceptableOrUnknown(data['access_token']!, _accessTokenMeta)); 130 + } else if (isInserting) { 131 + context.missing(_accessTokenMeta); 132 + } 133 + if (data.containsKey('refresh_token')) { 134 + context.handle(_refreshTokenMeta, refreshToken.isAcceptableOrUnknown(data['refresh_token']!, _refreshTokenMeta)); 135 + } 136 + if (data.containsKey('dpop_private_key')) { 137 + context.handle( 138 + _dpopPrivateKeyMeta, 139 + dpopPrivateKey.isAcceptableOrUnknown(data['dpop_private_key']!, _dpopPrivateKeyMeta), 140 + ); 141 + } 142 + if (data.containsKey('expires_at')) { 143 + context.handle(_expiresAtMeta, expiresAt.isAcceptableOrUnknown(data['expires_at']!, _expiresAtMeta)); 144 + } 145 + if (data.containsKey('created_at')) { 146 + context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); 147 + } 148 + if (data.containsKey('updated_at')) { 149 + context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); 150 + } 151 + return context; 152 + } 153 + 154 + @override 155 + Set<GeneratedColumn> get $primaryKey => {did}; 156 + @override 157 + Account map(Map<String, dynamic> data, {String? tablePrefix}) { 158 + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 159 + return Account( 160 + did: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}did'])!, 161 + handle: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}handle'])!, 162 + displayName: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}display_name']), 163 + accessToken: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}access_token'])!, 164 + refreshToken: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}refresh_token']), 165 + dpopPrivateKey: attachedDatabase.typeMapping.read( 166 + DriftSqlType.string, 167 + data['${effectivePrefix}dpop_private_key'], 168 + ), 169 + expiresAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}expires_at']), 170 + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, 171 + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, 172 + ); 173 + } 174 + 175 + @override 176 + $AccountsTable createAlias(String alias) { 177 + return $AccountsTable(attachedDatabase, alias); 178 + } 179 + } 180 + 181 + class Account extends DataClass implements Insertable<Account> { 182 + final String did; 183 + final String handle; 184 + final String? displayName; 185 + final String accessToken; 186 + final String? refreshToken; 187 + final String? dpopPrivateKey; 188 + final DateTime? expiresAt; 189 + final DateTime createdAt; 190 + final DateTime updatedAt; 191 + const Account({ 192 + required this.did, 193 + required this.handle, 194 + this.displayName, 195 + required this.accessToken, 196 + this.refreshToken, 197 + this.dpopPrivateKey, 198 + this.expiresAt, 199 + required this.createdAt, 200 + required this.updatedAt, 201 + }); 202 + @override 203 + Map<String, Expression> toColumns(bool nullToAbsent) { 204 + final map = <String, Expression>{}; 205 + map['did'] = Variable<String>(did); 206 + map['handle'] = Variable<String>(handle); 207 + if (!nullToAbsent || displayName != null) { 208 + map['display_name'] = Variable<String>(displayName); 209 + } 210 + map['access_token'] = Variable<String>(accessToken); 211 + if (!nullToAbsent || refreshToken != null) { 212 + map['refresh_token'] = Variable<String>(refreshToken); 213 + } 214 + if (!nullToAbsent || dpopPrivateKey != null) { 215 + map['dpop_private_key'] = Variable<String>(dpopPrivateKey); 216 + } 217 + if (!nullToAbsent || expiresAt != null) { 218 + map['expires_at'] = Variable<DateTime>(expiresAt); 219 + } 220 + map['created_at'] = Variable<DateTime>(createdAt); 221 + map['updated_at'] = Variable<DateTime>(updatedAt); 222 + return map; 223 + } 224 + 225 + AccountsCompanion toCompanion(bool nullToAbsent) { 226 + return AccountsCompanion( 227 + did: Value(did), 228 + handle: Value(handle), 229 + displayName: displayName == null && nullToAbsent ? const Value.absent() : Value(displayName), 230 + accessToken: Value(accessToken), 231 + refreshToken: refreshToken == null && nullToAbsent ? const Value.absent() : Value(refreshToken), 232 + dpopPrivateKey: dpopPrivateKey == null && nullToAbsent ? const Value.absent() : Value(dpopPrivateKey), 233 + expiresAt: expiresAt == null && nullToAbsent ? const Value.absent() : Value(expiresAt), 234 + createdAt: Value(createdAt), 235 + updatedAt: Value(updatedAt), 236 + ); 237 + } 238 + 239 + factory Account.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 240 + serializer ??= driftRuntimeOptions.defaultSerializer; 241 + return Account( 242 + did: serializer.fromJson<String>(json['did']), 243 + handle: serializer.fromJson<String>(json['handle']), 244 + displayName: serializer.fromJson<String?>(json['displayName']), 245 + accessToken: serializer.fromJson<String>(json['accessToken']), 246 + refreshToken: serializer.fromJson<String?>(json['refreshToken']), 247 + dpopPrivateKey: serializer.fromJson<String?>(json['dpopPrivateKey']), 248 + expiresAt: serializer.fromJson<DateTime?>(json['expiresAt']), 249 + createdAt: serializer.fromJson<DateTime>(json['createdAt']), 250 + updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), 251 + ); 252 + } 253 + @override 254 + Map<String, dynamic> toJson({ValueSerializer? serializer}) { 255 + serializer ??= driftRuntimeOptions.defaultSerializer; 256 + return <String, dynamic>{ 257 + 'did': serializer.toJson<String>(did), 258 + 'handle': serializer.toJson<String>(handle), 259 + 'displayName': serializer.toJson<String?>(displayName), 260 + 'accessToken': serializer.toJson<String>(accessToken), 261 + 'refreshToken': serializer.toJson<String?>(refreshToken), 262 + 'dpopPrivateKey': serializer.toJson<String?>(dpopPrivateKey), 263 + 'expiresAt': serializer.toJson<DateTime?>(expiresAt), 264 + 'createdAt': serializer.toJson<DateTime>(createdAt), 265 + 'updatedAt': serializer.toJson<DateTime>(updatedAt), 266 + }; 267 + } 268 + 269 + Account copyWith({ 270 + String? did, 271 + String? handle, 272 + Value<String?> displayName = const Value.absent(), 273 + String? accessToken, 274 + Value<String?> refreshToken = const Value.absent(), 275 + Value<String?> dpopPrivateKey = const Value.absent(), 276 + Value<DateTime?> expiresAt = const Value.absent(), 277 + DateTime? createdAt, 278 + DateTime? updatedAt, 279 + }) => Account( 280 + did: did ?? this.did, 281 + handle: handle ?? this.handle, 282 + displayName: displayName.present ? displayName.value : this.displayName, 283 + accessToken: accessToken ?? this.accessToken, 284 + refreshToken: refreshToken.present ? refreshToken.value : this.refreshToken, 285 + dpopPrivateKey: dpopPrivateKey.present ? dpopPrivateKey.value : this.dpopPrivateKey, 286 + expiresAt: expiresAt.present ? expiresAt.value : this.expiresAt, 287 + createdAt: createdAt ?? this.createdAt, 288 + updatedAt: updatedAt ?? this.updatedAt, 289 + ); 290 + Account copyWithCompanion(AccountsCompanion data) { 291 + return Account( 292 + did: data.did.present ? data.did.value : this.did, 293 + handle: data.handle.present ? data.handle.value : this.handle, 294 + displayName: data.displayName.present ? data.displayName.value : this.displayName, 295 + accessToken: data.accessToken.present ? data.accessToken.value : this.accessToken, 296 + refreshToken: data.refreshToken.present ? data.refreshToken.value : this.refreshToken, 297 + dpopPrivateKey: data.dpopPrivateKey.present ? data.dpopPrivateKey.value : this.dpopPrivateKey, 298 + expiresAt: data.expiresAt.present ? data.expiresAt.value : this.expiresAt, 299 + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, 300 + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, 301 + ); 302 + } 303 + 304 + @override 305 + String toString() { 306 + return (StringBuffer('Account(') 307 + ..write('did: $did, ') 308 + ..write('handle: $handle, ') 309 + ..write('displayName: $displayName, ') 310 + ..write('accessToken: $accessToken, ') 311 + ..write('refreshToken: $refreshToken, ') 312 + ..write('dpopPrivateKey: $dpopPrivateKey, ') 313 + ..write('expiresAt: $expiresAt, ') 314 + ..write('createdAt: $createdAt, ') 315 + ..write('updatedAt: $updatedAt') 316 + ..write(')')) 317 + .toString(); 318 + } 319 + 320 + @override 321 + int get hashCode => 322 + Object.hash(did, handle, displayName, accessToken, refreshToken, dpopPrivateKey, expiresAt, createdAt, updatedAt); 323 + @override 324 + bool operator ==(Object other) => 325 + identical(this, other) || 326 + (other is Account && 327 + other.did == this.did && 328 + other.handle == this.handle && 329 + other.displayName == this.displayName && 330 + other.accessToken == this.accessToken && 331 + other.refreshToken == this.refreshToken && 332 + other.dpopPrivateKey == this.dpopPrivateKey && 333 + other.expiresAt == this.expiresAt && 334 + other.createdAt == this.createdAt && 335 + other.updatedAt == this.updatedAt); 336 + } 337 + 338 + class AccountsCompanion extends UpdateCompanion<Account> { 339 + final Value<String> did; 340 + final Value<String> handle; 341 + final Value<String?> displayName; 342 + final Value<String> accessToken; 343 + final Value<String?> refreshToken; 344 + final Value<String?> dpopPrivateKey; 345 + final Value<DateTime?> expiresAt; 346 + final Value<DateTime> createdAt; 347 + final Value<DateTime> updatedAt; 348 + final Value<int> rowid; 349 + const AccountsCompanion({ 350 + this.did = const Value.absent(), 351 + this.handle = const Value.absent(), 352 + this.displayName = const Value.absent(), 353 + this.accessToken = const Value.absent(), 354 + this.refreshToken = const Value.absent(), 355 + this.dpopPrivateKey = const Value.absent(), 356 + this.expiresAt = const Value.absent(), 357 + this.createdAt = const Value.absent(), 358 + this.updatedAt = const Value.absent(), 359 + this.rowid = const Value.absent(), 360 + }); 361 + AccountsCompanion.insert({ 362 + required String did, 363 + required String handle, 364 + this.displayName = const Value.absent(), 365 + required String accessToken, 366 + this.refreshToken = const Value.absent(), 367 + this.dpopPrivateKey = const Value.absent(), 368 + this.expiresAt = const Value.absent(), 369 + this.createdAt = const Value.absent(), 370 + this.updatedAt = const Value.absent(), 371 + this.rowid = const Value.absent(), 372 + }) : did = Value(did), 373 + handle = Value(handle), 374 + accessToken = Value(accessToken); 375 + static Insertable<Account> custom({ 376 + Expression<String>? did, 377 + Expression<String>? handle, 378 + Expression<String>? displayName, 379 + Expression<String>? accessToken, 380 + Expression<String>? refreshToken, 381 + Expression<String>? dpopPrivateKey, 382 + Expression<DateTime>? expiresAt, 383 + Expression<DateTime>? createdAt, 384 + Expression<DateTime>? updatedAt, 385 + Expression<int>? rowid, 386 + }) { 387 + return RawValuesInsertable({ 388 + if (did != null) 'did': did, 389 + if (handle != null) 'handle': handle, 390 + if (displayName != null) 'display_name': displayName, 391 + if (accessToken != null) 'access_token': accessToken, 392 + if (refreshToken != null) 'refresh_token': refreshToken, 393 + if (dpopPrivateKey != null) 'dpop_private_key': dpopPrivateKey, 394 + if (expiresAt != null) 'expires_at': expiresAt, 395 + if (createdAt != null) 'created_at': createdAt, 396 + if (updatedAt != null) 'updated_at': updatedAt, 397 + if (rowid != null) 'rowid': rowid, 398 + }); 399 + } 400 + 401 + AccountsCompanion copyWith({ 402 + Value<String>? did, 403 + Value<String>? handle, 404 + Value<String?>? displayName, 405 + Value<String>? accessToken, 406 + Value<String?>? refreshToken, 407 + Value<String?>? dpopPrivateKey, 408 + Value<DateTime?>? expiresAt, 409 + Value<DateTime>? createdAt, 410 + Value<DateTime>? updatedAt, 411 + Value<int>? rowid, 412 + }) { 413 + return AccountsCompanion( 414 + did: did ?? this.did, 415 + handle: handle ?? this.handle, 416 + displayName: displayName ?? this.displayName, 417 + accessToken: accessToken ?? this.accessToken, 418 + refreshToken: refreshToken ?? this.refreshToken, 419 + dpopPrivateKey: dpopPrivateKey ?? this.dpopPrivateKey, 420 + expiresAt: expiresAt ?? this.expiresAt, 421 + createdAt: createdAt ?? this.createdAt, 422 + updatedAt: updatedAt ?? this.updatedAt, 423 + rowid: rowid ?? this.rowid, 424 + ); 425 + } 426 + 427 + @override 428 + Map<String, Expression> toColumns(bool nullToAbsent) { 429 + final map = <String, Expression>{}; 430 + if (did.present) { 431 + map['did'] = Variable<String>(did.value); 432 + } 433 + if (handle.present) { 434 + map['handle'] = Variable<String>(handle.value); 435 + } 436 + if (displayName.present) { 437 + map['display_name'] = Variable<String>(displayName.value); 438 + } 439 + if (accessToken.present) { 440 + map['access_token'] = Variable<String>(accessToken.value); 441 + } 442 + if (refreshToken.present) { 443 + map['refresh_token'] = Variable<String>(refreshToken.value); 444 + } 445 + if (dpopPrivateKey.present) { 446 + map['dpop_private_key'] = Variable<String>(dpopPrivateKey.value); 447 + } 448 + if (expiresAt.present) { 449 + map['expires_at'] = Variable<DateTime>(expiresAt.value); 450 + } 451 + if (createdAt.present) { 452 + map['created_at'] = Variable<DateTime>(createdAt.value); 453 + } 454 + if (updatedAt.present) { 455 + map['updated_at'] = Variable<DateTime>(updatedAt.value); 456 + } 457 + if (rowid.present) { 458 + map['rowid'] = Variable<int>(rowid.value); 459 + } 460 + return map; 461 + } 462 + 463 + @override 464 + String toString() { 465 + return (StringBuffer('AccountsCompanion(') 466 + ..write('did: $did, ') 467 + ..write('handle: $handle, ') 468 + ..write('displayName: $displayName, ') 469 + ..write('accessToken: $accessToken, ') 470 + ..write('refreshToken: $refreshToken, ') 471 + ..write('dpopPrivateKey: $dpopPrivateKey, ') 472 + ..write('expiresAt: $expiresAt, ') 473 + ..write('createdAt: $createdAt, ') 474 + ..write('updatedAt: $updatedAt, ') 475 + ..write('rowid: $rowid') 476 + ..write(')')) 477 + .toString(); 478 + } 479 + } 480 + 481 + class $SettingsTable extends Settings with TableInfo<$SettingsTable, SettingsEntry> { 482 + @override 483 + final GeneratedDatabase attachedDatabase; 484 + final String? _alias; 485 + $SettingsTable(this.attachedDatabase, [this._alias]); 486 + static const VerificationMeta _keyMeta = const VerificationMeta('key'); 487 + @override 488 + late final GeneratedColumn<String> key = GeneratedColumn<String>( 489 + 'key', 490 + aliasedName, 491 + false, 492 + type: DriftSqlType.string, 493 + requiredDuringInsert: true, 494 + ); 495 + static const VerificationMeta _valueMeta = const VerificationMeta('value'); 496 + @override 497 + late final GeneratedColumn<String> value = GeneratedColumn<String>( 498 + 'value', 499 + aliasedName, 500 + false, 501 + type: DriftSqlType.string, 502 + requiredDuringInsert: true, 503 + ); 504 + static const VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); 505 + @override 506 + late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>( 507 + 'updated_at', 508 + aliasedName, 509 + false, 510 + type: DriftSqlType.dateTime, 511 + requiredDuringInsert: false, 512 + defaultValue: currentDateAndTime, 513 + ); 514 + @override 515 + List<GeneratedColumn> get $columns => [key, value, updatedAt]; 516 + @override 517 + String get aliasedName => _alias ?? actualTableName; 518 + @override 519 + String get actualTableName => $name; 520 + static const String $name = 'settings'; 521 + @override 522 + VerificationContext validateIntegrity(Insertable<SettingsEntry> instance, {bool isInserting = false}) { 523 + final context = VerificationContext(); 524 + final data = instance.toColumns(true); 525 + if (data.containsKey('key')) { 526 + context.handle(_keyMeta, key.isAcceptableOrUnknown(data['key']!, _keyMeta)); 527 + } else if (isInserting) { 528 + context.missing(_keyMeta); 529 + } 530 + if (data.containsKey('value')) { 531 + context.handle(_valueMeta, value.isAcceptableOrUnknown(data['value']!, _valueMeta)); 532 + } else if (isInserting) { 533 + context.missing(_valueMeta); 534 + } 535 + if (data.containsKey('updated_at')) { 536 + context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); 537 + } 538 + return context; 539 + } 540 + 541 + @override 542 + Set<GeneratedColumn> get $primaryKey => {key}; 543 + @override 544 + SettingsEntry map(Map<String, dynamic> data, {String? tablePrefix}) { 545 + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 546 + return SettingsEntry( 547 + key: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}key'])!, 548 + value: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}value'])!, 549 + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, 550 + ); 551 + } 552 + 553 + @override 554 + $SettingsTable createAlias(String alias) { 555 + return $SettingsTable(attachedDatabase, alias); 556 + } 557 + } 558 + 559 + class SettingsEntry extends DataClass implements Insertable<SettingsEntry> { 560 + final String key; 561 + final String value; 562 + final DateTime updatedAt; 563 + const SettingsEntry({required this.key, required this.value, required this.updatedAt}); 564 + @override 565 + Map<String, Expression> toColumns(bool nullToAbsent) { 566 + final map = <String, Expression>{}; 567 + map['key'] = Variable<String>(key); 568 + map['value'] = Variable<String>(value); 569 + map['updated_at'] = Variable<DateTime>(updatedAt); 570 + return map; 571 + } 572 + 573 + SettingsCompanion toCompanion(bool nullToAbsent) { 574 + return SettingsCompanion(key: Value(key), value: Value(value), updatedAt: Value(updatedAt)); 575 + } 576 + 577 + factory SettingsEntry.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 578 + serializer ??= driftRuntimeOptions.defaultSerializer; 579 + return SettingsEntry( 580 + key: serializer.fromJson<String>(json['key']), 581 + value: serializer.fromJson<String>(json['value']), 582 + updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), 583 + ); 584 + } 585 + @override 586 + Map<String, dynamic> toJson({ValueSerializer? serializer}) { 587 + serializer ??= driftRuntimeOptions.defaultSerializer; 588 + return <String, dynamic>{ 589 + 'key': serializer.toJson<String>(key), 590 + 'value': serializer.toJson<String>(value), 591 + 'updatedAt': serializer.toJson<DateTime>(updatedAt), 592 + }; 593 + } 594 + 595 + SettingsEntry copyWith({String? key, String? value, DateTime? updatedAt}) => 596 + SettingsEntry(key: key ?? this.key, value: value ?? this.value, updatedAt: updatedAt ?? this.updatedAt); 597 + SettingsEntry copyWithCompanion(SettingsCompanion data) { 598 + return SettingsEntry( 599 + key: data.key.present ? data.key.value : this.key, 600 + value: data.value.present ? data.value.value : this.value, 601 + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, 602 + ); 603 + } 604 + 605 + @override 606 + String toString() { 607 + return (StringBuffer('SettingsEntry(') 608 + ..write('key: $key, ') 609 + ..write('value: $value, ') 610 + ..write('updatedAt: $updatedAt') 611 + ..write(')')) 612 + .toString(); 613 + } 614 + 615 + @override 616 + int get hashCode => Object.hash(key, value, updatedAt); 617 + @override 618 + bool operator ==(Object other) => 619 + identical(this, other) || 620 + (other is SettingsEntry && 621 + other.key == this.key && 622 + other.value == this.value && 623 + other.updatedAt == this.updatedAt); 624 + } 625 + 626 + class SettingsCompanion extends UpdateCompanion<SettingsEntry> { 627 + final Value<String> key; 628 + final Value<String> value; 629 + final Value<DateTime> updatedAt; 630 + final Value<int> rowid; 631 + const SettingsCompanion({ 632 + this.key = const Value.absent(), 633 + this.value = const Value.absent(), 634 + this.updatedAt = const Value.absent(), 635 + this.rowid = const Value.absent(), 636 + }); 637 + SettingsCompanion.insert({ 638 + required String key, 639 + required String value, 640 + this.updatedAt = const Value.absent(), 641 + this.rowid = const Value.absent(), 642 + }) : key = Value(key), 643 + value = Value(value); 644 + static Insertable<SettingsEntry> custom({ 645 + Expression<String>? key, 646 + Expression<String>? value, 647 + Expression<DateTime>? updatedAt, 648 + Expression<int>? rowid, 649 + }) { 650 + return RawValuesInsertable({ 651 + if (key != null) 'key': key, 652 + if (value != null) 'value': value, 653 + if (updatedAt != null) 'updated_at': updatedAt, 654 + if (rowid != null) 'rowid': rowid, 655 + }); 656 + } 657 + 658 + SettingsCompanion copyWith({ 659 + Value<String>? key, 660 + Value<String>? value, 661 + Value<DateTime>? updatedAt, 662 + Value<int>? rowid, 663 + }) { 664 + return SettingsCompanion( 665 + key: key ?? this.key, 666 + value: value ?? this.value, 667 + updatedAt: updatedAt ?? this.updatedAt, 668 + rowid: rowid ?? this.rowid, 669 + ); 670 + } 671 + 672 + @override 673 + Map<String, Expression> toColumns(bool nullToAbsent) { 674 + final map = <String, Expression>{}; 675 + if (key.present) { 676 + map['key'] = Variable<String>(key.value); 677 + } 678 + if (value.present) { 679 + map['value'] = Variable<String>(value.value); 680 + } 681 + if (updatedAt.present) { 682 + map['updated_at'] = Variable<DateTime>(updatedAt.value); 683 + } 684 + if (rowid.present) { 685 + map['rowid'] = Variable<int>(rowid.value); 686 + } 687 + return map; 688 + } 689 + 690 + @override 691 + String toString() { 692 + return (StringBuffer('SettingsCompanion(') 693 + ..write('key: $key, ') 694 + ..write('value: $value, ') 695 + ..write('updatedAt: $updatedAt, ') 696 + ..write('rowid: $rowid') 697 + ..write(')')) 698 + .toString(); 699 + } 700 + } 701 + 702 + abstract class _$AppDatabase extends GeneratedDatabase { 703 + _$AppDatabase(QueryExecutor e) : super(e); 704 + $AppDatabaseManager get managers => $AppDatabaseManager(this); 705 + late final $AccountsTable accounts = $AccountsTable(this); 706 + late final $SettingsTable settings = $SettingsTable(this); 707 + @override 708 + Iterable<TableInfo<Table, Object?>> get allTables => allSchemaEntities.whereType<TableInfo<Table, Object?>>(); 709 + @override 710 + List<DatabaseSchemaEntity> get allSchemaEntities => [accounts, settings]; 711 + } 712 + 713 + typedef $$AccountsTableCreateCompanionBuilder = 714 + AccountsCompanion Function({ 715 + required String did, 716 + required String handle, 717 + Value<String?> displayName, 718 + required String accessToken, 719 + Value<String?> refreshToken, 720 + Value<String?> dpopPrivateKey, 721 + Value<DateTime?> expiresAt, 722 + Value<DateTime> createdAt, 723 + Value<DateTime> updatedAt, 724 + Value<int> rowid, 725 + }); 726 + typedef $$AccountsTableUpdateCompanionBuilder = 727 + AccountsCompanion Function({ 728 + Value<String> did, 729 + Value<String> handle, 730 + Value<String?> displayName, 731 + Value<String> accessToken, 732 + Value<String?> refreshToken, 733 + Value<String?> dpopPrivateKey, 734 + Value<DateTime?> expiresAt, 735 + Value<DateTime> createdAt, 736 + Value<DateTime> updatedAt, 737 + Value<int> rowid, 738 + }); 739 + 740 + class $$AccountsTableFilterComposer extends Composer<_$AppDatabase, $AccountsTable> { 741 + $$AccountsTableFilterComposer({ 742 + required super.$db, 743 + required super.$table, 744 + super.joinBuilder, 745 + super.$addJoinBuilderToRootComposer, 746 + super.$removeJoinBuilderFromRootComposer, 747 + }); 748 + ColumnFilters<String> get did => $composableBuilder(column: $table.did, builder: (column) => ColumnFilters(column)); 749 + 750 + ColumnFilters<String> get handle => 751 + $composableBuilder(column: $table.handle, builder: (column) => ColumnFilters(column)); 752 + 753 + ColumnFilters<String> get displayName => 754 + $composableBuilder(column: $table.displayName, builder: (column) => ColumnFilters(column)); 755 + 756 + ColumnFilters<String> get accessToken => 757 + $composableBuilder(column: $table.accessToken, builder: (column) => ColumnFilters(column)); 758 + 759 + ColumnFilters<String> get refreshToken => 760 + $composableBuilder(column: $table.refreshToken, builder: (column) => ColumnFilters(column)); 761 + 762 + ColumnFilters<String> get dpopPrivateKey => 763 + $composableBuilder(column: $table.dpopPrivateKey, builder: (column) => ColumnFilters(column)); 764 + 765 + ColumnFilters<DateTime> get expiresAt => 766 + $composableBuilder(column: $table.expiresAt, builder: (column) => ColumnFilters(column)); 767 + 768 + ColumnFilters<DateTime> get createdAt => 769 + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); 770 + 771 + ColumnFilters<DateTime> get updatedAt => 772 + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); 773 + } 774 + 775 + class $$AccountsTableOrderingComposer extends Composer<_$AppDatabase, $AccountsTable> { 776 + $$AccountsTableOrderingComposer({ 777 + required super.$db, 778 + required super.$table, 779 + super.joinBuilder, 780 + super.$addJoinBuilderToRootComposer, 781 + super.$removeJoinBuilderFromRootComposer, 782 + }); 783 + ColumnOrderings<String> get did => 784 + $composableBuilder(column: $table.did, builder: (column) => ColumnOrderings(column)); 785 + 786 + ColumnOrderings<String> get handle => 787 + $composableBuilder(column: $table.handle, builder: (column) => ColumnOrderings(column)); 788 + 789 + ColumnOrderings<String> get displayName => 790 + $composableBuilder(column: $table.displayName, builder: (column) => ColumnOrderings(column)); 791 + 792 + ColumnOrderings<String> get accessToken => 793 + $composableBuilder(column: $table.accessToken, builder: (column) => ColumnOrderings(column)); 794 + 795 + ColumnOrderings<String> get refreshToken => 796 + $composableBuilder(column: $table.refreshToken, builder: (column) => ColumnOrderings(column)); 797 + 798 + ColumnOrderings<String> get dpopPrivateKey => 799 + $composableBuilder(column: $table.dpopPrivateKey, builder: (column) => ColumnOrderings(column)); 800 + 801 + ColumnOrderings<DateTime> get expiresAt => 802 + $composableBuilder(column: $table.expiresAt, builder: (column) => ColumnOrderings(column)); 803 + 804 + ColumnOrderings<DateTime> get createdAt => 805 + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnOrderings(column)); 806 + 807 + ColumnOrderings<DateTime> get updatedAt => 808 + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); 809 + } 810 + 811 + class $$AccountsTableAnnotationComposer extends Composer<_$AppDatabase, $AccountsTable> { 812 + $$AccountsTableAnnotationComposer({ 813 + required super.$db, 814 + required super.$table, 815 + super.joinBuilder, 816 + super.$addJoinBuilderToRootComposer, 817 + super.$removeJoinBuilderFromRootComposer, 818 + }); 819 + GeneratedColumn<String> get did => $composableBuilder(column: $table.did, builder: (column) => column); 820 + 821 + GeneratedColumn<String> get handle => $composableBuilder(column: $table.handle, builder: (column) => column); 822 + 823 + GeneratedColumn<String> get displayName => 824 + $composableBuilder(column: $table.displayName, builder: (column) => column); 825 + 826 + GeneratedColumn<String> get accessToken => 827 + $composableBuilder(column: $table.accessToken, builder: (column) => column); 828 + 829 + GeneratedColumn<String> get refreshToken => 830 + $composableBuilder(column: $table.refreshToken, builder: (column) => column); 831 + 832 + GeneratedColumn<String> get dpopPrivateKey => 833 + $composableBuilder(column: $table.dpopPrivateKey, builder: (column) => column); 834 + 835 + GeneratedColumn<DateTime> get expiresAt => $composableBuilder(column: $table.expiresAt, builder: (column) => column); 836 + 837 + GeneratedColumn<DateTime> get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); 838 + 839 + GeneratedColumn<DateTime> get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); 840 + } 841 + 842 + class $$AccountsTableTableManager 843 + extends 844 + RootTableManager< 845 + _$AppDatabase, 846 + $AccountsTable, 847 + Account, 848 + $$AccountsTableFilterComposer, 849 + $$AccountsTableOrderingComposer, 850 + $$AccountsTableAnnotationComposer, 851 + $$AccountsTableCreateCompanionBuilder, 852 + $$AccountsTableUpdateCompanionBuilder, 853 + (Account, BaseReferences<_$AppDatabase, $AccountsTable, Account>), 854 + Account, 855 + PrefetchHooks Function() 856 + > { 857 + $$AccountsTableTableManager(_$AppDatabase db, $AccountsTable table) 858 + : super( 859 + TableManagerState( 860 + db: db, 861 + table: table, 862 + createFilteringComposer: () => $$AccountsTableFilterComposer($db: db, $table: table), 863 + createOrderingComposer: () => $$AccountsTableOrderingComposer($db: db, $table: table), 864 + createComputedFieldComposer: () => $$AccountsTableAnnotationComposer($db: db, $table: table), 865 + updateCompanionCallback: 866 + ({ 867 + Value<String> did = const Value.absent(), 868 + Value<String> handle = const Value.absent(), 869 + Value<String?> displayName = const Value.absent(), 870 + Value<String> accessToken = const Value.absent(), 871 + Value<String?> refreshToken = const Value.absent(), 872 + Value<String?> dpopPrivateKey = const Value.absent(), 873 + Value<DateTime?> expiresAt = const Value.absent(), 874 + Value<DateTime> createdAt = const Value.absent(), 875 + Value<DateTime> updatedAt = const Value.absent(), 876 + Value<int> rowid = const Value.absent(), 877 + }) => AccountsCompanion( 878 + did: did, 879 + handle: handle, 880 + displayName: displayName, 881 + accessToken: accessToken, 882 + refreshToken: refreshToken, 883 + dpopPrivateKey: dpopPrivateKey, 884 + expiresAt: expiresAt, 885 + createdAt: createdAt, 886 + updatedAt: updatedAt, 887 + rowid: rowid, 888 + ), 889 + createCompanionCallback: 890 + ({ 891 + required String did, 892 + required String handle, 893 + Value<String?> displayName = const Value.absent(), 894 + required String accessToken, 895 + Value<String?> refreshToken = const Value.absent(), 896 + Value<String?> dpopPrivateKey = const Value.absent(), 897 + Value<DateTime?> expiresAt = const Value.absent(), 898 + Value<DateTime> createdAt = const Value.absent(), 899 + Value<DateTime> updatedAt = const Value.absent(), 900 + Value<int> rowid = const Value.absent(), 901 + }) => AccountsCompanion.insert( 902 + did: did, 903 + handle: handle, 904 + displayName: displayName, 905 + accessToken: accessToken, 906 + refreshToken: refreshToken, 907 + dpopPrivateKey: dpopPrivateKey, 908 + expiresAt: expiresAt, 909 + createdAt: createdAt, 910 + updatedAt: updatedAt, 911 + rowid: rowid, 912 + ), 913 + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 914 + prefetchHooksCallback: null, 915 + ), 916 + ); 917 + } 918 + 919 + typedef $$AccountsTableProcessedTableManager = 920 + ProcessedTableManager< 921 + _$AppDatabase, 922 + $AccountsTable, 923 + Account, 924 + $$AccountsTableFilterComposer, 925 + $$AccountsTableOrderingComposer, 926 + $$AccountsTableAnnotationComposer, 927 + $$AccountsTableCreateCompanionBuilder, 928 + $$AccountsTableUpdateCompanionBuilder, 929 + (Account, BaseReferences<_$AppDatabase, $AccountsTable, Account>), 930 + Account, 931 + PrefetchHooks Function() 932 + >; 933 + typedef $$SettingsTableCreateCompanionBuilder = 934 + SettingsCompanion Function({ 935 + required String key, 936 + required String value, 937 + Value<DateTime> updatedAt, 938 + Value<int> rowid, 939 + }); 940 + typedef $$SettingsTableUpdateCompanionBuilder = 941 + SettingsCompanion Function({Value<String> key, Value<String> value, Value<DateTime> updatedAt, Value<int> rowid}); 942 + 943 + class $$SettingsTableFilterComposer extends Composer<_$AppDatabase, $SettingsTable> { 944 + $$SettingsTableFilterComposer({ 945 + required super.$db, 946 + required super.$table, 947 + super.joinBuilder, 948 + super.$addJoinBuilderToRootComposer, 949 + super.$removeJoinBuilderFromRootComposer, 950 + }); 951 + ColumnFilters<String> get key => $composableBuilder(column: $table.key, builder: (column) => ColumnFilters(column)); 952 + 953 + ColumnFilters<String> get value => 954 + $composableBuilder(column: $table.value, builder: (column) => ColumnFilters(column)); 955 + 956 + ColumnFilters<DateTime> get updatedAt => 957 + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); 958 + } 959 + 960 + class $$SettingsTableOrderingComposer extends Composer<_$AppDatabase, $SettingsTable> { 961 + $$SettingsTableOrderingComposer({ 962 + required super.$db, 963 + required super.$table, 964 + super.joinBuilder, 965 + super.$addJoinBuilderToRootComposer, 966 + super.$removeJoinBuilderFromRootComposer, 967 + }); 968 + ColumnOrderings<String> get key => 969 + $composableBuilder(column: $table.key, builder: (column) => ColumnOrderings(column)); 970 + 971 + ColumnOrderings<String> get value => 972 + $composableBuilder(column: $table.value, builder: (column) => ColumnOrderings(column)); 973 + 974 + ColumnOrderings<DateTime> get updatedAt => 975 + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); 976 + } 977 + 978 + class $$SettingsTableAnnotationComposer extends Composer<_$AppDatabase, $SettingsTable> { 979 + $$SettingsTableAnnotationComposer({ 980 + required super.$db, 981 + required super.$table, 982 + super.joinBuilder, 983 + super.$addJoinBuilderToRootComposer, 984 + super.$removeJoinBuilderFromRootComposer, 985 + }); 986 + GeneratedColumn<String> get key => $composableBuilder(column: $table.key, builder: (column) => column); 987 + 988 + GeneratedColumn<String> get value => $composableBuilder(column: $table.value, builder: (column) => column); 989 + 990 + GeneratedColumn<DateTime> get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); 991 + } 992 + 993 + class $$SettingsTableTableManager 994 + extends 995 + RootTableManager< 996 + _$AppDatabase, 997 + $SettingsTable, 998 + SettingsEntry, 999 + $$SettingsTableFilterComposer, 1000 + $$SettingsTableOrderingComposer, 1001 + $$SettingsTableAnnotationComposer, 1002 + $$SettingsTableCreateCompanionBuilder, 1003 + $$SettingsTableUpdateCompanionBuilder, 1004 + (SettingsEntry, BaseReferences<_$AppDatabase, $SettingsTable, SettingsEntry>), 1005 + SettingsEntry, 1006 + PrefetchHooks Function() 1007 + > { 1008 + $$SettingsTableTableManager(_$AppDatabase db, $SettingsTable table) 1009 + : super( 1010 + TableManagerState( 1011 + db: db, 1012 + table: table, 1013 + createFilteringComposer: () => $$SettingsTableFilterComposer($db: db, $table: table), 1014 + createOrderingComposer: () => $$SettingsTableOrderingComposer($db: db, $table: table), 1015 + createComputedFieldComposer: () => $$SettingsTableAnnotationComposer($db: db, $table: table), 1016 + updateCompanionCallback: 1017 + ({ 1018 + Value<String> key = const Value.absent(), 1019 + Value<String> value = const Value.absent(), 1020 + Value<DateTime> updatedAt = const Value.absent(), 1021 + Value<int> rowid = const Value.absent(), 1022 + }) => SettingsCompanion(key: key, value: value, updatedAt: updatedAt, rowid: rowid), 1023 + createCompanionCallback: 1024 + ({ 1025 + required String key, 1026 + required String value, 1027 + Value<DateTime> updatedAt = const Value.absent(), 1028 + Value<int> rowid = const Value.absent(), 1029 + }) => SettingsCompanion.insert(key: key, value: value, updatedAt: updatedAt, rowid: rowid), 1030 + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 1031 + prefetchHooksCallback: null, 1032 + ), 1033 + ); 1034 + } 1035 + 1036 + typedef $$SettingsTableProcessedTableManager = 1037 + ProcessedTableManager< 1038 + _$AppDatabase, 1039 + $SettingsTable, 1040 + SettingsEntry, 1041 + $$SettingsTableFilterComposer, 1042 + $$SettingsTableOrderingComposer, 1043 + $$SettingsTableAnnotationComposer, 1044 + $$SettingsTableCreateCompanionBuilder, 1045 + $$SettingsTableUpdateCompanionBuilder, 1046 + (SettingsEntry, BaseReferences<_$AppDatabase, $SettingsTable, SettingsEntry>), 1047 + SettingsEntry, 1048 + PrefetchHooks Function() 1049 + >; 1050 + 1051 + class $AppDatabaseManager { 1052 + final _$AppDatabase _db; 1053 + $AppDatabaseManager(this._db); 1054 + $$AccountsTableTableManager get accounts => $$AccountsTableTableManager(_db, _db.accounts); 1055 + $$SettingsTableTableManager get settings => $$SettingsTableTableManager(_db, _db.settings); 1056 + }
+27
lib/core/database/tables.dart
··· 1 + import 'package:drift/drift.dart'; 2 + 3 + @DataClassName('Account') 4 + class Accounts extends Table { 5 + TextColumn get did => text()(); 6 + TextColumn get handle => text()(); 7 + TextColumn get displayName => text().nullable()(); 8 + TextColumn get accessToken => text()(); 9 + TextColumn get refreshToken => text().nullable()(); 10 + TextColumn get dpopPrivateKey => text().nullable()(); 11 + DateTimeColumn get expiresAt => dateTime().nullable()(); 12 + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); 13 + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); 14 + 15 + @override 16 + Set<Column> get primaryKey => {did}; 17 + } 18 + 19 + @DataClassName('SettingsEntry') 20 + class Settings extends Table { 21 + TextColumn get key => text()(); 22 + TextColumn get value => text()(); 23 + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); 24 + 25 + @override 26 + Set<Column> get primaryKey => {key}; 27 + }
+51
lib/core/router/app_router.dart
··· 1 + import 'dart:async'; 2 + 3 + import 'package:flutter/material.dart'; 4 + import 'package:go_router/go_router.dart'; 5 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 6 + import 'package:lazurite/features/auth/presentation/home_screen.dart'; 7 + import 'package:lazurite/features/auth/presentation/login_screen.dart'; 8 + import 'package:lazurite/features/settings/presentation/settings_screen.dart'; 9 + 10 + class AppRouter { 11 + AppRouter({required this.authBloc}); 12 + final AuthBloc authBloc; 13 + 14 + GoRouter get router => GoRouter( 15 + refreshListenable: GoRouterRefreshStream(authBloc.stream), 16 + redirect: (context, state) { 17 + final isAuthenticated = authBloc.state.isAuthenticated; 18 + final isLoggingIn = state.uri.path == '/login'; 19 + 20 + if (!isAuthenticated && !isLoggingIn) { 21 + return '/login'; 22 + } 23 + 24 + if (isAuthenticated && isLoggingIn) { 25 + return '/'; 26 + } 27 + 28 + return null; 29 + }, 30 + routes: [ 31 + GoRoute(path: '/', builder: (context, state) => const HomeScreen()), 32 + GoRoute(path: '/login', builder: (context, state) => const LoginScreen()), 33 + GoRoute(path: '/settings', builder: (context, state) => const SettingsScreen()), 34 + ], 35 + ); 36 + } 37 + 38 + class GoRouterRefreshStream extends ChangeNotifier { 39 + GoRouterRefreshStream(Stream<AuthState> stream) { 40 + notifyListeners(); 41 + _subscription = stream.asBroadcastStream().listen((state) => notifyListeners()); 42 + } 43 + 44 + late final StreamSubscription<AuthState> _subscription; 45 + 46 + @override 47 + void dispose() { 48 + _subscription.cancel(); 49 + super.dispose(); 50 + } 51 + }
+93
lib/features/auth/bloc/auth_bloc.dart
··· 1 + import 'package:equatable/equatable.dart'; 2 + import 'package:flutter_bloc/flutter_bloc.dart'; 3 + import 'package:lazurite/features/auth/data/auth_repository.dart'; 4 + import 'package:lazurite/features/auth/data/models/auth_models.dart'; 5 + 6 + part 'auth_event.dart'; 7 + part 'auth_state.dart'; 8 + 9 + class AuthBloc extends Bloc<AuthEvent, AuthState> { 10 + AuthBloc({required AuthRepository authRepository}) 11 + : _authRepository = authRepository, 12 + super(const AuthState.unauthenticated()) { 13 + on<AuthEvent>(_onEvent); 14 + } 15 + final AuthRepository _authRepository; 16 + 17 + Future<void> _onEvent(AuthEvent event, Emitter<AuthState> emit) async { 18 + if (event is LoginRequested) { 19 + await _onLoginRequested(event, emit); 20 + } else if (event is OAuthLoginRequested) { 21 + await _onOAuthLoginRequested(event, emit); 22 + } else if (event is LogoutRequested) { 23 + await _onLogoutRequested(event, emit); 24 + } else if (event is SessionRestored) { 25 + await _onSessionRestored(event, emit); 26 + } else if (event is CheckSessionRequested) { 27 + await _onCheckSessionRequested(event, emit); 28 + } 29 + } 30 + 31 + Future<void> _onLoginRequested(LoginRequested event, Emitter<AuthState> emit) async { 32 + emit(const AuthState.authenticating()); 33 + try { 34 + final tokens = await _authRepository.loginWithAppPassword(event.handle, event.appPassword); 35 + if (tokens != null) { 36 + emit(AuthState.authenticated(tokens)); 37 + } else { 38 + emit(const AuthState.authError('Login failed')); 39 + } 40 + } catch (e) { 41 + emit(const AuthState.authError('Login failed: \$e')); 42 + } 43 + } 44 + 45 + Future<void> _onOAuthLoginRequested(OAuthLoginRequested event, Emitter<AuthState> emit) async { 46 + emit(const AuthState.authenticating()); 47 + try { 48 + final tokens = await _authRepository.loginWithOAuth(event.handle); 49 + if (tokens != null) { 50 + emit(AuthState.authenticated(tokens)); 51 + } else { 52 + emit(const AuthState.authError('OAuth login failed')); 53 + } 54 + } catch (e) { 55 + emit(const AuthState.authError('OAuth login failed: \$e')); 56 + } 57 + } 58 + 59 + Future<void> _onLogoutRequested(LogoutRequested event, Emitter<AuthState> emit) async { 60 + try { 61 + await _authRepository.logout(); 62 + emit(const AuthState.unauthenticated()); 63 + } catch (e) { 64 + emit(const AuthState.authError('Logout failed: \$e')); 65 + } 66 + } 67 + 68 + Future<void> _onSessionRestored(SessionRestored event, Emitter<AuthState> emit) async { 69 + emit(AuthState.authenticated(event.tokens)); 70 + } 71 + 72 + Future<void> _onCheckSessionRequested(CheckSessionRequested event, Emitter<AuthState> emit) async { 73 + try { 74 + final tokens = await _authRepository.getStoredSession(); 75 + if (tokens != null) { 76 + if (tokens.isExpired && tokens.refreshToken != null) { 77 + final refreshed = await _authRepository.refreshSession(tokens.refreshToken!); 78 + if (refreshed != null) { 79 + emit(AuthState.authenticated(refreshed)); 80 + } else { 81 + emit(const AuthState.unauthenticated()); 82 + } 83 + } else { 84 + emit(AuthState.authenticated(tokens)); 85 + } 86 + } else { 87 + emit(const AuthState.unauthenticated()); 88 + } 89 + } catch (e) { 90 + emit(const AuthState.unauthenticated()); 91 + } 92 + } 93 + }
+41
lib/features/auth/bloc/auth_event.dart
··· 1 + part of 'auth_bloc.dart'; 2 + 3 + abstract class AuthEvent extends Equatable { 4 + const AuthEvent(); 5 + 6 + @override 7 + List<Object?> get props => []; 8 + } 9 + 10 + class LoginRequested extends AuthEvent { 11 + const LoginRequested({required this.handle, required this.appPassword}); 12 + final String handle; 13 + final String appPassword; 14 + 15 + @override 16 + List<Object?> get props => [handle, appPassword]; 17 + } 18 + 19 + class OAuthLoginRequested extends AuthEvent { 20 + const OAuthLoginRequested({required this.handle}); 21 + final String handle; 22 + 23 + @override 24 + List<Object?> get props => [handle]; 25 + } 26 + 27 + class LogoutRequested extends AuthEvent { 28 + const LogoutRequested(); 29 + } 30 + 31 + class SessionRestored extends AuthEvent { 32 + const SessionRestored({required this.tokens}); 33 + final AuthTokens tokens; 34 + 35 + @override 36 + List<Object?> get props => [tokens]; 37 + } 38 + 39 + class CheckSessionRequested extends AuthEvent { 40 + const CheckSessionRequested(); 41 + }
+33
lib/features/auth/bloc/auth_state.dart
··· 1 + part of 'auth_bloc.dart'; 2 + 3 + enum AuthStatus { unauthenticated, authenticating, authenticated, authError } 4 + 5 + class AuthState extends Equatable { 6 + const AuthState._({required this.status, this.tokens, this.errorMessage}); 7 + 8 + const AuthState.unauthenticated() : this._(status: AuthStatus.unauthenticated); 9 + 10 + const AuthState.authenticating() : this._(status: AuthStatus.authenticating); 11 + 12 + const AuthState.authenticated(AuthTokens tokens) : this._(status: AuthStatus.authenticated, tokens: tokens); 13 + 14 + const AuthState.authError(String message) : this._(status: AuthStatus.authError, errorMessage: message); 15 + final AuthStatus status; 16 + final AuthTokens? tokens; 17 + final String? errorMessage; 18 + 19 + bool get isAuthenticated => status == AuthStatus.authenticated; 20 + bool get isLoading => status == AuthStatus.authenticating; 21 + bool get hasError => status == AuthStatus.authError; 22 + 23 + AuthState copyWith({AuthStatus? status, AuthTokens? tokens, String? errorMessage}) { 24 + return AuthState._( 25 + status: status ?? this.status, 26 + tokens: tokens ?? this.tokens, 27 + errorMessage: errorMessage ?? this.errorMessage, 28 + ); 29 + } 30 + 31 + @override 32 + List<Object?> get props => [status, tokens, errorMessage]; 33 + }
+194
lib/features/auth/data/auth_repository.dart
··· 1 + import 'dart:async'; 2 + import 'dart:convert'; 3 + import 'dart:io'; 4 + import 'dart:math'; 5 + 6 + import 'package:atproto_oauth/atproto_oauth.dart'; 7 + import 'package:bluesky/atproto.dart' as atp; 8 + import 'package:crypto/crypto.dart'; 9 + import 'package:drift/drift.dart'; 10 + import 'package:lazurite/features/auth/data/models/auth_models.dart'; 11 + import 'package:url_launcher/url_launcher.dart'; 12 + 13 + import '../../../../core/database/app_database.dart'; 14 + 15 + class AuthRepository { 16 + AuthRepository({required AppDatabase database}) : _database = database; 17 + static const String _clientId = 'https://lazurite.stormlightlabs.org/client-metadata.json'; 18 + 19 + final AppDatabase _database; 20 + HttpServer? _callbackServer; 21 + Completer<AuthTokens?>? _oauthCompleter; 22 + 23 + Future<AuthTokens?> getStoredSession() async { 24 + final account = await _database.getActiveAccount(); 25 + if (account == null) return null; 26 + 27 + return AuthTokens( 28 + accessToken: account.accessToken, 29 + refreshToken: account.refreshToken, 30 + expiresAt: account.expiresAt, 31 + did: account.did, 32 + handle: account.handle, 33 + displayName: account.displayName, 34 + ); 35 + } 36 + 37 + Future<void> saveSession(AuthTokens tokens) async { 38 + await _database.insertAccount( 39 + AccountsCompanion( 40 + did: Value(tokens.did), 41 + handle: Value(tokens.handle), 42 + displayName: tokens.displayName != null ? Value(tokens.displayName) : const Value.absent(), 43 + accessToken: Value(tokens.accessToken), 44 + refreshToken: tokens.refreshToken != null ? Value(tokens.refreshToken) : const Value.absent(), 45 + expiresAt: tokens.expiresAt != null ? Value(tokens.expiresAt!) : const Value.absent(), 46 + ), 47 + ); 48 + } 49 + 50 + Future<void> clearSession() async { 51 + await _database.deleteAllAccounts(); 52 + } 53 + 54 + Future<AuthTokens?> loginWithOAuth(String handle) async { 55 + try { 56 + _oauthCompleter = Completer<AuthTokens?>(); 57 + 58 + await _startCallbackServer(); 59 + 60 + final metadata = await getClientMetadata(_clientId); 61 + final oauthClient = OAuthClient(metadata); 62 + 63 + final result = await oauthClient.authorize(handle); 64 + final authorizationUrl = result.$1; 65 + 66 + await _launchUrl(authorizationUrl); 67 + 68 + final tokens = await _oauthCompleter!.future; 69 + return tokens; 70 + } catch (e) { 71 + await _stopCallbackServer(); 72 + rethrow; 73 + } 74 + } 75 + 76 + Future<AuthTokens?> loginWithAppPassword(String handle, String appPassword) async { 77 + try { 78 + final session = await atp.createSession(identifier: handle, password: appPassword); 79 + 80 + final tokens = AuthTokens( 81 + accessToken: session.data.accessJwt, 82 + refreshToken: session.data.refreshJwt, 83 + did: session.data.did, 84 + handle: session.data.handle, 85 + displayName: null, 86 + ); 87 + 88 + await saveSession(tokens); 89 + return tokens; 90 + } catch (e) { 91 + throw Exception('Failed to login with app password: $e'); 92 + } 93 + } 94 + 95 + Future<AuthTokens?> refreshSession(String refreshToken) async { 96 + try { 97 + final refreshed = await atp.refreshSession(refreshJwt: refreshToken); 98 + 99 + final tokens = AuthTokens( 100 + accessToken: refreshed.data.accessJwt, 101 + refreshToken: refreshed.data.refreshJwt, 102 + did: refreshed.data.did, 103 + handle: refreshed.data.handle, 104 + displayName: null, 105 + ); 106 + 107 + await saveSession(tokens); 108 + return tokens; 109 + } catch (e) { 110 + await clearSession(); 111 + throw Exception('Failed to refresh session: $e'); 112 + } 113 + } 114 + 115 + Future<void> logout() async { 116 + await clearSession(); 117 + } 118 + 119 + Future<void> _startCallbackServer() async { 120 + _callbackServer = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); 121 + 122 + _callbackServer!.listen((request) async { 123 + final uri = request.requestedUri; 124 + 125 + if (uri.path == '/callback') { 126 + final code = uri.queryParameters['code']; 127 + final error = uri.queryParameters['error']; 128 + 129 + request.response 130 + ..statusCode = 200 131 + ..headers.contentType = ContentType.html 132 + ..write(''' 133 + <!DOCTYPE html> 134 + <html> 135 + <head><title>Authentication Complete</title></head> 136 + <body> 137 + <h1>Authentication Complete</h1> 138 + <p>You can close this window and return to the app.</p> 139 + </body> 140 + </html> 141 + '''); 142 + 143 + await request.response.close(); 144 + await _stopCallbackServer(); 145 + 146 + if (error != null) { 147 + _oauthCompleter?.completeError(Exception('OAuth error: $error')); 148 + } else if (code != null) { 149 + try { 150 + final tokens = await _exchangeCodeForTokens(code); 151 + await saveSession(tokens); 152 + _oauthCompleter?.complete(tokens); 153 + } catch (e) { 154 + _oauthCompleter?.completeError(e); 155 + } 156 + } else { 157 + _oauthCompleter?.completeError(Exception('No authorization code received')); 158 + } 159 + } else { 160 + request.response.statusCode = 404; 161 + await request.response.close(); 162 + } 163 + }); 164 + } 165 + 166 + Future<void> _stopCallbackServer() async { 167 + await _callbackServer?.close(); 168 + _callbackServer = null; 169 + } 170 + 171 + Future<AuthTokens> _exchangeCodeForTokens(String code) async { 172 + throw UnimplementedError('OAuth token exchange not yet implemented'); 173 + } 174 + 175 + Future<void> _launchUrl(Uri url) async { 176 + if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { 177 + throw Exception('Could not launch $url'); 178 + } 179 + } 180 + 181 + int get callbackPort => _callbackServer?.port ?? 0; 182 + } 183 + 184 + String generateCodeVerifier() { 185 + final random = Random.secure(); 186 + final values = List<int>.generate(32, (_) => random.nextInt(256)); 187 + return base64Url.encode(values).replaceAll('=', ''); 188 + } 189 + 190 + String generateCodeChallenge(String codeVerifier) { 191 + final bytes = utf8.encode(codeVerifier); 192 + final digest = sha256.convert(bytes); 193 + return base64Url.encode(digest.bytes).replaceAll('=', ''); 194 + }
+66
lib/features/auth/data/models/auth_models.dart
··· 1 + import 'package:equatable/equatable.dart'; 2 + 3 + class AuthTokens extends Equatable { 4 + const AuthTokens({ 5 + required this.accessToken, 6 + this.refreshToken, 7 + this.expiresAt, 8 + required this.did, 9 + required this.handle, 10 + this.displayName, 11 + }); 12 + final String accessToken; 13 + final String? refreshToken; 14 + final DateTime? expiresAt; 15 + final String did; 16 + final String handle; 17 + final String? displayName; 18 + 19 + AuthTokens copyWith({ 20 + String? accessToken, 21 + String? refreshToken, 22 + DateTime? expiresAt, 23 + String? did, 24 + String? handle, 25 + String? displayName, 26 + }) { 27 + return AuthTokens( 28 + accessToken: accessToken ?? this.accessToken, 29 + refreshToken: refreshToken ?? this.refreshToken, 30 + expiresAt: expiresAt ?? this.expiresAt, 31 + did: did ?? this.did, 32 + handle: handle ?? this.handle, 33 + displayName: displayName ?? this.displayName, 34 + ); 35 + } 36 + 37 + bool get isExpired { 38 + if (expiresAt == null) return false; 39 + return DateTime.now().isAfter(expiresAt!.subtract(const Duration(minutes: 5))); 40 + } 41 + 42 + @override 43 + List<Object?> get props => [accessToken, refreshToken, expiresAt, did, handle, displayName]; 44 + } 45 + 46 + class User extends Equatable { 47 + const User({required this.did, required this.handle, this.displayName, this.avatar, this.description}); 48 + final String did; 49 + final String handle; 50 + final String? displayName; 51 + final String? avatar; 52 + final String? description; 53 + 54 + User copyWith({String? did, String? handle, String? displayName, String? avatar, String? description}) { 55 + return User( 56 + did: did ?? this.did, 57 + handle: handle ?? this.handle, 58 + displayName: displayName ?? this.displayName, 59 + avatar: avatar ?? this.avatar, 60 + description: description ?? this.description, 61 + ); 62 + } 63 + 64 + @override 65 + List<Object?> get props => [did, handle, displayName, avatar, description]; 66 + }
+46
lib/features/auth/presentation/home_screen.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:flutter_bloc/flutter_bloc.dart'; 3 + import 'package:go_router/go_router.dart'; 4 + 5 + import '../../../features/auth/bloc/auth_bloc.dart'; 6 + 7 + class HomeScreen extends StatelessWidget { 8 + const HomeScreen({super.key}); 9 + 10 + @override 11 + Widget build(BuildContext context) { 12 + return Scaffold( 13 + appBar: AppBar( 14 + title: const Text('Lazurite'), 15 + actions: [ 16 + IconButton(icon: const Icon(Icons.settings), onPressed: () => context.push('/settings')), 17 + IconButton( 18 + icon: const Icon(Icons.logout), 19 + onPressed: () { 20 + context.read<AuthBloc>().add(const LogoutRequested()); 21 + }, 22 + ), 23 + ], 24 + ), 25 + body: BlocBuilder<AuthBloc, AuthState>( 26 + builder: (context, state) { 27 + if (!state.isAuthenticated || state.tokens == null) { 28 + return const Center(child: Text('Not authenticated')); 29 + } 30 + 31 + return Center( 32 + child: Column( 33 + mainAxisAlignment: MainAxisAlignment.center, 34 + children: [ 35 + Text('Welcome!', style: Theme.of(context).textTheme.headlineMedium), 36 + const SizedBox(height: 16), 37 + const Text('Handle: @\${state.tokens!.handle}'), 38 + const Text('DID: \${state.tokens!.did}'), 39 + ], 40 + ), 41 + ); 42 + }, 43 + ), 44 + ); 45 + } 46 + }
+165
lib/features/auth/presentation/login_screen.dart
··· 1 + import 'package:flutter/foundation.dart'; 2 + import 'package:flutter/material.dart'; 3 + import 'package:flutter_bloc/flutter_bloc.dart'; 4 + 5 + import '../../../features/auth/bloc/auth_bloc.dart'; 6 + 7 + class LoginScreen extends StatefulWidget { 8 + const LoginScreen({super.key}); 9 + 10 + @override 11 + State<LoginScreen> createState() => _LoginScreenState(); 12 + } 13 + 14 + class _LoginScreenState extends State<LoginScreen> { 15 + final _handleController = TextEditingController(); 16 + final _appPasswordController = TextEditingController(); 17 + final _formKey = GlobalKey<FormState>(); 18 + bool _showDebugForm = false; 19 + 20 + @override 21 + void dispose() { 22 + _handleController.dispose(); 23 + _appPasswordController.dispose(); 24 + super.dispose(); 25 + } 26 + 27 + void _onOAuthLogin() { 28 + if (_formKey.currentState?.validate() ?? false) { 29 + final handle = _handleController.text.trim(); 30 + context.read<AuthBloc>().add(OAuthLoginRequested(handle: handle)); 31 + } 32 + } 33 + 34 + void _onAppPasswordLogin() { 35 + if (_formKey.currentState?.validate() ?? false) { 36 + final handle = _handleController.text.trim(); 37 + final appPassword = _appPasswordController.text.trim(); 38 + context.read<AuthBloc>().add(LoginRequested(handle: handle, appPassword: appPassword)); 39 + } 40 + } 41 + 42 + @override 43 + Widget build(BuildContext context) { 44 + return Scaffold( 45 + body: BlocListener<AuthBloc, AuthState>( 46 + listener: (context, state) { 47 + if (state.hasError && state.errorMessage != null) { 48 + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.errorMessage!))); 49 + } 50 + }, 51 + child: Center( 52 + child: SingleChildScrollView( 53 + padding: const EdgeInsets.all(24.0), 54 + child: Form( 55 + key: _formKey, 56 + child: Column( 57 + mainAxisAlignment: MainAxisAlignment.center, 58 + crossAxisAlignment: CrossAxisAlignment.stretch, 59 + children: [ 60 + const Icon(Icons.cloud, size: 80, color: Colors.blue), 61 + const SizedBox(height: 24), 62 + Text('Lazurite', style: Theme.of(context).textTheme.headlineLarge, textAlign: TextAlign.center), 63 + const SizedBox(height: 8), 64 + Text('Sign in to BlueSky', style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center), 65 + const SizedBox(height: 32), 66 + TextFormField( 67 + controller: _handleController, 68 + decoration: const InputDecoration( 69 + labelText: 'Handle', 70 + hintText: 'your-handle.bsky.social', 71 + prefixIcon: Icon(Icons.person), 72 + border: OutlineInputBorder(), 73 + ), 74 + validator: (value) { 75 + if (value == null || value.isEmpty) { 76 + return 'Please enter your handle'; 77 + } 78 + return null; 79 + }, 80 + textInputAction: TextInputAction.next, 81 + autocorrect: false, 82 + ), 83 + const SizedBox(height: 24), 84 + BlocBuilder<AuthBloc, AuthState>( 85 + builder: (context, state) { 86 + return ElevatedButton.icon( 87 + onPressed: state.isLoading ? null : _onOAuthLogin, 88 + icon: state.isLoading 89 + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) 90 + : const Icon(Icons.login), 91 + label: Text(state.isLoading ? 'Signing in...' : 'Sign in with BlueSky'), 92 + style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)), 93 + ); 94 + }, 95 + ), 96 + if (kDebugMode) ...[ 97 + const SizedBox(height: 16), 98 + TextButton( 99 + onPressed: () { 100 + setState(() { 101 + _showDebugForm = !_showDebugForm; 102 + }); 103 + }, 104 + child: Text(_showDebugForm ? 'Hide Debug Login' : 'Show Debug Login (App Password)'), 105 + ), 106 + if (_showDebugForm) ...[ 107 + const SizedBox(height: 16), 108 + const Divider(), 109 + const SizedBox(height: 16), 110 + Text( 111 + 'Debug Login (App Password)', 112 + style: Theme.of(context).textTheme.titleMedium, 113 + textAlign: TextAlign.center, 114 + ), 115 + const SizedBox(height: 16), 116 + TextFormField( 117 + controller: _appPasswordController, 118 + decoration: const InputDecoration( 119 + labelText: 'App Password', 120 + hintText: 'xxxx-xxxx-xxxx-xxxx', 121 + prefixIcon: Icon(Icons.lock), 122 + border: OutlineInputBorder(), 123 + ), 124 + obscureText: true, 125 + validator: (value) { 126 + if (_showDebugForm && (value == null || value.isEmpty)) { 127 + return 'Please enter your app password'; 128 + } 129 + return null; 130 + }, 131 + textInputAction: TextInputAction.done, 132 + onFieldSubmitted: (_) => _onAppPasswordLogin(), 133 + ), 134 + const SizedBox(height: 16), 135 + BlocBuilder<AuthBloc, AuthState>( 136 + builder: (context, state) { 137 + return ElevatedButton.icon( 138 + onPressed: state.isLoading ? null : _onAppPasswordLogin, 139 + icon: state.isLoading 140 + ? const SizedBox( 141 + width: 20, 142 + height: 20, 143 + child: CircularProgressIndicator(strokeWidth: 2), 144 + ) 145 + : const Icon(Icons.bug_report), 146 + label: Text(state.isLoading ? 'Signing in...' : 'Debug Sign In'), 147 + style: ElevatedButton.styleFrom( 148 + backgroundColor: Colors.orange, 149 + foregroundColor: Colors.white, 150 + padding: const EdgeInsets.symmetric(vertical: 16), 151 + ), 152 + ); 153 + }, 154 + ), 155 + ], 156 + ], 157 + ], 158 + ), 159 + ), 160 + ), 161 + ), 162 + ), 163 + ); 164 + } 165 + }
+41
lib/features/settings/presentation/settings_screen.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:flutter_bloc/flutter_bloc.dart'; 3 + import 'package:go_router/go_router.dart'; 4 + 5 + import '../../../features/auth/bloc/auth_bloc.dart'; 6 + 7 + class SettingsScreen extends StatelessWidget { 8 + const SettingsScreen({super.key}); 9 + 10 + @override 11 + Widget build(BuildContext context) { 12 + return Scaffold( 13 + appBar: AppBar(title: const Text('Settings')), 14 + body: ListView( 15 + children: [ 16 + BlocBuilder<AuthBloc, AuthState>( 17 + builder: (context, state) { 18 + if (state.isAuthenticated && state.tokens != null) { 19 + return ListTile( 20 + leading: const Icon(Icons.person), 21 + title: const Text('@\${state.tokens!.handle}'), 22 + subtitle: Text(state.tokens!.displayName ?? 'No display name'), 23 + ); 24 + } 25 + return const SizedBox.shrink(); 26 + }, 27 + ), 28 + const Divider(), 29 + ListTile( 30 + leading: const Icon(Icons.logout), 31 + title: const Text('Logout'), 32 + onTap: () { 33 + context.read<AuthBloc>().add(const LogoutRequested()); 34 + context.go('/login'); 35 + }, 36 + ), 37 + ], 38 + ), 39 + ); 40 + } 41 + }
+24 -107
lib/main.dart
··· 1 1 import 'package:flutter/material.dart'; 2 - 3 - void main() { 4 - runApp(const MyApp()); 5 - } 6 - 7 - class MyApp extends StatelessWidget { 8 - const MyApp({super.key}); 9 - 10 - // This widget is the root of your application. 11 - @override 12 - Widget build(BuildContext context) { 13 - return MaterialApp( 14 - title: 'Flutter Demo', 15 - theme: ThemeData( 16 - // This is the theme of your application. 17 - // 18 - // TRY THIS: Try running your application with "flutter run". You'll see 19 - // the application has a purple toolbar. Then, without quitting the app, 20 - // try changing the seedColor in the colorScheme below to Colors.green 21 - // and then invoke "hot reload" (save your changes or press the "hot 22 - // reload" button in a Flutter-supported IDE, or press "r" if you used 23 - // the command line to start the app). 24 - // 25 - // Notice that the counter didn't reset back to zero; the application 26 - // state is not lost during the reload. To reset the state, use hot 27 - // restart instead. 28 - // 29 - // This works for code too, not just values: Most code changes can be 30 - // tested with just a hot reload. 31 - colorScheme: .fromSeed(seedColor: Colors.deepPurple), 32 - ), 33 - home: const MyHomePage(title: 'Flutter Demo Home Page'), 34 - ); 35 - } 36 - } 2 + import 'package:flutter_bloc/flutter_bloc.dart'; 37 3 38 - class MyHomePage extends StatefulWidget { 39 - const MyHomePage({super.key, required this.title}); 4 + import 'core/database/app_database.dart'; 5 + import 'core/router/app_router.dart'; 6 + import 'features/auth/bloc/auth_bloc.dart'; 7 + import 'features/auth/data/auth_repository.dart'; 40 8 41 - // This widget is the home page of your application. It is stateful, meaning 42 - // that it has a State object (defined below) that contains fields that affect 43 - // how it looks. 9 + void main() async { 10 + WidgetsFlutterBinding.ensureInitialized(); 44 11 45 - // This class is the configuration for the state. It holds the values (in this 46 - // case the title) provided by the parent (in this case the App widget) and 47 - // used by the build method of the State. Fields in a Widget subclass are 48 - // always marked "final". 12 + final database = AppDatabase(); 13 + final authRepository = AuthRepository(database: database); 14 + final authBloc = AuthBloc(authRepository: authRepository); 49 15 50 - final String title; 16 + authBloc.add(const CheckSessionRequested()); 51 17 52 - @override 53 - State<MyHomePage> createState() => _MyHomePageState(); 18 + runApp(LazuriteApp(authBloc: authBloc)); 54 19 } 55 20 56 - class _MyHomePageState extends State<MyHomePage> { 57 - int _counter = 0; 58 - 59 - void _incrementCounter() { 60 - setState(() { 61 - // This call to setState tells the Flutter framework that something has 62 - // changed in this State, which causes it to rerun the build method below 63 - // so that the display can reflect the updated values. If we changed 64 - // _counter without calling setState(), then the build method would not be 65 - // called again, and so nothing would appear to happen. 66 - _counter++; 67 - }); 68 - } 21 + class LazuriteApp extends StatelessWidget { 22 + const LazuriteApp({super.key, required this.authBloc}); 23 + final AuthBloc authBloc; 69 24 70 25 @override 71 26 Widget build(BuildContext context) { 72 - // This method is rerun every time setState is called, for instance as done 73 - // by the _incrementCounter method above. 74 - // 75 - // The Flutter framework has been optimized to make rerunning build methods 76 - // fast, so that you can just rebuild anything that needs updating rather 77 - // than having to individually change instances of widgets. 78 - return Scaffold( 79 - appBar: AppBar( 80 - // TRY THIS: Try changing the color here to a specific color (to 81 - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar 82 - // change color while the other colors stay the same. 83 - backgroundColor: Theme.of(context).colorScheme.inversePrimary, 84 - // Here we take the value from the MyHomePage object that was created by 85 - // the App.build method, and use it to set our appbar title. 86 - title: Text(widget.title), 87 - ), 88 - body: Center( 89 - // Center is a layout widget. It takes a single child and positions it 90 - // in the middle of the parent. 91 - child: Column( 92 - // Column is also a layout widget. It takes a list of children and 93 - // arranges them vertically. By default, it sizes itself to fit its 94 - // children horizontally, and tries to be as tall as its parent. 95 - // 96 - // Column has various properties to control how it sizes itself and 97 - // how it positions its children. Here we use mainAxisAlignment to 98 - // center the children vertically; the main axis here is the vertical 99 - // axis because Columns are vertical (the cross axis would be 100 - // horizontal). 101 - // 102 - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" 103 - // action in the IDE, or press "p" in the console), to see the 104 - // wireframe for each widget. 105 - mainAxisAlignment: .center, 106 - children: [ 107 - const Text('You have pushed the button this many times:'), 108 - Text( 109 - '$_counter', 110 - style: Theme.of(context).textTheme.headlineMedium, 111 - ), 112 - ], 113 - ), 114 - ), 115 - floatingActionButton: FloatingActionButton( 116 - onPressed: _incrementCounter, 117 - tooltip: 'Increment', 118 - child: const Icon(Icons.add), 27 + final router = AppRouter(authBloc: authBloc).router; 28 + 29 + return BlocProvider.value( 30 + value: authBloc, 31 + child: MaterialApp.router( 32 + title: 'Lazurite', 33 + debugShowCheckedModeBanner: false, 34 + theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true), 35 + routerConfig: router, 119 36 ), 120 37 ); 121 38 }
+567 -31
pubspec.lock
··· 1 1 # Generated by pub 2 2 # See https://dart.dev/tools/pub/glossary#lockfile 3 3 packages: 4 + _fe_analyzer_shared: 5 + dependency: transitive 6 + description: 7 + name: _fe_analyzer_shared 8 + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d 9 + url: "https://pub.dev" 10 + source: hosted 11 + version: "91.0.0" 12 + analyzer: 13 + dependency: transitive 14 + description: 15 + name: analyzer 16 + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 17 + url: "https://pub.dev" 18 + source: hosted 19 + version: "8.4.1" 20 + args: 21 + dependency: transitive 22 + description: 23 + name: args 24 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 25 + url: "https://pub.dev" 26 + source: hosted 27 + version: "2.7.0" 4 28 async: 5 29 dependency: transitive 6 30 description: ··· 57 81 url: "https://pub.dev" 58 82 source: hosted 59 83 version: "9.2.0" 84 + bloc_test: 85 + dependency: "direct dev" 86 + description: 87 + name: bloc_test 88 + sha256: "1dd549e58be35148bc22a9135962106aa29334bc1e3f285994946a1057b29d7b" 89 + url: "https://pub.dev" 90 + source: hosted 91 + version: "10.0.0" 60 92 bluesky: 61 93 dependency: "direct main" 62 94 description: ··· 89 121 url: "https://pub.dev" 90 122 source: hosted 91 123 version: "1.2.3" 124 + build: 125 + dependency: transitive 126 + description: 127 + name: build 128 + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" 129 + url: "https://pub.dev" 130 + source: hosted 131 + version: "4.0.4" 132 + build_config: 133 + dependency: transitive 134 + description: 135 + name: build_config 136 + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" 137 + url: "https://pub.dev" 138 + source: hosted 139 + version: "1.3.0" 140 + build_daemon: 141 + dependency: transitive 142 + description: 143 + name: build_daemon 144 + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 145 + url: "https://pub.dev" 146 + source: hosted 147 + version: "4.1.1" 148 + build_runner: 149 + dependency: "direct dev" 150 + description: 151 + name: build_runner 152 + sha256: "7981eb922842c77033026eb4341d5af651562008cdb116bdfa31fc46516b6462" 153 + url: "https://pub.dev" 154 + source: hosted 155 + version: "2.12.2" 156 + built_collection: 157 + dependency: transitive 158 + description: 159 + name: built_collection 160 + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" 161 + url: "https://pub.dev" 162 + source: hosted 163 + version: "5.1.1" 164 + built_value: 165 + dependency: transitive 166 + description: 167 + name: built_value 168 + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" 169 + url: "https://pub.dev" 170 + source: hosted 171 + version: "8.12.4" 92 172 cbor: 93 173 dependency: transitive 94 174 description: ··· 105 185 url: "https://pub.dev" 106 186 source: hosted 107 187 version: "1.4.0" 188 + charcode: 189 + dependency: transitive 190 + description: 191 + name: charcode 192 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a 193 + url: "https://pub.dev" 194 + source: hosted 195 + version: "1.4.0" 196 + checked_yaml: 197 + dependency: transitive 198 + description: 199 + name: checked_yaml 200 + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" 201 + url: "https://pub.dev" 202 + source: hosted 203 + version: "2.0.4" 204 + cli_config: 205 + dependency: transitive 206 + description: 207 + name: cli_config 208 + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec 209 + url: "https://pub.dev" 210 + source: hosted 211 + version: "0.2.0" 212 + cli_util: 213 + dependency: transitive 214 + description: 215 + name: cli_util 216 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c 217 + url: "https://pub.dev" 218 + source: hosted 219 + version: "0.4.2" 108 220 clock: 109 221 dependency: transitive 110 222 description: ··· 113 225 url: "https://pub.dev" 114 226 source: hosted 115 227 version: "1.1.2" 116 - code_assets: 228 + code_builder: 117 229 dependency: transitive 118 230 description: 119 - name: code_assets 120 - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" 231 + name: code_builder 232 + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" 121 233 url: "https://pub.dev" 122 234 source: hosted 123 - version: "1.0.0" 235 + version: "4.11.1" 124 236 collection: 125 237 dependency: transitive 126 238 description: ··· 137 249 url: "https://pub.dev" 138 250 source: hosted 139 251 version: "3.1.2" 252 + coverage: 253 + dependency: transitive 254 + description: 255 + name: coverage 256 + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" 257 + url: "https://pub.dev" 258 + source: hosted 259 + version: "1.15.0" 140 260 crypto: 141 - dependency: transitive 261 + dependency: "direct main" 142 262 description: 143 263 name: crypto 144 264 sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf ··· 161 281 url: "https://pub.dev" 162 282 source: hosted 163 283 version: "1.0.1" 284 + dart_style: 285 + dependency: transitive 286 + description: 287 + name: dart_style 288 + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b 289 + url: "https://pub.dev" 290 + source: hosted 291 + version: "3.1.3" 292 + diff_match_patch: 293 + dependency: transitive 294 + description: 295 + name: diff_match_patch 296 + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" 297 + url: "https://pub.dev" 298 + source: hosted 299 + version: "0.4.1" 164 300 drift: 165 301 dependency: "direct main" 166 302 description: 167 303 name: drift 168 - sha256: "61f876c0291b194980bafd203f48e85d5fb04e4a7334367d1a89f44004dbcb83" 304 + sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e" 305 + url: "https://pub.dev" 306 + source: hosted 307 + version: "2.31.0" 308 + drift_dev: 309 + dependency: "direct dev" 310 + description: 311 + name: drift_dev 312 + sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587" 313 + url: "https://pub.dev" 314 + source: hosted 315 + version: "2.31.0" 316 + drift_flutter: 317 + dependency: "direct main" 318 + description: 319 + name: drift_flutter 320 + sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7 169 321 url: "https://pub.dev" 170 322 source: hosted 171 - version: "2.32.0" 323 + version: "0.2.8" 324 + equatable: 325 + dependency: "direct main" 326 + description: 327 + name: equatable 328 + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" 329 + url: "https://pub.dev" 330 + source: hosted 331 + version: "2.0.8" 172 332 fake_async: 173 333 dependency: transitive 174 334 description: ··· 193 353 url: "https://pub.dev" 194 354 source: hosted 195 355 version: "7.0.1" 356 + fixnum: 357 + dependency: transitive 358 + description: 359 + name: fixnum 360 + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 361 + url: "https://pub.dev" 362 + source: hosted 363 + version: "1.1.1" 196 364 flutter: 197 365 dependency: "direct main" 198 366 description: flutter ··· 224 392 description: flutter 225 393 source: sdk 226 394 version: "0.0.0" 395 + freezed: 396 + dependency: "direct dev" 397 + description: 398 + name: freezed 399 + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" 400 + url: "https://pub.dev" 401 + source: hosted 402 + version: "3.2.3" 227 403 freezed_annotation: 228 - dependency: transitive 404 + dependency: "direct main" 229 405 description: 230 406 name: freezed_annotation 231 407 sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" 232 408 url: "https://pub.dev" 233 409 source: hosted 234 410 version: "3.1.0" 411 + frontend_server_client: 412 + dependency: transitive 413 + description: 414 + name: frontend_server_client 415 + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 416 + url: "https://pub.dev" 417 + source: hosted 418 + version: "4.0.0" 235 419 glob: 236 420 dependency: transitive 237 421 description: ··· 248 432 url: "https://pub.dev" 249 433 source: hosted 250 434 version: "17.1.0" 435 + graphs: 436 + dependency: transitive 437 + description: 438 + name: graphs 439 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" 440 + url: "https://pub.dev" 441 + source: hosted 442 + version: "2.3.2" 251 443 hex: 252 444 dependency: transitive 253 445 description: ··· 256 448 url: "https://pub.dev" 257 449 source: hosted 258 450 version: "0.2.0" 259 - hooks: 260 - dependency: transitive 261 - description: 262 - name: hooks 263 - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 264 - url: "https://pub.dev" 265 - source: hosted 266 - version: "1.0.2" 267 451 http: 268 452 dependency: transitive 269 453 description: ··· 272 456 url: "https://pub.dev" 273 457 source: hosted 274 458 version: "1.6.0" 459 + http_multi_server: 460 + dependency: transitive 461 + description: 462 + name: http_multi_server 463 + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 464 + url: "https://pub.dev" 465 + source: hosted 466 + version: "3.2.2" 275 467 http_parser: 276 468 dependency: transitive 277 469 description: ··· 280 472 url: "https://pub.dev" 281 473 source: hosted 282 474 version: "4.1.2" 475 + io: 476 + dependency: transitive 477 + description: 478 + name: io 479 + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b 480 + url: "https://pub.dev" 481 + source: hosted 482 + version: "1.0.5" 483 + js: 484 + dependency: transitive 485 + description: 486 + name: js 487 + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" 488 + url: "https://pub.dev" 489 + source: hosted 490 + version: "0.7.2" 283 491 json_annotation: 284 - dependency: transitive 492 + dependency: "direct main" 285 493 description: 286 494 name: json_annotation 287 - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 495 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 288 496 url: "https://pub.dev" 289 497 source: hosted 290 - version: "4.11.0" 498 + version: "4.9.0" 499 + json_serializable: 500 + dependency: "direct dev" 501 + description: 502 + name: json_serializable 503 + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 504 + url: "https://pub.dev" 505 + source: hosted 506 + version: "6.11.2" 291 507 leak_tracker: 292 508 dependency: transitive 293 509 description: ··· 360 576 url: "https://pub.dev" 361 577 source: hosted 362 578 version: "2.0.0" 579 + mocktail: 580 + dependency: "direct dev" 581 + description: 582 + name: mocktail 583 + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" 584 + url: "https://pub.dev" 585 + source: hosted 586 + version: "1.0.4" 363 587 multiformats: 364 588 dependency: transitive 365 589 description: ··· 376 600 url: "https://pub.dev" 377 601 source: hosted 378 602 version: "1.0.0" 379 - native_toolchain_c: 603 + nested: 604 + dependency: transitive 605 + description: 606 + name: nested 607 + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 608 + url: "https://pub.dev" 609 + source: hosted 610 + version: "1.0.0" 611 + node_preamble: 380 612 dependency: transitive 381 613 description: 382 - name: native_toolchain_c 383 - sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" 614 + name: node_preamble 615 + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 384 616 url: "https://pub.dev" 385 617 source: hosted 386 - version: "0.17.5" 387 - nested: 618 + version: "2.0.2" 619 + package_config: 388 620 dependency: transitive 389 621 description: 390 - name: nested 391 - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 622 + name: package_config 623 + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc 392 624 url: "https://pub.dev" 393 625 source: hosted 394 - version: "1.0.0" 626 + version: "2.2.0" 395 627 path: 396 - dependency: transitive 628 + dependency: "direct main" 397 629 description: 398 630 name: path 399 631 sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 400 632 url: "https://pub.dev" 401 633 source: hosted 402 634 version: "1.9.1" 635 + path_provider: 636 + dependency: "direct main" 637 + description: 638 + name: path_provider 639 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" 640 + url: "https://pub.dev" 641 + source: hosted 642 + version: "2.1.5" 643 + path_provider_android: 644 + dependency: transitive 645 + description: 646 + name: path_provider_android 647 + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e 648 + url: "https://pub.dev" 649 + source: hosted 650 + version: "2.2.22" 651 + path_provider_foundation: 652 + dependency: transitive 653 + description: 654 + name: path_provider_foundation 655 + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" 656 + url: "https://pub.dev" 657 + source: hosted 658 + version: "2.5.1" 659 + path_provider_linux: 660 + dependency: transitive 661 + description: 662 + name: path_provider_linux 663 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 664 + url: "https://pub.dev" 665 + source: hosted 666 + version: "2.2.1" 667 + path_provider_platform_interface: 668 + dependency: transitive 669 + description: 670 + name: path_provider_platform_interface 671 + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 672 + url: "https://pub.dev" 673 + source: hosted 674 + version: "2.1.2" 675 + path_provider_windows: 676 + dependency: transitive 677 + description: 678 + name: path_provider_windows 679 + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 680 + url: "https://pub.dev" 681 + source: hosted 682 + version: "2.3.0" 683 + platform: 684 + dependency: transitive 685 + description: 686 + name: platform 687 + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 688 + url: "https://pub.dev" 689 + source: hosted 690 + version: "3.1.6" 691 + plugin_platform_interface: 692 + dependency: transitive 693 + description: 694 + name: plugin_platform_interface 695 + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 696 + url: "https://pub.dev" 697 + source: hosted 698 + version: "2.1.8" 403 699 pointycastle: 404 700 dependency: transitive 405 701 description: ··· 408 704 url: "https://pub.dev" 409 705 source: hosted 410 706 version: "4.0.0" 707 + pool: 708 + dependency: transitive 709 + description: 710 + name: pool 711 + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" 712 + url: "https://pub.dev" 713 + source: hosted 714 + version: "1.5.2" 411 715 provider: 412 716 dependency: transitive 413 717 description: ··· 424 728 url: "https://pub.dev" 425 729 source: hosted 426 730 version: "2.2.0" 731 + pubspec_parse: 732 + dependency: transitive 733 + description: 734 + name: pubspec_parse 735 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" 736 + url: "https://pub.dev" 737 + source: hosted 738 + version: "1.5.0" 739 + recase: 740 + dependency: transitive 741 + description: 742 + name: recase 743 + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 744 + url: "https://pub.dev" 745 + source: hosted 746 + version: "4.1.0" 747 + shelf: 748 + dependency: transitive 749 + description: 750 + name: shelf 751 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 752 + url: "https://pub.dev" 753 + source: hosted 754 + version: "1.4.2" 755 + shelf_packages_handler: 756 + dependency: transitive 757 + description: 758 + name: shelf_packages_handler 759 + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 760 + url: "https://pub.dev" 761 + source: hosted 762 + version: "3.0.2" 763 + shelf_static: 764 + dependency: transitive 765 + description: 766 + name: shelf_static 767 + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 768 + url: "https://pub.dev" 769 + source: hosted 770 + version: "1.1.3" 771 + shelf_web_socket: 772 + dependency: transitive 773 + description: 774 + name: shelf_web_socket 775 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" 776 + url: "https://pub.dev" 777 + source: hosted 778 + version: "3.0.0" 427 779 sky_engine: 428 780 dependency: transitive 429 781 description: flutter 430 782 source: sdk 431 783 version: "0.0.0" 784 + source_gen: 785 + dependency: transitive 786 + description: 787 + name: source_gen 788 + sha256: adc962c96fffb2de1728ef396a995aaedcafbe635abdca13d2a987ce17e57751 789 + url: "https://pub.dev" 790 + source: hosted 791 + version: "4.2.1" 792 + source_helper: 793 + dependency: transitive 794 + description: 795 + name: source_helper 796 + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" 797 + url: "https://pub.dev" 798 + source: hosted 799 + version: "1.3.8" 800 + source_map_stack_trace: 801 + dependency: transitive 802 + description: 803 + name: source_map_stack_trace 804 + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b 805 + url: "https://pub.dev" 806 + source: hosted 807 + version: "2.1.2" 808 + source_maps: 809 + dependency: transitive 810 + description: 811 + name: source_maps 812 + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" 813 + url: "https://pub.dev" 814 + source: hosted 815 + version: "0.10.13" 432 816 source_span: 433 817 dependency: transitive 434 818 description: ··· 437 821 url: "https://pub.dev" 438 822 source: hosted 439 823 version: "1.10.2" 824 + sqflite_common: 825 + dependency: transitive 826 + description: 827 + name: sqflite_common 828 + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" 829 + url: "https://pub.dev" 830 + source: hosted 831 + version: "2.5.6" 832 + sqflite_common_ffi: 833 + dependency: "direct dev" 834 + description: 835 + name: sqflite_common_ffi 836 + sha256: "8d7b8749a516cbf6e9057f9b480b716ad14fc4f3d3873ca6938919cc626d9025" 837 + url: "https://pub.dev" 838 + source: hosted 839 + version: "2.3.7+1" 440 840 sqlite3: 441 841 dependency: transitive 442 842 description: 443 843 name: sqlite3 444 - sha256: caa693ad15a587a2b4fde093b728131a1827903872171089dedb16f7665d3a91 844 + sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" 845 + url: "https://pub.dev" 846 + source: hosted 847 + version: "2.9.4" 848 + sqlite3_flutter_libs: 849 + dependency: transitive 850 + description: 851 + name: sqlite3_flutter_libs 852 + sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad 445 853 url: "https://pub.dev" 446 854 source: hosted 447 - version: "3.2.0" 855 + version: "0.5.42" 856 + sqlparser: 857 + dependency: transitive 858 + description: 859 + name: sqlparser 860 + sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19" 861 + url: "https://pub.dev" 862 + source: hosted 863 + version: "0.43.1" 448 864 stack_trace: 449 865 dependency: transitive 450 866 description: ··· 461 877 url: "https://pub.dev" 462 878 source: hosted 463 879 version: "2.1.4" 880 + stream_transform: 881 + dependency: transitive 882 + description: 883 + name: stream_transform 884 + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 885 + url: "https://pub.dev" 886 + source: hosted 887 + version: "2.1.1" 464 888 string_scanner: 465 889 dependency: transitive 466 890 description: ··· 469 893 url: "https://pub.dev" 470 894 source: hosted 471 895 version: "1.4.1" 896 + synchronized: 897 + dependency: transitive 898 + description: 899 + name: synchronized 900 + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 901 + url: "https://pub.dev" 902 + source: hosted 903 + version: "3.4.0" 472 904 term_glyph: 473 905 dependency: transitive 474 906 description: ··· 477 909 url: "https://pub.dev" 478 910 source: hosted 479 911 version: "1.2.2" 912 + test: 913 + dependency: transitive 914 + description: 915 + name: test 916 + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" 917 + url: "https://pub.dev" 918 + source: hosted 919 + version: "1.26.3" 480 920 test_api: 481 921 dependency: transitive 482 922 description: ··· 485 925 url: "https://pub.dev" 486 926 source: hosted 487 927 version: "0.7.7" 928 + test_core: 929 + dependency: transitive 930 + description: 931 + name: test_core 932 + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" 933 + url: "https://pub.dev" 934 + source: hosted 935 + version: "0.6.12" 488 936 typed_data: 489 937 dependency: transitive 490 938 description: ··· 493 941 url: "https://pub.dev" 494 942 source: hosted 495 943 version: "1.4.0" 944 + url_launcher: 945 + dependency: "direct main" 946 + description: 947 + name: url_launcher 948 + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 949 + url: "https://pub.dev" 950 + source: hosted 951 + version: "6.3.2" 952 + url_launcher_android: 953 + dependency: transitive 954 + description: 955 + name: url_launcher_android 956 + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" 957 + url: "https://pub.dev" 958 + source: hosted 959 + version: "6.3.28" 960 + url_launcher_ios: 961 + dependency: transitive 962 + description: 963 + name: url_launcher_ios 964 + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" 965 + url: "https://pub.dev" 966 + source: hosted 967 + version: "6.4.1" 968 + url_launcher_linux: 969 + dependency: transitive 970 + description: 971 + name: url_launcher_linux 972 + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a 973 + url: "https://pub.dev" 974 + source: hosted 975 + version: "3.2.2" 976 + url_launcher_macos: 977 + dependency: transitive 978 + description: 979 + name: url_launcher_macos 980 + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" 981 + url: "https://pub.dev" 982 + source: hosted 983 + version: "3.2.5" 984 + url_launcher_platform_interface: 985 + dependency: transitive 986 + description: 987 + name: url_launcher_platform_interface 988 + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" 989 + url: "https://pub.dev" 990 + source: hosted 991 + version: "2.3.2" 992 + url_launcher_web: 993 + dependency: transitive 994 + description: 995 + name: url_launcher_web 996 + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f 997 + url: "https://pub.dev" 998 + source: hosted 999 + version: "2.4.2" 1000 + url_launcher_windows: 1001 + dependency: transitive 1002 + description: 1003 + name: url_launcher_windows 1004 + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" 1005 + url: "https://pub.dev" 1006 + source: hosted 1007 + version: "3.1.5" 496 1008 vector_math: 497 1009 dependency: transitive 498 1010 description: ··· 509 1021 url: "https://pub.dev" 510 1022 source: hosted 511 1023 version: "15.0.2" 1024 + watcher: 1025 + dependency: transitive 1026 + description: 1027 + name: watcher 1028 + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" 1029 + url: "https://pub.dev" 1030 + source: hosted 1031 + version: "1.2.1" 512 1032 web: 513 1033 dependency: transitive 514 1034 description: ··· 533 1053 url: "https://pub.dev" 534 1054 source: hosted 535 1055 version: "3.0.3" 1056 + webkit_inspection_protocol: 1057 + dependency: transitive 1058 + description: 1059 + name: webkit_inspection_protocol 1060 + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 1061 + url: "https://pub.dev" 1062 + source: hosted 1063 + version: "1.2.1" 1064 + xdg_directories: 1065 + dependency: transitive 1066 + description: 1067 + name: xdg_directories 1068 + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 1069 + url: "https://pub.dev" 1070 + source: hosted 1071 + version: "1.1.0" 536 1072 xrpc: 537 1073 dependency: transitive 538 1074 description: ··· 551 1087 version: "3.1.3" 552 1088 sdks: 553 1089 dart: ">=3.10.1 <4.0.0" 554 - flutter: ">=3.35.0" 1090 + flutter: ">=3.38.0"
+17 -59
pubspec.yaml
··· 1 1 name: lazurite 2 2 description: "A new Flutter project." 3 - # The following line prevents the package from being accidentally published to 4 - # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 - publish_to: "none" # Remove this line if you wish to publish to pub.dev 6 - 7 - # The following defines the version and build number for your application. 8 - # A version number is three numbers separated by dots, like 1.2.43 9 - # followed by an optional build number separated by a +. 10 - # Both the version and the builder number may be overridden in flutter 11 - # build by specifying --build-name and --build-number, respectively. 12 - # In Android, build-name is used as versionName while build-number used as versionCode. 13 - # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 - # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 - # Read more about iOS versioning at 16 - # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 - # In Windows, build-name is used as the major, minor, and patch parts 18 - # of the product and file versions while build-number is used as the build suffix. 3 + publish_to: "none" 19 4 version: 1.0.0+1 20 5 21 6 environment: ··· 24 9 dependencies: 25 10 flutter: 26 11 sdk: flutter 27 - 28 12 cupertino_icons: ^1.0.8 29 13 bluesky: ^1.4.1 30 14 bluesky_text: ^1.1.1 31 15 atproto_oauth: ^0.2.0 32 16 flutter_bloc: ^9.1.1 33 - drift: ^2.12.0 17 + drift: ^2.24.0 18 + drift_flutter: ^0.2.8 34 19 go_router: ^17.1.0 20 + path_provider: ^2.1.5 21 + path: ^1.9.0 22 + equatable: ^2.0.7 23 + freezed_annotation: ^3.1.0 24 + json_annotation: ^4.9.0 25 + crypto: ^3.0.6 26 + url_launcher: ^6.3.1 35 27 36 28 dev_dependencies: 37 29 flutter_test: 38 30 sdk: flutter 39 - 40 - # The "flutter_lints" package below contains a set of recommended lints to 41 - # encourage good coding practices. The lint set provided by the package is 42 - # activated in the `analysis_options.yaml` file located at the root of your 43 - # package. See that file for information about deactivating specific lint 44 - # rules and activating additional ones. 45 31 flutter_lints: ^6.0.0 46 - 47 - # For information on the generic Dart part of this file, see the 48 - # following page: https://dart.dev/tools/pub/pubspec 32 + drift_dev: ^2.24.0 33 + build_runner: ^2.4.15 34 + freezed: ^3.0.0 35 + json_serializable: ^6.8.0 36 + bloc_test: ^10.0.0 37 + mocktail: ^1.0.4 38 + sqflite_common_ffi: ^2.3.5 49 39 50 - # The following section is specific to Flutter packages. 51 40 flutter: 52 41 uses-material-design: true 53 - 54 - # To add assets to your application, add an assets section, like this: 55 - # assets: 56 - # - images/a_dot_burr.jpeg 57 - # - images/a_dot_ham.jpeg 58 - 59 - # An image asset can refer to one or more resolution-specific "variants", see 60 - # https://flutter.dev/to/resolution-aware-images 61 - 62 - # For details regarding adding assets from package dependencies, see 63 - # https://flutter.dev/to/asset-from-package 64 - 65 - # To add custom fonts to your application, add a fonts section here, 66 - # in this "flutter" section. Each entry in this list should have a 67 - # "family" key with the font family name, and a "fonts" key with a 68 - # list giving the asset and other descriptors for the font. For 69 - # example: 70 - # fonts: 71 - # - family: Schyler 72 - # fonts: 73 - # - asset: fonts/Schyler-Regular.ttf 74 - # - asset: fonts/Schyler-Italic.ttf 75 - # style: italic 76 - # - family: Trajan Pro 77 - # fonts: 78 - # - asset: fonts/TrajanPro.ttf 79 - # - asset: fonts/TrajanPro_Bold.ttf 80 - # weight: 700 81 - # 82 - # For details regarding fonts from package dependencies, 83 - # see https://flutter.dev/to/font-from-package
+168
test/core/database/app_database_test.dart
··· 1 + import 'package:flutter_test/flutter_test.dart'; 2 + import 'package:lazurite/core/database/app_database.dart'; 3 + import 'package:sqflite_common_ffi/sqflite_ffi.dart'; 4 + 5 + void main() { 6 + late AppDatabase database; 7 + 8 + setUpAll(() { 9 + sqfliteFfiInit(); 10 + databaseFactory = databaseFactoryFfi; 11 + }); 12 + 13 + setUp(() async { 14 + database = AppDatabase(); 15 + }); 16 + 17 + tearDown(() async { 18 + await database.close(); 19 + }); 20 + 21 + group('AppDatabase', () { 22 + group('Account operations', () { 23 + test('should insert and retrieve an account', () async { 24 + final account = AccountsCompanion.insert( 25 + did: 'did:plc:abc123', 26 + handle: 'user.bsky.social', 27 + accessToken: 'access_token', 28 + ); 29 + 30 + await database.insertAccount(account); 31 + final retrieved = await database.getAccount('did:plc:abc123'); 32 + 33 + expect(retrieved, isNotNull); 34 + expect(retrieved!.did, equals('did:plc:abc123')); 35 + expect(retrieved.handle, equals('user.bsky.social')); 36 + expect(retrieved.accessToken, equals('access_token')); 37 + }); 38 + 39 + test('should return null for non-existent account', () async { 40 + final result = await database.getAccount('did:plc:nonexistent'); 41 + expect(result, isNull); 42 + }); 43 + 44 + test('should get active account', () async { 45 + final account = AccountsCompanion.insert( 46 + did: 'did:plc:abc123', 47 + handle: 'user.bsky.social', 48 + accessToken: 'access_token', 49 + ); 50 + 51 + await database.insertAccount(account); 52 + final active = await database.getActiveAccount(); 53 + 54 + expect(active, isNotNull); 55 + expect(active!.did, equals('did:plc:abc123')); 56 + }); 57 + 58 + test('should return null when no active account exists', () async { 59 + final active = await database.getActiveAccount(); 60 + expect(active, isNull); 61 + }); 62 + 63 + test('should get all accounts', () async { 64 + final account1 = AccountsCompanion.insert( 65 + did: 'did:plc:abc123', 66 + handle: 'user1.bsky.social', 67 + accessToken: 'token1', 68 + ); 69 + final account2 = AccountsCompanion.insert( 70 + did: 'did:plc:def456', 71 + handle: 'user2.bsky.social', 72 + accessToken: 'token2', 73 + ); 74 + 75 + await database.insertAccount(account1); 76 + await database.insertAccount(account2); 77 + 78 + final accounts = await database.getAllAccounts(); 79 + expect(accounts.length, equals(2)); 80 + }); 81 + 82 + test('should delete account', () async { 83 + final account = AccountsCompanion.insert( 84 + did: 'did:plc:abc123', 85 + handle: 'user.bsky.social', 86 + accessToken: 'access_token', 87 + ); 88 + 89 + await database.insertAccount(account); 90 + await database.deleteAccount('did:plc:abc123'); 91 + 92 + final retrieved = await database.getAccount('did:plc:abc123'); 93 + expect(retrieved, isNull); 94 + }); 95 + 96 + test('should delete all accounts', () async { 97 + final account1 = AccountsCompanion.insert( 98 + did: 'did:plc:abc123', 99 + handle: 'user1.bsky.social', 100 + accessToken: 'token1', 101 + ); 102 + final account2 = AccountsCompanion.insert( 103 + did: 'did:plc:def456', 104 + handle: 'user2.bsky.social', 105 + accessToken: 'token2', 106 + ); 107 + 108 + await database.insertAccount(account1); 109 + await database.insertAccount(account2); 110 + await database.deleteAllAccounts(); 111 + 112 + final accounts = await database.getAllAccounts(); 113 + expect(accounts, isEmpty); 114 + }); 115 + 116 + test('should update account tokens', () async { 117 + final account = AccountsCompanion.insert( 118 + did: 'did:plc:abc123', 119 + handle: 'user.bsky.social', 120 + accessToken: 'old_token', 121 + ); 122 + 123 + await database.insertAccount(account); 124 + final updated = await database.updateAccountTokens( 125 + 'did:plc:abc123', 126 + accessToken: 'new_token', 127 + refreshToken: 'new_refresh', 128 + ); 129 + 130 + expect(updated, isTrue); 131 + 132 + final retrieved = await database.getAccount('did:plc:abc123'); 133 + expect(retrieved!.accessToken, equals('new_token')); 134 + expect(retrieved.refreshToken, equals('new_refresh')); 135 + }); 136 + }); 137 + 138 + group('Settings operations', () { 139 + test('should set and get setting', () async { 140 + await database.setSetting('theme', 'dark'); 141 + final value = await database.getSetting('theme'); 142 + 143 + expect(value, equals('dark')); 144 + }); 145 + 146 + test('should return null for non-existent setting', () async { 147 + final value = await database.getSetting('nonexistent'); 148 + expect(value, isNull); 149 + }); 150 + 151 + test('should update existing setting', () async { 152 + await database.setSetting('theme', 'light'); 153 + await database.setSetting('theme', 'dark'); 154 + final value = await database.getSetting('theme'); 155 + 156 + expect(value, equals('dark')); 157 + }); 158 + 159 + test('should delete setting', () async { 160 + await database.setSetting('theme', 'dark'); 161 + await database.deleteSetting('theme'); 162 + final value = await database.getSetting('theme'); 163 + 164 + expect(value, isNull); 165 + }); 166 + }); 167 + }); 168 + }
+106
test/features/auth/bloc/auth_bloc_test.dart
··· 1 + import 'package:bloc_test/bloc_test.dart'; 2 + import 'package:flutter_test/flutter_test.dart'; 3 + import 'package:mocktail/mocktail.dart'; 4 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 5 + import 'package:lazurite/features/auth/data/auth_repository.dart'; 6 + import 'package:lazurite/features/auth/data/models/auth_models.dart'; 7 + 8 + class MockAuthRepository extends Mock implements AuthRepository {} 9 + 10 + void main() { 11 + late MockAuthRepository mockAuthRepository; 12 + 13 + setUp(() { 14 + mockAuthRepository = MockAuthRepository(); 15 + }); 16 + 17 + group('AuthBloc', () { 18 + const tokens = AuthTokens( 19 + accessToken: 'access_token', 20 + refreshToken: 'refresh_token', 21 + did: 'did:plc:abc123', 22 + handle: 'user.bsky.social', 23 + ); 24 + 25 + blocTest<AuthBloc, AuthState>( 26 + 'emits [unauthenticated] when initial state is unauthenticated', 27 + build: () => AuthBloc(authRepository: mockAuthRepository), 28 + verify: (bloc) { 29 + expect(bloc.state.status, AuthStatus.unauthenticated); 30 + }, 31 + ); 32 + 33 + blocTest<AuthBloc, AuthState>( 34 + 'emits [authenticating, authenticated] when LoginRequested is added and succeeds', 35 + build: () => AuthBloc(authRepository: mockAuthRepository), 36 + setUp: () { 37 + when(() => mockAuthRepository.loginWithAppPassword(any(), any())).thenAnswer((_) async => tokens); 38 + }, 39 + act: (bloc) => bloc.add(const LoginRequested(handle: 'user.bsky.social', appPassword: 'password')), 40 + expect: () => [const AuthState.authenticating(), const AuthState.authenticated(tokens)], 41 + ); 42 + 43 + blocTest<AuthBloc, AuthState>( 44 + 'emits [authenticating, authError] when LoginRequested is added and fails', 45 + build: () => AuthBloc(authRepository: mockAuthRepository), 46 + setUp: () { 47 + when(() => mockAuthRepository.loginWithAppPassword(any(), any())).thenThrow(Exception('Login failed')); 48 + }, 49 + act: (bloc) => bloc.add(const LoginRequested(handle: 'user.bsky.social', appPassword: 'password')), 50 + expect: () => [ 51 + const AuthState.authenticating(), 52 + predicate<AuthState>( 53 + (state) => state.status == AuthStatus.authError && state.errorMessage!.contains('Login failed'), 54 + ), 55 + ], 56 + ); 57 + 58 + blocTest<AuthBloc, AuthState>( 59 + 'emits [authenticating, authenticated] when OAuthLoginRequested is added and succeeds', 60 + build: () => AuthBloc(authRepository: mockAuthRepository), 61 + setUp: () { 62 + when(() => mockAuthRepository.loginWithOAuth(any())).thenAnswer((_) async => tokens); 63 + }, 64 + act: (bloc) => bloc.add(const OAuthLoginRequested(handle: 'user.bsky.social')), 65 + expect: () => [const AuthState.authenticating(), const AuthState.authenticated(tokens)], 66 + ); 67 + 68 + blocTest<AuthBloc, AuthState>( 69 + 'emits [unauthenticated] when LogoutRequested is added', 70 + build: () => AuthBloc(authRepository: mockAuthRepository), 71 + seed: () => const AuthState.authenticated(tokens), 72 + setUp: () { 73 + when(() => mockAuthRepository.logout()).thenAnswer((_) async {}); 74 + }, 75 + act: (bloc) => bloc.add(const LogoutRequested()), 76 + expect: () => [const AuthState.unauthenticated()], 77 + ); 78 + 79 + blocTest<AuthBloc, AuthState>( 80 + 'emits [authenticated] when SessionRestored is added', 81 + build: () => AuthBloc(authRepository: mockAuthRepository), 82 + act: (bloc) => bloc.add(const SessionRestored(tokens: tokens)), 83 + expect: () => [const AuthState.authenticated(tokens)], 84 + ); 85 + 86 + blocTest<AuthBloc, AuthState>( 87 + 'emits [unauthenticated] when CheckSessionRequested is added and no session exists', 88 + build: () => AuthBloc(authRepository: mockAuthRepository), 89 + setUp: () { 90 + when(() => mockAuthRepository.getStoredSession()).thenAnswer((_) async => null); 91 + }, 92 + act: (bloc) => bloc.add(const CheckSessionRequested()), 93 + expect: () => [const AuthState.unauthenticated()], 94 + ); 95 + 96 + blocTest<AuthBloc, AuthState>( 97 + 'emits [authenticated] when CheckSessionRequested is added and valid session exists', 98 + build: () => AuthBloc(authRepository: mockAuthRepository), 99 + setUp: () { 100 + when(() => mockAuthRepository.getStoredSession()).thenAnswer((_) async => tokens); 101 + }, 102 + act: (bloc) => bloc.add(const CheckSessionRequested()), 103 + expect: () => [const AuthState.authenticated(tokens)], 104 + ); 105 + }); 106 + }
+72
test/features/auth/bloc/auth_event_test.dart
··· 1 + import 'package:flutter_test/flutter_test.dart'; 2 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 3 + import 'package:lazurite/features/auth/data/models/auth_models.dart'; 4 + 5 + void main() { 6 + group('AuthEvent', () { 7 + const tokens = AuthTokens(accessToken: 'token', did: 'did:plc:abc', handle: 'user.bsky.social'); 8 + 9 + group('LoginRequested', () { 10 + test('should support value equality', () { 11 + const event1 = LoginRequested(handle: 'user.bsky.social', appPassword: 'password'); 12 + const event2 = LoginRequested(handle: 'user.bsky.social', appPassword: 'password'); 13 + 14 + expect(event1, equals(event2)); 15 + }); 16 + 17 + test('should have correct props', () { 18 + const event = LoginRequested(handle: 'user.bsky.social', appPassword: 'password'); 19 + 20 + expect(event.props, equals(['user.bsky.social', 'password'])); 21 + }); 22 + }); 23 + 24 + group('OAuthLoginRequested', () { 25 + test('should support value equality', () { 26 + const event1 = OAuthLoginRequested(handle: 'user.bsky.social'); 27 + const event2 = OAuthLoginRequested(handle: 'user.bsky.social'); 28 + 29 + expect(event1, equals(event2)); 30 + }); 31 + 32 + test('should have correct props', () { 33 + const event = OAuthLoginRequested(handle: 'user.bsky.social'); 34 + 35 + expect(event.props, equals(['user.bsky.social'])); 36 + }); 37 + }); 38 + 39 + group('LogoutRequested', () { 40 + test('should support value equality', () { 41 + const event1 = LogoutRequested(); 42 + const event2 = LogoutRequested(); 43 + 44 + expect(event1, equals(event2)); 45 + }); 46 + }); 47 + 48 + group('SessionRestored', () { 49 + test('should support value equality', () { 50 + const event1 = SessionRestored(tokens: tokens); 51 + const event2 = SessionRestored(tokens: tokens); 52 + 53 + expect(event1, equals(event2)); 54 + }); 55 + 56 + test('should have correct props', () { 57 + const event = SessionRestored(tokens: tokens); 58 + 59 + expect(event.props, equals([tokens])); 60 + }); 61 + }); 62 + 63 + group('CheckSessionRequested', () { 64 + test('should support value equality', () { 65 + const event1 = CheckSessionRequested(); 66 + const event2 = CheckSessionRequested(); 67 + 68 + expect(event1, equals(event2)); 69 + }); 70 + }); 71 + }); 72 + }
+74
test/features/auth/bloc/auth_state_test.dart
··· 1 + import 'package:flutter_test/flutter_test.dart'; 2 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 3 + import 'package:lazurite/features/auth/data/models/auth_models.dart'; 4 + 5 + void main() { 6 + group('AuthState', () { 7 + const tokens = AuthTokens(accessToken: 'token', did: 'did:plc:abc', handle: 'user.bsky.social'); 8 + 9 + test('should create unauthenticated state', () { 10 + const state = AuthState.unauthenticated(); 11 + 12 + expect(state.status, equals(AuthStatus.unauthenticated)); 13 + expect(state.tokens, isNull); 14 + expect(state.errorMessage, isNull); 15 + expect(state.isAuthenticated, isFalse); 16 + expect(state.isLoading, isFalse); 17 + expect(state.hasError, isFalse); 18 + }); 19 + 20 + test('should create authenticating state', () { 21 + const state = AuthState.authenticating(); 22 + 23 + expect(state.status, equals(AuthStatus.authenticating)); 24 + expect(state.tokens, isNull); 25 + expect(state.errorMessage, isNull); 26 + expect(state.isAuthenticated, isFalse); 27 + expect(state.isLoading, isTrue); 28 + expect(state.hasError, isFalse); 29 + }); 30 + 31 + test('should create authenticated state', () { 32 + const state = AuthState.authenticated(tokens); 33 + 34 + expect(state.status, equals(AuthStatus.authenticated)); 35 + expect(state.tokens, equals(tokens)); 36 + expect(state.errorMessage, isNull); 37 + expect(state.isAuthenticated, isTrue); 38 + expect(state.isLoading, isFalse); 39 + expect(state.hasError, isFalse); 40 + }); 41 + 42 + test('should create authError state', () { 43 + const state = AuthState.authError('Error message'); 44 + 45 + expect(state.status, equals(AuthStatus.authError)); 46 + expect(state.tokens, isNull); 47 + expect(state.errorMessage, equals('Error message')); 48 + expect(state.isAuthenticated, isFalse); 49 + expect(state.isLoading, isFalse); 50 + expect(state.hasError, isTrue); 51 + }); 52 + 53 + test('should copy with new values', () { 54 + const state = AuthState.unauthenticated(); 55 + final newState = state.copyWith(status: AuthStatus.authenticated, tokens: tokens); 56 + 57 + expect(newState.status, equals(AuthStatus.authenticated)); 58 + expect(newState.tokens, equals(tokens)); 59 + }); 60 + 61 + test('should support value equality', () { 62 + const state1 = AuthState.unauthenticated(); 63 + const state2 = AuthState.unauthenticated(); 64 + 65 + expect(state1, equals(state2)); 66 + }); 67 + 68 + test('should have correct props', () { 69 + const state = AuthState.authenticated(tokens); 70 + 71 + expect(state.props, equals([AuthStatus.authenticated, tokens, null])); 72 + }); 73 + }); 74 + }
+97
test/features/auth/data/auth_repository_test.dart
··· 1 + import 'package:flutter_test/flutter_test.dart'; 2 + import 'package:mocktail/mocktail.dart'; 3 + import 'package:lazurite/core/database/app_database.dart'; 4 + import 'package:lazurite/features/auth/data/auth_repository.dart'; 5 + import 'package:lazurite/features/auth/data/models/auth_models.dart'; 6 + 7 + class MockAppDatabase extends Mock implements AppDatabase {} 8 + 9 + class FakeAccountsCompanion extends Fake implements AccountsCompanion {} 10 + 11 + void main() { 12 + late AuthRepository authRepository; 13 + late MockAppDatabase mockDatabase; 14 + 15 + setUpAll(() { 16 + registerFallbackValue(FakeAccountsCompanion()); 17 + }); 18 + 19 + setUp(() { 20 + mockDatabase = MockAppDatabase(); 21 + authRepository = AuthRepository(database: mockDatabase); 22 + }); 23 + 24 + group('AuthRepository', () { 25 + group('getStoredSession', () { 26 + test('should return null when no account exists', () async { 27 + when(() => mockDatabase.getActiveAccount()).thenAnswer((_) async => null); 28 + 29 + final result = await authRepository.getStoredSession(); 30 + 31 + expect(result, isNull); 32 + verify(() => mockDatabase.getActiveAccount()).called(1); 33 + }); 34 + 35 + test('should return AuthTokens when account exists', () async { 36 + final account = Account( 37 + did: 'did:plc:abc123', 38 + handle: 'user.bsky.social', 39 + accessToken: 'access_token', 40 + refreshToken: 'refresh_token', 41 + displayName: 'User Name', 42 + createdAt: DateTime.now(), 43 + updatedAt: DateTime.now(), 44 + ); 45 + 46 + when(() => mockDatabase.getActiveAccount()).thenAnswer((_) async => account); 47 + 48 + final result = await authRepository.getStoredSession(); 49 + 50 + expect(result, isNotNull); 51 + expect(result!.did, equals('did:plc:abc123')); 52 + expect(result.handle, equals('user.bsky.social')); 53 + expect(result.accessToken, equals('access_token')); 54 + expect(result.refreshToken, equals('refresh_token')); 55 + expect(result.displayName, equals('User Name')); 56 + }); 57 + }); 58 + 59 + group('saveSession', () { 60 + test('should save session to database', () async { 61 + const tokens = AuthTokens( 62 + accessToken: 'access_token', 63 + refreshToken: 'refresh_token', 64 + did: 'did:plc:abc123', 65 + handle: 'user.bsky.social', 66 + displayName: 'User Name', 67 + ); 68 + 69 + when(() => mockDatabase.insertAccount(any())).thenAnswer((_) async => 1); 70 + 71 + await authRepository.saveSession(tokens); 72 + 73 + verify(() => mockDatabase.insertAccount(any())).called(1); 74 + }); 75 + }); 76 + 77 + group('clearSession', () { 78 + test('should delete all accounts', () async { 79 + when(() => mockDatabase.deleteAllAccounts()).thenAnswer((_) async => 1); 80 + 81 + await authRepository.clearSession(); 82 + 83 + verify(() => mockDatabase.deleteAllAccounts()).called(1); 84 + }); 85 + }); 86 + 87 + group('logout', () { 88 + test('should clear session', () async { 89 + when(() => mockDatabase.deleteAllAccounts()).thenAnswer((_) async => 1); 90 + 91 + await authRepository.logout(); 92 + 93 + verify(() => mockDatabase.deleteAllAccounts()).called(1); 94 + }); 95 + }); 96 + }); 97 + }
+124
test/features/auth/data/models/auth_models_test.dart
··· 1 + import 'package:flutter_test/flutter_test.dart'; 2 + import 'package:lazurite/features/auth/data/models/auth_models.dart'; 3 + 4 + void main() { 5 + group('AuthTokens', () { 6 + test('should create AuthTokens with all fields', () { 7 + const tokens = AuthTokens( 8 + accessToken: 'access_token', 9 + refreshToken: 'refresh_token', 10 + did: 'did:plc:abc123', 11 + handle: 'user.bsky.social', 12 + displayName: 'User Name', 13 + ); 14 + 15 + expect(tokens.accessToken, equals('access_token')); 16 + expect(tokens.refreshToken, equals('refresh_token')); 17 + expect(tokens.did, equals('did:plc:abc123')); 18 + expect(tokens.handle, equals('user.bsky.social')); 19 + expect(tokens.displayName, equals('User Name')); 20 + }); 21 + 22 + test('should create AuthTokens without optional fields', () { 23 + const tokens = AuthTokens(accessToken: 'access_token', did: 'did:plc:abc123', handle: 'user.bsky.social'); 24 + 25 + expect(tokens.accessToken, equals('access_token')); 26 + expect(tokens.refreshToken, isNull); 27 + expect(tokens.did, equals('did:plc:abc123')); 28 + expect(tokens.handle, equals('user.bsky.social')); 29 + expect(tokens.displayName, isNull); 30 + }); 31 + 32 + test('should copy with new values', () { 33 + const tokens = AuthTokens(accessToken: 'old_token', did: 'did:plc:abc123', handle: 'user.bsky.social'); 34 + 35 + final newTokens = tokens.copyWith(accessToken: 'new_token', displayName: 'New Name'); 36 + 37 + expect(newTokens.accessToken, equals('new_token')); 38 + expect(newTokens.did, equals('did:plc:abc123')); 39 + expect(newTokens.displayName, equals('New Name')); 40 + }); 41 + 42 + test('should check if tokens are expired', () { 43 + final expiredDate = DateTime.now().subtract(const Duration(hours: 1)); 44 + final tokens = AuthTokens( 45 + accessToken: 'token', 46 + did: 'did:plc:abc123', 47 + handle: 'user.bsky.social', 48 + expiresAt: expiredDate, 49 + ); 50 + 51 + expect(tokens.isExpired, isTrue); 52 + }); 53 + 54 + test('should check if tokens are not expired', () { 55 + final futureDate = DateTime.now().add(const Duration(hours: 1)); 56 + final tokens = AuthTokens( 57 + accessToken: 'token', 58 + did: 'did:plc:abc123', 59 + handle: 'user.bsky.social', 60 + expiresAt: futureDate, 61 + ); 62 + 63 + expect(tokens.isExpired, isFalse); 64 + }); 65 + 66 + test('should check if tokens without expiry are not expired', () { 67 + const tokens = AuthTokens(accessToken: 'token', did: 'did:plc:abc123', handle: 'user.bsky.social'); 68 + 69 + expect(tokens.isExpired, isFalse); 70 + }); 71 + 72 + test('should support value equality', () { 73 + const tokens1 = AuthTokens(accessToken: 'token', did: 'did:plc:abc123', handle: 'user.bsky.social'); 74 + const tokens2 = AuthTokens(accessToken: 'token', did: 'did:plc:abc123', handle: 'user.bsky.social'); 75 + 76 + expect(tokens1, equals(tokens2)); 77 + }); 78 + }); 79 + 80 + group('User', () { 81 + test('should create User with all fields', () { 82 + const user = User( 83 + did: 'did:plc:abc123', 84 + handle: 'user.bsky.social', 85 + displayName: 'User Name', 86 + avatar: 'https://example.com/avatar.jpg', 87 + description: 'User description', 88 + ); 89 + 90 + expect(user.did, equals('did:plc:abc123')); 91 + expect(user.handle, equals('user.bsky.social')); 92 + expect(user.displayName, equals('User Name')); 93 + expect(user.avatar, equals('https://example.com/avatar.jpg')); 94 + expect(user.description, equals('User description')); 95 + }); 96 + 97 + test('should create User without optional fields', () { 98 + const user = User(did: 'did:plc:abc123', handle: 'user.bsky.social'); 99 + 100 + expect(user.did, equals('did:plc:abc123')); 101 + expect(user.handle, equals('user.bsky.social')); 102 + expect(user.displayName, isNull); 103 + expect(user.avatar, isNull); 104 + expect(user.description, isNull); 105 + }); 106 + 107 + test('should copy with new values', () { 108 + const user = User(did: 'did:plc:abc123', handle: 'user.bsky.social'); 109 + 110 + final newUser = user.copyWith(displayName: 'New Name', avatar: 'https://example.com/new-avatar.jpg'); 111 + 112 + expect(newUser.did, equals('did:plc:abc123')); 113 + expect(newUser.displayName, equals('New Name')); 114 + expect(newUser.avatar, equals('https://example.com/new-avatar.jpg')); 115 + }); 116 + 117 + test('should support value equality', () { 118 + const user1 = User(did: 'did:plc:abc123', handle: 'user.bsky.social'); 119 + const user2 = User(did: 'did:plc:abc123', handle: 'user.bsky.social'); 120 + 121 + expect(user1, equals(user2)); 122 + }); 123 + }); 124 + }
-30
test/widget_test.dart
··· 1 - // This is a basic Flutter widget test. 2 - // 3 - // To perform an interaction with a widget in your test, use the WidgetTester 4 - // utility in the flutter_test package. For example, you can send tap and scroll 5 - // gestures. You can also use WidgetTester to find child widgets in the widget 6 - // tree, read text, and verify that the values of widget properties are correct. 7 - 8 - import 'package:flutter/material.dart'; 9 - import 'package:flutter_test/flutter_test.dart'; 10 - 11 - import 'package:lazurite/main.dart'; 12 - 13 - void main() { 14 - testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 - // Build our app and trigger a frame. 16 - await tester.pumpWidget(const MyApp()); 17 - 18 - // Verify that our counter starts at 0. 19 - expect(find.text('0'), findsOneWidget); 20 - expect(find.text('1'), findsNothing); 21 - 22 - // Tap the '+' icon and trigger a frame. 23 - await tester.tap(find.byIcon(Icons.add)); 24 - await tester.pump(); 25 - 26 - // Verify that our counter has incremented. 27 - expect(find.text('0'), findsNothing); 28 - expect(find.text('1'), findsOneWidget); 29 - }); 30 - }