improve faq

This commit is contained in:
otsmr 2026-04-22 17:15:45 +02:00
parent 4a8fbdce28
commit 81370d27a9
6 changed files with 91 additions and 22 deletions

@ -1 +1 @@
Subproject commit 24d048b4abbe5c266b09965cc6f3ebdf83f97855
Subproject commit 4e6d3eb9b7602030bf3d4b6dcb92ecc0f23de386

View file

@ -2,11 +2,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:twonly/src/visual/views/settings/help/faq/faq_markdown.view.dart';
class FaqView extends StatefulWidget {
const FaqView({super.key});
@ -19,7 +21,7 @@ class _FaqViewState extends State<FaqView> {
Map<String, dynamic>? _faqData;
String? _locale;
late String domain;
bool noInternet = false;
bool _noInternet = false;
@override
void initState() {
@ -29,30 +31,40 @@ class _FaqViewState extends State<FaqView> {
}
Future<void> _fetchFAQData() async {
final cacheFile = File('${AppEnvironment.cacheDir}/faq.json');
try {
final response = await http.get(Uri.parse('$domain/faq.json'));
if (response.statusCode == 200) {
final jsonData = utf8.decode(response.bodyBytes);
setState(() {
_faqData =
json.decode(utf8.decode(response.bodyBytes))
as Map<String, dynamic>?;
noInternet = false;
_faqData = json.decode(jsonData) as Map<String, dynamic>?;
_noInternet = false;
});
cacheFile.writeAsStringSync(jsonData);
} else {
Log.error('FAQ got ${response.statusCode}');
}
} catch (e) {
Log.error(e);
setState(() {
noInternet = true;
_noInternet = true;
});
}
if (_noInternet && cacheFile.existsSync()) {
final jsonData = cacheFile.readAsStringSync();
setState(() {
_faqData = json.decode(jsonData) as Map<String, dynamic>?;
_noInternet = false;
});
}
}
@override
Widget build(BuildContext context) {
if (noInternet) {
if (_noInternet) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsHelpFAQ),
@ -73,26 +85,39 @@ class _FaqViewState extends State<FaqView> {
}
final faq = _faqData![_locale ?? 'en'] as Map;
final sortedCategories = faq.entries.toList()
..sort((a, b) {
final aPriority = (a.value['meta']['priority'] as num? ?? 0).toInt();
final bPriority = (b.value['meta']['priority'] as num? ?? 0).toInt();
return bPriority.compareTo(aPriority);
});
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsHelpFAQ),
),
body: ListView.builder(
itemCount: faq.keys.length,
itemCount: sortedCategories.length,
itemBuilder: (context, index) {
final category = faq.keys.elementAt(index);
final categoryData = faq[category];
final categoryData = sortedCategories[index].value;
return Card(
child: ExpansionTile(
title: Text(categoryData['meta']['title'] as String),
subtitle: Text(categoryData['meta']['desc'] as String),
shape: const RoundedRectangleBorder(),
backgroundColor: context.color.surfaceContainer,
collapsedShape: const RoundedRectangleBorder(),
children:
categoryData['questions'].map<Widget>((question) {
return ListTile(
title: Text(question['title'] as String),
onTap: () => _launchURL(question['path'] as String),
onTap: () => context.navPush(
FaqMarkdownView(
markdown: question['body'] as String,
title: question['title'] as String,
),
),
);
}).toList()
as List<Widget>,
@ -102,12 +127,4 @@ class _FaqViewState extends State<FaqView> {
),
);
}
Future<void> _launchURL(String path) async {
try {
await launchUrl(Uri.parse('$domain$path'));
} catch (e) {
Log.error('Could not launch $e');
}
}
}

View file

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
class FaqMarkdownView extends StatelessWidget {
const FaqMarkdownView({
required this.markdown,
required this.title,
super.key,
});
final String title;
final String markdown;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(substringBy(title, 30)),
),
body: Markdown(
data: markdown,
padding: const EdgeInsets.only(
top: 16,
left: 16,
right: 16,
bottom: 40,
),
),
);
}
}

View file

@ -3,7 +3,9 @@ import 'dart:async';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
@ -160,7 +162,7 @@ class _UserDiscoveryEnabledCompState extends State<UserDiscoveryEnabledComp> {
subtitle: Text(
context.lang.userDiscoveryEnabledFaq,
),
// onTap: _disableUserDiscovery,
onTap: () => context.push(Routes.settingsHelpFaq),
),
const Divider(),
ListTile(

View file

@ -760,6 +760,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_markdown_plus:
dependency: "direct main"
description:
path: "dependencies/flutter_markdown_plus"
relative: true
source: path
version: "1.0.7"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -1261,6 +1268,14 @@ packages:
relative: true
source: path
version: "3.3.2"
markdown:
dependency: transitive
description:
name: markdown
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
url: "https://pub.dev"
source: hosted
version: "7.3.1"
matcher:
dependency: transitive
description:

View file

@ -85,6 +85,7 @@ dependencies:
hand_signature: ^3.0.3
flutter_sharing_intent: ^2.0.4
no_screenshot: ^0.3.1
flutter_markdown_plus: ^1.0.7
# With high download. (But should be checked nonetheless.)
app_links: ^7.0.0 # 1.6 mio
@ -153,6 +154,8 @@ dependency_overrides:
path: ./dependencies/qr_flutter
no_screenshot:
path: ./dependencies/no_screenshot
flutter_markdown_plus:
path: ./dependencies/flutter_markdown_plus
camera_android_camerax:
# path: ../flutter-packages/packages/camera/camera_android_camerax
git: