chore: update morph meaning repo

This commit is contained in:
ggurdin 2025-12-15 13:49:32 -05:00
parent c3f6682fca
commit ba4d05c8af
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
2 changed files with 198 additions and 142 deletions

View file

@ -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,
),
),

View file

@ -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;
}
}
}