OTA Updates for adnroid app and web apk uploader
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
// We render Quill deltas here without depending on the flutter_quill editor
|
||||
// API to avoid analyzer/API mismatches; we support common inline styles.
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
|
||||
import '../models/app_version.dart';
|
||||
import '../services/app_update_service.dart';
|
||||
@@ -25,6 +24,9 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
bool _failed = false;
|
||||
List<dynamic>? _notesDelta;
|
||||
String? _notesPlain;
|
||||
quill.QuillController? _notesController;
|
||||
final FocusNode _notesFocusNode = FocusNode();
|
||||
final ScrollController _notesScrollController = ScrollController();
|
||||
|
||||
Future<void> _startDownload() async {
|
||||
setState(() {
|
||||
@@ -56,11 +58,19 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
final notes = widget.info.latestVersion?.releaseNotes ?? '';
|
||||
|
||||
// parse release notes into a Quill delta list if possible
|
||||
if (_notesDelta == null && _notesPlain == null && notes.isNotEmpty) {
|
||||
if ((_notesDelta == null && _notesPlain == null) && notes.isNotEmpty) {
|
||||
try {
|
||||
final parsed = jsonDecode(notes);
|
||||
if (parsed is List) {
|
||||
_notesDelta = parsed;
|
||||
_notesController = quill.QuillController(
|
||||
document: quill.Document.fromJson(parsed),
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
readOnly: true,
|
||||
);
|
||||
|
||||
// Prevent keyboard focus while still allowing text selection + copy.
|
||||
_notesFocusNode.canRequestFocus = false;
|
||||
} else {
|
||||
_notesPlain = notes;
|
||||
}
|
||||
@@ -85,20 +95,36 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Show current + new versions
|
||||
if (widget.info.latestVersion != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(
|
||||
'Version: ${widget.info.currentBuildNumber} → ${widget.info.latestVersion!.versionCode}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
// Render release notes: prefer Quill delta if available
|
||||
if (_notesDelta != null)
|
||||
if (_notesController != null)
|
||||
SizedBox(
|
||||
height: 250,
|
||||
child: SingleChildScrollView(
|
||||
child: RichText(
|
||||
text: _deltaToTextSpan(
|
||||
_notesDelta!,
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
child: quill.QuillEditor.basic(
|
||||
controller: _notesController!,
|
||||
focusNode: _notesFocusNode,
|
||||
scrollController: _notesScrollController,
|
||||
config: const quill.QuillEditorConfig(
|
||||
// Keep the editor readable/copyable but avoid showing the
|
||||
// selection toolbar or receiving keyboard focus.
|
||||
autoFocus: false,
|
||||
showCursor: false,
|
||||
enableInteractiveSelection: true,
|
||||
enableSelectionToolbar: false,
|
||||
// Prevent the context menu / selection toolbar from showing.
|
||||
contextMenuBuilder: _noContextMenu,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_notesDelta == null &&
|
||||
if (_notesController == null &&
|
||||
_notesPlain != null &&
|
||||
_notesPlain!.isNotEmpty)
|
||||
SizedBox(
|
||||
@@ -163,64 +189,6 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
return actions;
|
||||
}
|
||||
|
||||
TextSpan _deltaToTextSpan(List<dynamic> delta, TextStyle? baseStyle) {
|
||||
final children = <TextSpan>[];
|
||||
|
||||
TextStyle styleFromAttributes(Map? attrs) {
|
||||
var s = baseStyle ?? const TextStyle();
|
||||
if (attrs == null) return s;
|
||||
if (attrs['header'] != null) {
|
||||
final level = attrs['header'] is int ? attrs['header'] as int : 1;
|
||||
s = s.copyWith(
|
||||
fontSize: 18.0 - (level - 1) * 2,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
}
|
||||
if (attrs['bold'] == true) s = s.copyWith(fontWeight: FontWeight.bold);
|
||||
if (attrs['italic'] == true) s = s.copyWith(fontStyle: FontStyle.italic);
|
||||
if (attrs['underline'] == true) {
|
||||
s = s.copyWith(decoration: TextDecoration.underline);
|
||||
}
|
||||
if (attrs['strike'] == true) {
|
||||
s = s.copyWith(decoration: TextDecoration.lineThrough);
|
||||
}
|
||||
if (attrs['color'] is String) {
|
||||
try {
|
||||
final col = attrs['color'] as String;
|
||||
// simple support for hex colors like #rrggbb
|
||||
if (col.startsWith('#') && (col.length == 7 || col.length == 9)) {
|
||||
final hex = col.replaceFirst('#', '');
|
||||
final value = int.parse(hex, radix: 16);
|
||||
s = s.copyWith(
|
||||
color: Color((hex.length == 6 ? 0xFF000000 : 0) | value),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
for (final op in delta) {
|
||||
if (op is Map && op.containsKey('insert')) {
|
||||
final insert = op['insert'];
|
||||
final attrs = op['attributes'] as Map?;
|
||||
String text;
|
||||
if (insert is String) {
|
||||
text = insert;
|
||||
} else if (insert is Map && insert.containsKey('image')) {
|
||||
// render image as alt text placeholder
|
||||
text = '[image]';
|
||||
} else {
|
||||
text = insert.toString();
|
||||
}
|
||||
|
||||
children.add(TextSpan(text: text, style: styleFromAttributes(attrs)));
|
||||
}
|
||||
}
|
||||
|
||||
return TextSpan(children: children, style: baseStyle);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -231,6 +199,14 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
final parsed = jsonDecode(notes);
|
||||
if (parsed is List) {
|
||||
_notesDelta = parsed;
|
||||
_notesController = quill.QuillController(
|
||||
document: quill.Document.fromJson(parsed),
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
readOnly: true,
|
||||
);
|
||||
|
||||
// Prevent keyboard focus while still allowing text selection + copy.
|
||||
_notesFocusNode.canRequestFocus = false;
|
||||
} else {
|
||||
_notesPlain = notes;
|
||||
}
|
||||
@@ -242,6 +218,11 @@ class _UpdateDialogState extends State<UpdateDialog> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_notesFocusNode.dispose();
|
||||
_notesScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static Widget _noContextMenu(BuildContext context, Object state) =>
|
||||
const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user