| .. | .. |
|---|
| 63 | 63 | color: _isUser |
|---|
| 64 | 64 | ? (isDark ? AppColors.userBubble : AppColors.lightUserBubble) |
|---|
| 65 | 65 | : (isDark |
|---|
| 66 | | - ? AppColors.assistantBubble |
|---|
| 67 | | - : AppColors.lightAssistantBubble), |
|---|
| 66 | + ? AppColors.assistantBubble |
|---|
| 67 | + : AppColors.lightAssistantBubble), |
|---|
| 68 | 68 | borderRadius: BorderRadius.only( |
|---|
| 69 | 69 | topLeft: const Radius.circular(16), |
|---|
| 70 | 70 | topRight: const Radius.circular(16), |
|---|
| .. | .. |
|---|
| 138 | 138 | ? Colors.white.withAlpha(20) |
|---|
| 139 | 139 | : Colors.black.withAlpha(15); |
|---|
| 140 | 140 | |
|---|
| 141 | | - return MarkdownBody( |
|---|
| 142 | | - data: message.content, |
|---|
| 143 | | - selectable: true, |
|---|
| 144 | | - softLineBreak: true, |
|---|
| 145 | | - styleSheet: MarkdownStyleSheet( |
|---|
| 146 | | - p: TextStyle(fontSize: 15, height: 1.4, color: textColor), |
|---|
| 147 | | - h1: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: textColor), |
|---|
| 148 | | - h2: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: textColor), |
|---|
| 149 | | - h3: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: textColor), |
|---|
| 150 | | - strong: TextStyle(fontWeight: FontWeight.bold, color: textColor), |
|---|
| 151 | | - em: TextStyle(fontStyle: FontStyle.italic, color: textColor), |
|---|
| 152 | | - code: TextStyle( |
|---|
| 153 | | - fontSize: 13, |
|---|
| 154 | | - fontFamily: 'monospace', |
|---|
| 155 | | - color: textColor, |
|---|
| 156 | | - backgroundColor: codeBackground, |
|---|
| 157 | | - ), |
|---|
| 158 | | - codeblockDecoration: BoxDecoration( |
|---|
| 159 | | - color: codeBackground, |
|---|
| 160 | | - borderRadius: BorderRadius.circular(8), |
|---|
| 161 | | - ), |
|---|
| 162 | | - codeblockPadding: const EdgeInsets.all(10), |
|---|
| 163 | | - listBullet: TextStyle(fontSize: 15, color: textColor), |
|---|
| 164 | | - blockquoteDecoration: BoxDecoration( |
|---|
| 165 | | - border: Border( |
|---|
| 166 | | - left: BorderSide(color: AppColors.accent, width: 3), |
|---|
| 141 | + // SelectionArea gives one continuous selection across all markdown blocks |
|---|
| 142 | + // (paragraphs, lists, code). MarkdownBody's own `selectable: true` makes each |
|---|
| 143 | + // block a separate SelectableText, which is why dragging snapped paragraph |
|---|
| 144 | + // by paragraph — so it's disabled here in favour of the SelectionArea. |
|---|
| 145 | + return SelectionArea( |
|---|
| 146 | + child: MarkdownBody( |
|---|
| 147 | + data: message.content, |
|---|
| 148 | + selectable: false, |
|---|
| 149 | + softLineBreak: true, |
|---|
| 150 | + styleSheet: MarkdownStyleSheet( |
|---|
| 151 | + p: TextStyle(fontSize: 15, height: 1.4, color: textColor), |
|---|
| 152 | + h1: TextStyle( |
|---|
| 153 | + fontSize: 20, |
|---|
| 154 | + fontWeight: FontWeight.bold, |
|---|
| 155 | + color: textColor, |
|---|
| 167 | 156 | ), |
|---|
| 157 | + h2: TextStyle( |
|---|
| 158 | + fontSize: 18, |
|---|
| 159 | + fontWeight: FontWeight.bold, |
|---|
| 160 | + color: textColor, |
|---|
| 161 | + ), |
|---|
| 162 | + h3: TextStyle( |
|---|
| 163 | + fontSize: 16, |
|---|
| 164 | + fontWeight: FontWeight.bold, |
|---|
| 165 | + color: textColor, |
|---|
| 166 | + ), |
|---|
| 167 | + strong: TextStyle(fontWeight: FontWeight.bold, color: textColor), |
|---|
| 168 | + em: TextStyle(fontStyle: FontStyle.italic, color: textColor), |
|---|
| 169 | + code: TextStyle( |
|---|
| 170 | + fontSize: 13, |
|---|
| 171 | + fontFamily: 'monospace', |
|---|
| 172 | + color: textColor, |
|---|
| 173 | + backgroundColor: codeBackground, |
|---|
| 174 | + ), |
|---|
| 175 | + codeblockDecoration: BoxDecoration( |
|---|
| 176 | + color: codeBackground, |
|---|
| 177 | + borderRadius: BorderRadius.circular(8), |
|---|
| 178 | + ), |
|---|
| 179 | + codeblockPadding: const EdgeInsets.all(10), |
|---|
| 180 | + listBullet: TextStyle(fontSize: 15, color: textColor), |
|---|
| 181 | + blockquoteDecoration: BoxDecoration( |
|---|
| 182 | + border: Border(left: BorderSide(color: AppColors.accent, width: 3)), |
|---|
| 183 | + ), |
|---|
| 184 | + blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), |
|---|
| 168 | 185 | ), |
|---|
| 169 | | - blockquotePadding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), |
|---|
| 170 | | - ), |
|---|
| 171 | | - onTapLink: (text, href, title) { |
|---|
| 172 | | - if (href != null) { |
|---|
| 173 | | - final uri = Uri.tryParse(href); |
|---|
| 174 | | - if (uri != null) { |
|---|
| 175 | | - launchUrl(uri, mode: LaunchMode.externalApplication); |
|---|
| 186 | + onTapLink: (text, href, title) { |
|---|
| 187 | + if (href != null) { |
|---|
| 188 | + final uri = Uri.tryParse(href); |
|---|
| 189 | + if (uri != null) { |
|---|
| 190 | + launchUrl(uri, mode: LaunchMode.externalApplication); |
|---|
| 191 | + } |
|---|
| 176 | 192 | } |
|---|
| 177 | | - } |
|---|
| 178 | | - }, |
|---|
| 193 | + }, |
|---|
| 194 | + ), |
|---|
| 179 | 195 | ); |
|---|
| 180 | 196 | } |
|---|
| 181 | 197 | |
|---|
| .. | .. |
|---|
| 224 | 240 | color: _isUser |
|---|
| 225 | 241 | ? Colors.white.withAlpha(180) |
|---|
| 226 | 242 | : (isDark |
|---|
| 227 | | - ? AppColors.darkTextSecondary |
|---|
| 228 | | - : AppColors.lightTextSecondary), |
|---|
| 243 | + ? AppColors.darkTextSecondary |
|---|
| 244 | + : AppColors.lightTextSecondary), |
|---|
| 229 | 245 | borderRadius: BorderRadius.circular(2), |
|---|
| 230 | 246 | ), |
|---|
| 231 | 247 | ); |
|---|
| .. | .. |
|---|
| 242 | 258 | color: _isUser |
|---|
| 243 | 259 | ? Colors.white70 |
|---|
| 244 | 260 | : (isDark |
|---|
| 245 | | - ? AppColors.darkTextTertiary |
|---|
| 246 | | - : AppColors.lightTextSecondary), |
|---|
| 261 | + ? AppColors.darkTextTertiary |
|---|
| 262 | + : AppColors.lightTextSecondary), |
|---|
| 247 | 263 | ), |
|---|
| 248 | 264 | ), |
|---|
| 249 | 265 | ], |
|---|
| .. | .. |
|---|
| 289 | 305 | _imageCache.remove(_imageCache.keys.first); |
|---|
| 290 | 306 | } |
|---|
| 291 | 307 | final raw = message.imageBase64!; |
|---|
| 292 | | - _imageCache[message.id] = Uint8List.fromList(base64Decode( |
|---|
| 293 | | - raw.contains(',') ? raw.split(',').last : raw, |
|---|
| 294 | | - )); |
|---|
| 308 | + _imageCache[message.id] = Uint8List.fromList( |
|---|
| 309 | + base64Decode(raw.contains(',') ? raw.split(',').last : raw), |
|---|
| 310 | + ); |
|---|
| 295 | 311 | } |
|---|
| 296 | 312 | final bytes = _imageCache[message.id]!; |
|---|
| 297 | 313 | |
|---|
| .. | .. |
|---|
| 301 | 317 | GestureDetector( |
|---|
| 302 | 318 | onTap: () { |
|---|
| 303 | 319 | Navigator.of(context).push( |
|---|
| 304 | | - MaterialPageRoute( |
|---|
| 305 | | - builder: (_) => ImageViewer(imageBytes: bytes), |
|---|
| 306 | | - ), |
|---|
| 320 | + MaterialPageRoute(builder: (_) => ImageViewer(imageBytes: bytes)), |
|---|
| 307 | 321 | ); |
|---|
| 308 | 322 | }, |
|---|
| 309 | 323 | child: ClipRRect( |
|---|
| .. | .. |
|---|
| 348 | 362 | if (isPdf) { |
|---|
| 349 | 363 | icon = Icons.picture_as_pdf; |
|---|
| 350 | 364 | iconColor = Colors.red; |
|---|
| 351 | | - } else if (mime.contains('spreadsheet') || mime.contains('excel') || mime == 'text/csv') { |
|---|
| 365 | + } else if (mime.contains('spreadsheet') || |
|---|
| 366 | + mime.contains('excel') || |
|---|
| 367 | + mime == 'text/csv') { |
|---|
| 352 | 368 | icon = Icons.table_chart; |
|---|
| 353 | 369 | iconColor = Colors.green; |
|---|
| 354 | 370 | } else if (mime.contains('word') || mime.contains('document')) { |
|---|
| .. | .. |
|---|
| 370 | 386 | width: 260, |
|---|
| 371 | 387 | padding: const EdgeInsets.all(12), |
|---|
| 372 | 388 | decoration: BoxDecoration( |
|---|
| 373 | | - color: (_isUser ? Colors.white : Theme.of(context).colorScheme.primary).withAlpha(25), |
|---|
| 389 | + color: |
|---|
| 390 | + (_isUser ? Colors.white : Theme.of(context).colorScheme.primary) |
|---|
| 391 | + .withAlpha(25), |
|---|
| 374 | 392 | borderRadius: BorderRadius.circular(8), |
|---|
| 375 | 393 | border: Border.all( |
|---|
| 376 | 394 | color: (_isUser ? Colors.white : Colors.grey).withAlpha(50), |
|---|
| .. | .. |
|---|
| 402 | 420 | '${mime.split('/').last.toUpperCase()} - ${sizeKB} KB', |
|---|
| 403 | 421 | style: TextStyle( |
|---|
| 404 | 422 | fontSize: 11, |
|---|
| 405 | | - color: (_isUser ? Colors.white : Colors.grey).withAlpha(180), |
|---|
| 423 | + color: (_isUser ? Colors.white : Colors.grey) |
|---|
| 424 | + .withAlpha(180), |
|---|
| 406 | 425 | ), |
|---|
| 407 | 426 | ), |
|---|
| 408 | 427 | ], |
|---|
| .. | .. |
|---|
| 449 | 468 | // Other files: save to temp and share |
|---|
| 450 | 469 | final ext = _mimeToExt(mime); |
|---|
| 451 | 470 | final dir = await getTemporaryDirectory(); |
|---|
| 452 | | - final fileName = '${message.content.isNotEmpty ? message.content.replaceAll(RegExp(r'[^\w\s.-]'), '').trim() : 'file'}.$ext'; |
|---|
| 471 | + final fileName = |
|---|
| 472 | + '${message.content.isNotEmpty ? message.content.replaceAll(RegExp(r'[^\w\s.-]'), '').trim() : 'file'}.$ext'; |
|---|
| 453 | 473 | final file = File('${dir.path}/$fileName'); |
|---|
| 454 | 474 | await file.writeAsBytes(bytes); |
|---|
| 455 | 475 | |
|---|
| .. | .. |
|---|
| 458 | 478 | ); |
|---|
| 459 | 479 | } catch (e) { |
|---|
| 460 | 480 | if (context.mounted) { |
|---|
| 461 | | - ScaffoldMessenger.of(context).showSnackBar( |
|---|
| 462 | | - SnackBar(content: Text('Could not open file: $e')), |
|---|
| 463 | | - ); |
|---|
| 481 | + ScaffoldMessenger.of( |
|---|
| 482 | + context, |
|---|
| 483 | + ).showSnackBar(SnackBar(content: Text('Could not open file: $e'))); |
|---|
| 464 | 484 | } |
|---|
| 465 | 485 | } |
|---|
| 466 | 486 | } |
|---|
| .. | .. |
|---|
| 469 | 489 | const map = { |
|---|
| 470 | 490 | 'application/pdf': 'pdf', |
|---|
| 471 | 491 | 'application/msword': 'doc', |
|---|
| 472 | | - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', |
|---|
| 492 | + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': |
|---|
| 493 | + 'docx', |
|---|
| 473 | 494 | 'application/vnd.ms-excel': 'xls', |
|---|
| 474 | | - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx', |
|---|
| 495 | + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': |
|---|
| 496 | + 'xlsx', |
|---|
| 475 | 497 | 'text/plain': 'txt', |
|---|
| 476 | 498 | 'text/csv': 'csv', |
|---|
| 477 | 499 | 'text/markdown': 'md', |
|---|
| .. | .. |
|---|
| 499 | 521 | fontSize: 11, |
|---|
| 500 | 522 | color: _isUser |
|---|
| 501 | 523 | ? Colors.white60 |
|---|
| 502 | | - : (isDark |
|---|
| 503 | | - ? AppColors.darkTextTertiary |
|---|
| 504 | | - : Colors.grey.shade500), |
|---|
| 524 | + : (isDark ? AppColors.darkTextTertiary : Colors.grey.shade500), |
|---|
| 505 | 525 | ), |
|---|
| 506 | 526 | ), |
|---|
| 507 | 527 | if (message.status == MessageStatus.sending) ...[ |
|---|
| .. | .. |
|---|
| 546 | 566 | ), |
|---|
| 547 | 567 | if (onDelete != null) |
|---|
| 548 | 568 | ListTile( |
|---|
| 549 | | - leading: const Icon(Icons.delete_outline, color: AppColors.error), |
|---|
| 550 | | - title: const Text('Delete', style: TextStyle(color: AppColors.error)), |
|---|
| 569 | + leading: const Icon( |
|---|
| 570 | + Icons.delete_outline, |
|---|
| 571 | + color: AppColors.error, |
|---|
| 572 | + ), |
|---|
| 573 | + title: const Text( |
|---|
| 574 | + 'Delete', |
|---|
| 575 | + style: TextStyle(color: AppColors.error), |
|---|
| 576 | + ), |
|---|
| 551 | 577 | onTap: () { |
|---|
| 552 | 578 | Navigator.pop(ctx); |
|---|
| 553 | 579 | onDelete?.call(); |
|---|