mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 04:22:12 +00:00
improve faq to directly open a specific question
This commit is contained in:
parent
a93187c86d
commit
8021768883
5 changed files with 222 additions and 50 deletions
89
lib/src/model/json/faq.model.dart
Normal file
89
lib/src/model/json/faq.model.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'faq.model.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FaqData {
|
||||||
|
const FaqData({required this.languages});
|
||||||
|
|
||||||
|
factory FaqData.fromJson(Map<String, dynamic> json) {
|
||||||
|
return FaqData(
|
||||||
|
languages: json.map(
|
||||||
|
(key, value) => MapEntry(
|
||||||
|
key,
|
||||||
|
(value as Map<String, dynamic>).map(
|
||||||
|
(catKey, catValue) => MapEntry(
|
||||||
|
catKey,
|
||||||
|
FaqCategory.fromJson(catValue as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Map<String, FaqCategory>> languages;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => languages.map(
|
||||||
|
(key, value) => MapEntry(
|
||||||
|
key,
|
||||||
|
value.map((catKey, catValue) => MapEntry(catKey, catValue.toJson())),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FaqCategory {
|
||||||
|
const FaqCategory({
|
||||||
|
required this.meta,
|
||||||
|
required this.questions,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FaqCategory.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$FaqCategoryFromJson(json);
|
||||||
|
|
||||||
|
final FaqMeta meta;
|
||||||
|
final List<FaqQuestion> questions;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FaqCategoryToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FaqMeta {
|
||||||
|
const FaqMeta({
|
||||||
|
required this.title,
|
||||||
|
required this.desc,
|
||||||
|
this.priority = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FaqMeta.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$FaqMetaFromJson(json);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String desc;
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: 0)
|
||||||
|
final int priority;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FaqMetaToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class FaqQuestion {
|
||||||
|
const FaqQuestion({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.body,
|
||||||
|
required this.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory FaqQuestion.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$FaqQuestionFromJson(json);
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String body;
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FaqQuestionToJson(this);
|
||||||
|
}
|
||||||
59
lib/src/model/json/faq.model.g.dart
Normal file
59
lib/src/model/json/faq.model.g.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'faq.model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
FaqData _$FaqDataFromJson(Map<String, dynamic> json) => FaqData(
|
||||||
|
languages: (json['languages'] as Map<String, dynamic>).map(
|
||||||
|
(k, e) => MapEntry(
|
||||||
|
k,
|
||||||
|
(e as Map<String, dynamic>).map(
|
||||||
|
(k, e) => MapEntry(k, FaqCategory.fromJson(e as Map<String, dynamic>)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FaqDataToJson(FaqData instance) => <String, dynamic>{
|
||||||
|
'languages': instance.languages,
|
||||||
|
};
|
||||||
|
|
||||||
|
FaqCategory _$FaqCategoryFromJson(Map<String, dynamic> json) => FaqCategory(
|
||||||
|
meta: FaqMeta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||||
|
questions: (json['questions'] as List<dynamic>)
|
||||||
|
.map((e) => FaqQuestion.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FaqCategoryToJson(FaqCategory instance) =>
|
||||||
|
<String, dynamic>{'meta': instance.meta, 'questions': instance.questions};
|
||||||
|
|
||||||
|
FaqMeta _$FaqMetaFromJson(Map<String, dynamic> json) => FaqMeta(
|
||||||
|
title: json['title'] as String,
|
||||||
|
desc: json['desc'] as String,
|
||||||
|
priority: (json['priority'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FaqMetaToJson(FaqMeta instance) => <String, dynamic>{
|
||||||
|
'title': instance.title,
|
||||||
|
'desc': instance.desc,
|
||||||
|
'priority': instance.priority,
|
||||||
|
};
|
||||||
|
|
||||||
|
FaqQuestion _$FaqQuestionFromJson(Map<String, dynamic> json) => FaqQuestion(
|
||||||
|
id: json['id'] as String,
|
||||||
|
title: json['title'] as String,
|
||||||
|
body: json['body'] as String,
|
||||||
|
path: json['path'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FaqQuestionToJson(FaqQuestion instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'title': instance.title,
|
||||||
|
'body': instance.body,
|
||||||
|
'path': instance.path,
|
||||||
|
};
|
||||||
|
|
@ -4,16 +4,18 @@ import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/secure_storage.dart';
|
import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/contact_us/submit_message.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/contact_us/submit_message.view.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
||||||
|
|
||||||
class ContactUsView extends StatefulWidget {
|
class ContactUsView extends StatefulWidget {
|
||||||
const ContactUsView({super.key});
|
const ContactUsView({super.key});
|
||||||
|
|
@ -237,9 +239,7 @@ $debugLogToken
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () => context.push(Routes.settingsHelpFaq),
|
||||||
await launchUrl(Uri.parse('https://twonly.eu/en/faq/'));
|
|
||||||
},
|
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.contactUsFaq,
|
context.lang.contactUsFaq,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|
@ -296,15 +296,6 @@ class IncludeDebugLog extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _IncludeDebugLogState extends State<IncludeDebugLog> {
|
class _IncludeDebugLogState extends State<IncludeDebugLog> {
|
||||||
Future<void> _launchURL() async {
|
|
||||||
const url = 'https://twonly.eu/en/faq/troubleshooting/debug-log.html';
|
|
||||||
if (await launchUrl(Uri.parse(url))) {
|
|
||||||
} else {
|
|
||||||
// ignore: only_throw_errors
|
|
||||||
throw 'Could not launch $url';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
|
|
@ -321,7 +312,11 @@ class _IncludeDebugLogState extends State<IncludeDebugLog> {
|
||||||
Text(context.lang.contactUsIncludeLog),
|
Text(context.lang.contactUsIncludeLog),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _launchURL,
|
onTap: () => context.navPush(
|
||||||
|
const FaqView(
|
||||||
|
questionId: 'debug-log',
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.contactUsWhatsThat,
|
context.lang.contactUsWhatsThat,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_untyped_parameter
|
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/model/json/faq.model.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/faq/faq_markdown.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/faq/faq_markdown.view.dart';
|
||||||
|
|
||||||
class FaqView extends StatefulWidget {
|
class FaqView extends StatefulWidget {
|
||||||
const FaqView({super.key});
|
const FaqView({this.questionId, super.key});
|
||||||
|
|
||||||
|
final String? questionId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FaqView> createState() => _FaqViewState();
|
State<FaqView> createState() => _FaqViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FaqViewState extends State<FaqView> {
|
class _FaqViewState extends State<FaqView> {
|
||||||
Map<String, dynamic>? _faqData;
|
Map<String, FaqCategory>? _faqData;
|
||||||
late String domain;
|
late String domain;
|
||||||
bool _noInternet = false;
|
bool _noInternet = false;
|
||||||
|
|
||||||
|
|
@ -31,14 +33,16 @@ class _FaqViewState extends State<FaqView> {
|
||||||
|
|
||||||
Future<void> _fetchFAQData() async {
|
Future<void> _fetchFAQData() async {
|
||||||
final cacheFile = File('${AppEnvironment.cacheDir}/faq.json');
|
final cacheFile = File('${AppEnvironment.cacheDir}/faq.json');
|
||||||
|
FaqData? faqData;
|
||||||
try {
|
try {
|
||||||
final response = await http.get(Uri.parse('$domain/faq.json'));
|
final response = await http.get(Uri.parse('$domain/faq.json'));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final jsonData = utf8.decode(response.bodyBytes);
|
final jsonData = utf8.decode(response.bodyBytes);
|
||||||
setState(() {
|
setState(() {
|
||||||
_faqData = json.decode(jsonData) as Map<String, dynamic>?;
|
faqData = FaqData.fromJson(
|
||||||
|
json.decode(jsonData) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
_noInternet = false;
|
_noInternet = false;
|
||||||
});
|
});
|
||||||
cacheFile.writeAsStringSync(jsonData);
|
cacheFile.writeAsStringSync(jsonData);
|
||||||
|
|
@ -54,11 +58,45 @@ class _FaqViewState extends State<FaqView> {
|
||||||
if (_noInternet && cacheFile.existsSync()) {
|
if (_noInternet && cacheFile.existsSync()) {
|
||||||
final jsonData = cacheFile.readAsStringSync();
|
final jsonData = cacheFile.readAsStringSync();
|
||||||
setState(() {
|
setState(() {
|
||||||
_faqData = json.decode(jsonData) as Map<String, dynamic>?;
|
faqData = FaqData.fromJson(
|
||||||
|
json.decode(jsonData) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
_noInternet = false;
|
_noInternet = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final locale = Localizations.localeOf(context).languageCode;
|
||||||
|
_faqData = faqData!.languages[locale] ?? faqData!.languages['en'];
|
||||||
|
|
||||||
|
if (widget.questionId != null && _faqData != null) {
|
||||||
|
if (!_navigateToQuestion(widget.questionId!, _faqData!)) {
|
||||||
|
final englishData = faqData!.languages['en'];
|
||||||
|
if (englishData != null && englishData != _faqData) {
|
||||||
|
_navigateToQuestion(widget.questionId!, englishData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _navigateToQuestion(String questionId, Map<String, FaqCategory> data) {
|
||||||
|
for (final category in data.values) {
|
||||||
|
for (final question in category.questions) {
|
||||||
|
if (question.id == questionId) {
|
||||||
|
unawaited(
|
||||||
|
context.navPush(
|
||||||
|
FaqMarkdownView(
|
||||||
|
markdown: question.body,
|
||||||
|
title: question.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -83,17 +121,9 @@ class _FaqViewState extends State<FaqView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final locale = Localizations.localeOf(context).languageCode;
|
final sortedCategories = _faqData!.entries.toList()
|
||||||
var faq = _faqData!['en'] as Map;
|
|
||||||
if (_faqData!.containsKey(locale)) {
|
|
||||||
faq = _faqData![locale] as Map;
|
|
||||||
}
|
|
||||||
|
|
||||||
final sortedCategories = faq.entries.toList()
|
|
||||||
..sort((a, b) {
|
..sort((a, b) {
|
||||||
final aPriority = (a.value['meta']['priority'] as num? ?? 0).toInt();
|
return b.value.meta.priority.compareTo(a.value.meta.priority);
|
||||||
final bPriority = (b.value['meta']['priority'] as num? ?? 0).toInt();
|
|
||||||
return bPriority.compareTo(aPriority);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -107,24 +137,22 @@ class _FaqViewState extends State<FaqView> {
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: ExpansionTile(
|
child: ExpansionTile(
|
||||||
title: Text(categoryData['meta']['title'] as String),
|
title: Text(categoryData.meta.title),
|
||||||
subtitle: Text(categoryData['meta']['desc'] as String),
|
subtitle: Text(categoryData.meta.desc),
|
||||||
shape: const RoundedRectangleBorder(),
|
shape: const RoundedRectangleBorder(),
|
||||||
backgroundColor: context.color.surfaceContainer,
|
backgroundColor: context.color.surfaceContainer,
|
||||||
collapsedShape: const RoundedRectangleBorder(),
|
collapsedShape: const RoundedRectangleBorder(),
|
||||||
children:
|
children: categoryData.questions.map<Widget>((question) {
|
||||||
categoryData['questions'].map<Widget>((question) {
|
return ListTile(
|
||||||
return ListTile(
|
title: Text(question.title),
|
||||||
title: Text(question['title'] as String),
|
onTap: () => context.navPush(
|
||||||
onTap: () => context.navPush(
|
FaqMarkdownView(
|
||||||
FaqMarkdownView(
|
markdown: question.body,
|
||||||
markdown: question['body'] as String,
|
title: question.title,
|
||||||
title: question['title'] as String,
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}).toList(),
|
||||||
}).toList()
|
|
||||||
as List<Widget>,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:twonly/locator.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/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
|
||||||
|
|
@ -14,6 +12,7 @@ import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
|
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/privacy/user_discovery/user_discovery_settings.view.dart';
|
import 'package:twonly/src/visual/views/settings/privacy/user_discovery/user_discovery_settings.view.dart';
|
||||||
|
|
||||||
class UserDiscoveryEnabledComp extends StatefulWidget {
|
class UserDiscoveryEnabledComp extends StatefulWidget {
|
||||||
|
|
@ -158,7 +157,9 @@ class _UserDiscoveryEnabledCompState extends State<UserDiscoveryEnabledComp> {
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
context.lang.userDiscoveryEnabledFaq,
|
context.lang.userDiscoveryEnabledFaq,
|
||||||
),
|
),
|
||||||
onTap: () => context.push(Routes.settingsHelpFaq),
|
onTap: () => context.navPush(
|
||||||
|
const FaqView(questionId: 'user-discovery'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue