chore: update morph meaning repo
This commit is contained in:
parent
c3f6682fca
commit
ba4d05c8af
2 changed files with 198 additions and 142 deletions
|
|
@ -4,11 +4,12 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/languages/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class MorphMeaningWidget extends StatefulWidget {
|
||||
|
|
@ -29,16 +30,16 @@ class MorphMeaningWidget extends StatefulWidget {
|
|||
|
||||
class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
|
||||
bool _editMode = false;
|
||||
|
||||
late TextEditingController _controller;
|
||||
static const int maxCharacters = 140;
|
||||
String? _cachedResponse;
|
||||
|
||||
String? _definition;
|
||||
bool _isLoading = true;
|
||||
Object? _error;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MorphMeaningWidget oldWidget) {
|
||||
if (oldWidget.tag != widget.tag || oldWidget.feature != widget.feature) {
|
||||
_cachedResponse = null;
|
||||
_isLoading = true;
|
||||
_loadMorphMeaning();
|
||||
}
|
||||
|
|
@ -58,28 +59,44 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
MorphInfoRequest get _request => MorphInfoRequest(
|
||||
userL1: MatrixState.pangeaController.userController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
userL2: MatrixState.pangeaController.userController.userL2?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
);
|
||||
|
||||
Future<void> _loadMorphMeaning() async {
|
||||
try {
|
||||
final response = await _morphMeaning();
|
||||
_setMeaningText(response);
|
||||
} catch (e) {
|
||||
_error = e;
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_definition = null;
|
||||
});
|
||||
}
|
||||
|
||||
final response = await _morphMeaning();
|
||||
_controller.text = response.substring(
|
||||
0,
|
||||
min(response.length, maxCharacters),
|
||||
);
|
||||
_definition = response;
|
||||
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
|
||||
Future<String> _morphMeaning() async {
|
||||
if (_cachedResponse != null) {
|
||||
return _cachedResponse!;
|
||||
final result = await MorphInfoRepo.get(
|
||||
MatrixState.pangeaController.userController.accessToken,
|
||||
_request,
|
||||
);
|
||||
|
||||
if (result.isError) {
|
||||
return L10n.of(context).meaningNotFound;
|
||||
}
|
||||
|
||||
final response = await MorphInfoRepo.get(
|
||||
feature: widget.feature,
|
||||
tag: widget.tag,
|
||||
);
|
||||
_cachedResponse = response;
|
||||
return response ?? L10n.of(context).meaningNotFound;
|
||||
final morph = result.result!.getFeatureByCode(widget.feature.name);
|
||||
final data = morph?.getTagByCode(widget.tag);
|
||||
return data?.l1Description ?? L10n.of(context).meaningNotFound;
|
||||
}
|
||||
|
||||
void _toggleEditMode(bool value) => setState(() => _editMode = value);
|
||||
|
|
@ -90,22 +107,15 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
|
|||
? userEdit.substring(0, maxCharacters)
|
||||
: userEdit;
|
||||
|
||||
await MorphInfoRepo.setMorphDefinition(
|
||||
await MorphInfoRepo.update(
|
||||
_request,
|
||||
feature: widget.feature,
|
||||
tag: widget.tag,
|
||||
defintion: truncatedEdit,
|
||||
definition: truncatedEdit,
|
||||
);
|
||||
|
||||
// Update the cached response
|
||||
_cachedResponse = truncatedEdit;
|
||||
_toggleEditMode(false);
|
||||
}
|
||||
|
||||
void _setMeaningText(String initialText) {
|
||||
_controller.text = initialText.substring(
|
||||
0,
|
||||
min(initialText.length, maxCharacters),
|
||||
);
|
||||
_loadMorphMeaning();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -114,27 +124,11 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
|
|||
return const TextLoadingShimmer();
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: _error is UnsubscribedException
|
||||
? ErrorIndicator(
|
||||
message: L10n.of(context).subscribeToUnlockDefinitions,
|
||||
onTap: () {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
},
|
||||
)
|
||||
: ErrorIndicator(
|
||||
message: L10n.of(context).errorFetchingDefinition,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_editMode) {
|
||||
return MorphEditView(
|
||||
morphFeature: widget.feature,
|
||||
morphTag: widget.tag,
|
||||
meaning: _cachedResponse ?? "",
|
||||
meaning: _definition ?? "",
|
||||
controller: _controller,
|
||||
toggleEditMode: _toggleEditMode,
|
||||
editMorphMeaning: editMorphMeaning,
|
||||
|
|
@ -149,7 +143,7 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
|
|||
onDoubleTap: () => _toggleEditMode(true),
|
||||
child: Text(
|
||||
textAlign: TextAlign.center,
|
||||
_cachedResponse ?? L10n.of(context).meaningNotFound,
|
||||
_definition ?? L10n.of(context).meaningNotFound,
|
||||
style: widget.style,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,130 +1,192 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||
import 'package:fluffychat/pangea/languages/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_response.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class _APICallCacheItem {
|
||||
final DateTime time;
|
||||
final Future<MorphInfoResponse> future;
|
||||
class _MorphInfoCacheItem {
|
||||
final Future<Result<MorphInfoResponse>> resultFuture;
|
||||
final DateTime timestamp;
|
||||
|
||||
_APICallCacheItem(this.time, this.future);
|
||||
const _MorphInfoCacheItem({
|
||||
required this.resultFuture,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
class MorphInfoRepo {
|
||||
static final GetStorage _morphMeaningStorage =
|
||||
GetStorage('morph_meaning_storage');
|
||||
static final shortTermCache = <String, _APICallCacheItem>{};
|
||||
static const int _cacheDurationMinutes = 1;
|
||||
// In-memory cache
|
||||
static final Map<String, _MorphInfoCacheItem> _cache = {};
|
||||
static const Duration _cacheDuration = Duration(minutes: 10);
|
||||
|
||||
static void set(MorphInfoRequest request, MorphInfoResponse response) {
|
||||
_morphMeaningStorage.write(request.storageKey, response.toJson());
|
||||
}
|
||||
// Persistent storage
|
||||
static final GetStorage _storage = GetStorage('morph_info_storage');
|
||||
|
||||
static Future<MorphInfoResponse> _fetch(MorphInfoRequest request) async {
|
||||
try {
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.morphDictionary,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = MorphInfoResponse.fromJson(decodedBody);
|
||||
|
||||
set(request, response);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching morph info: $e');
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<MorphInfoResponse> _get(MorphInfoRequest request) async {
|
||||
request.userL1 == request.userL1.split('-').first;
|
||||
request.userL2 == request.userL2.split('-').first;
|
||||
|
||||
final cachedJson = _morphMeaningStorage.read(request.storageKey);
|
||||
if (cachedJson != null) {
|
||||
return MorphInfoResponse.fromJson(cachedJson);
|
||||
static Future<Result<MorphInfoResponse>> get(
|
||||
String accessToken,
|
||||
MorphInfoRequest request,
|
||||
) {
|
||||
// 1. Try memory cache
|
||||
final cached = _getCached(request);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final _APICallCacheItem? cachedCall = shortTermCache[request.storageKey];
|
||||
if (cachedCall != null) {
|
||||
if (DateTime.now().difference(cachedCall.time).inMinutes <
|
||||
_cacheDurationMinutes) {
|
||||
return cachedCall.future;
|
||||
} else {
|
||||
shortTermCache.remove(request.storageKey);
|
||||
}
|
||||
// 2. Try disk cache
|
||||
final stored = _getStored(request);
|
||||
if (stored != null) {
|
||||
return Future.value(Result.value(stored));
|
||||
}
|
||||
|
||||
final future = _fetch(request);
|
||||
shortTermCache[request.storageKey] =
|
||||
_APICallCacheItem(DateTime.now(), future);
|
||||
// 3. Fetch from network (safe future)
|
||||
final future = _safeFetch(accessToken, request);
|
||||
|
||||
// 4. Save to in-memory cache
|
||||
_cache[request.hashCode.toString()] = _MorphInfoCacheItem(
|
||||
resultFuture: future,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
|
||||
// 5. Write to disk *after* the fetch finishes, without rethrowing
|
||||
writeToDisk(request, future);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
static Future<String?> get({
|
||||
required MorphFeaturesEnum feature,
|
||||
required String tag,
|
||||
}) async {
|
||||
final res = await _get(
|
||||
MorphInfoRequest(
|
||||
userL1: MatrixState.pangeaController.userController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
userL2: MatrixState.pangeaController.userController.userL2?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
),
|
||||
);
|
||||
final morph = res.getFeatureByCode(feature.name);
|
||||
|
||||
final data = morph?.getTagByCode(tag);
|
||||
|
||||
return data?.l1Description;
|
||||
static Future<void> set(
|
||||
MorphInfoRequest request,
|
||||
MorphInfoResponse resultFuture,
|
||||
) async {
|
||||
final key = request.hashCode.toString();
|
||||
try {
|
||||
await _storage.write(key, resultFuture.toJson());
|
||||
_cache.remove(key); // Invalidate in-memory cache
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {'request': request.toJson()},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> setMorphDefinition({
|
||||
static Future<void> update(
|
||||
MorphInfoRequest request, {
|
||||
required MorphFeaturesEnum feature,
|
||||
required String tag,
|
||||
required String defintion,
|
||||
required String definition,
|
||||
}) async {
|
||||
final userL1 =
|
||||
MatrixState.pangeaController.userController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage;
|
||||
final userL2 =
|
||||
MatrixState.pangeaController.userController.userL2?.langCode ??
|
||||
LanguageKeys.defaultLanguage;
|
||||
final userL1Short = userL1.split('-').first;
|
||||
final userL2Short = userL2.split('-').first;
|
||||
final cachedJson = _morphMeaningStorage.read(userL1Short + userL2Short);
|
||||
try {
|
||||
final cachedJson = await _getCached(request);
|
||||
final resp = cachedJson?.result ??
|
||||
MorphInfoResponse(
|
||||
userL1: request.userL1,
|
||||
userL2: request.userL2,
|
||||
features: [],
|
||||
);
|
||||
|
||||
MorphInfoResponse? resp = MorphInfoResponse(
|
||||
userL1: userL1,
|
||||
userL2: userL2,
|
||||
features: [],
|
||||
resp.setMorphDefinition(feature.name, tag, definition);
|
||||
await set(request, resp);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {'request': request.toJson()},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Result<MorphInfoResponse>> _safeFetch(
|
||||
String token,
|
||||
MorphInfoRequest request,
|
||||
) async {
|
||||
try {
|
||||
final resp = await _fetch(token, request);
|
||||
return Result.value(resp);
|
||||
} catch (e, s) {
|
||||
// Ensure error is logged and converted to a Result
|
||||
ErrorHandler.logError(e: e, s: s, data: request.toJson());
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<MorphInfoResponse> _fetch(
|
||||
String accessToken,
|
||||
MorphInfoRequest request,
|
||||
) async {
|
||||
final req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: accessToken,
|
||||
);
|
||||
|
||||
if (cachedJson is Map<String, dynamic>) {
|
||||
resp = MorphInfoResponse.fromJson(cachedJson);
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.morphDictionary,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw HttpException(
|
||||
'Failed to fetch morph info: ${res.statusCode} ${res.reasonPhrase}',
|
||||
);
|
||||
}
|
||||
|
||||
resp.setMorphDefinition(feature.name, tag, defintion);
|
||||
await _morphMeaningStorage.write(userL1Short + userL2Short, resp.toJson());
|
||||
return MorphInfoResponse.fromJson(
|
||||
jsonDecode(utf8.decode(res.bodyBytes)),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Result<MorphInfoResponse>>? _getCached(
|
||||
MorphInfoRequest request,
|
||||
) {
|
||||
final now = DateTime.now();
|
||||
final key = request.hashCode.toString();
|
||||
|
||||
// Remove stale entries first
|
||||
_cache.removeWhere(
|
||||
(_, item) => now.difference(item.timestamp) >= _cacheDuration,
|
||||
);
|
||||
|
||||
final item = _cache[key];
|
||||
return item?.resultFuture;
|
||||
}
|
||||
|
||||
static Future<void> writeToDisk(
|
||||
MorphInfoRequest request,
|
||||
Future<Result<MorphInfoResponse>> resultFuture,
|
||||
) async {
|
||||
final result = await resultFuture; // SAFE: never throws
|
||||
|
||||
if (!result.isValue) return; // only cache successful responses
|
||||
await set(request, result.asValue!.value);
|
||||
}
|
||||
|
||||
static MorphInfoResponse? _getStored(
|
||||
MorphInfoRequest request,
|
||||
) {
|
||||
final key = request.hashCode.toString();
|
||||
try {
|
||||
final entry = _storage.read(key);
|
||||
if (entry == null) return null;
|
||||
|
||||
return MorphInfoResponse.fromJson(entry);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {'request': request.toJson()},
|
||||
);
|
||||
_storage.remove(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue