Skip to content

Commit

Permalink
Set the classes to use the ApiRequestService
Browse files Browse the repository at this point in the history
to remove the dependency on the `language_tool` package;

Enabled changing the spellcheck language on the fly in the `ColoredTextEditingController`.
Implementing #25.
  • Loading branch information
mitryp committed May 29, 2023
1 parent 333aed3 commit 26d40fc
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 95 deletions.
43 changes: 38 additions & 5 deletions lib/core/controllers/colored_text_editing_controller.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'dart:math';
import 'dart:math' show min;

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -26,6 +26,31 @@ class ColoredTextEditingController extends TextEditingController {
/// Callback that will be executed after mistake clicked
ShowPopupCallback? showPopup;

/// Currently set spellcheck language.
String _checkLanguage;

/// Current picky mode.
bool _isPicky;

/// The currently set spellcheck language of this controller.
String get checkLanguage => _checkLanguage;

/// Sets the given [languageCode] as a new check language and re-checks the
/// current text of this controller.
set checkLanguage(String languageCode) {
_checkLanguage = languageCode;
_handleTextChange(value.text, force: true);
}

/// Whether additional spellcheck rules are enabled.
bool get isPicky => _isPicky;

/// Sets the picky mode and re-checks the current text of this controller.
set isPicky(bool value) {
_isPicky = value;
_handleTextChange(this.value.text, force: true);
}

@override
set value(TextEditingValue newValue) {
_handleTextChange(newValue.text);
Expand All @@ -36,7 +61,10 @@ class ColoredTextEditingController extends TextEditingController {
ColoredTextEditingController({
required this.languageCheckService,
this.highlightStyle = const HighlightStyle(),
});
String checkLanguage = 'auto',
bool isPicky = false,
}) : _checkLanguage = checkLanguage,
_isPicky = isPicky;

/// Generates TextSpan from Mistake list
@override
Expand Down Expand Up @@ -72,18 +100,23 @@ class ColoredTextEditingController extends TextEditingController {

/// Clear mistakes list when text mas modified and get a new list of mistakes
/// via API
Future<void> _handleTextChange(String newText) async {
Future<void> _handleTextChange(String newText, {bool force = false}) async {
///set value triggers each time, even when cursor changes its location
///so this check avoid cleaning Mistake list when text wasn't really changed
if (newText == text) return;
if (newText == text && !force) return;

_mistakes.clear();
for (final recognizer in _recognizers) {
recognizer.dispose();
}
_recognizers.clear();

final mistakes = await languageCheckService.findMistakes(newText);
final mistakes = await languageCheckService.findMistakes(
newText,
checkLanguage: checkLanguage,
isPicky: isPicky,
);

_mistakes = mistakes;
notifyListeners();
}
Expand Down
7 changes: 6 additions & 1 deletion lib/domain/language_check_service.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:languagetool_textfield/domain/api_request_service.dart';
import 'package:languagetool_textfield/domain/mistake.dart';

/// A base language check service.
Expand All @@ -6,7 +7,11 @@ abstract class LanguageCheckService {
const LanguageCheckService();

/// Returns found mistakes in the given [text].
Future<List<Mistake>> findMistakes(String text);
Future<List<Mistake>> findMistakes(
String text, {
required String checkLanguage,
required bool isPicky,
});

/// Disposes resources of this [LanguageCheckService].
Future<void> dispose() async {
Expand Down
6 changes: 3 additions & 3 deletions lib/domain/language_fetch_service.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'package:language_tool/language_tool.dart';
import 'package:languagetool_textfield/core/dataclasses/language/supported_language.dart';

/// A language fetch service interface.
abstract class LanguageFetchService {
/// Creates a new [LanguageFetchService].
const LanguageFetchService();

/// Returns a Future List of [Language]s that are supported by the API.
Future<List<Language>> fetchLanguages();
/// Returns a Future List of APIs [SupportedLanguage]s.
Future<List<SupportedLanguage>> fetchLanguages();
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import 'dart:async';

import 'package:language_tool/language_tool.dart';
import 'package:languagetool_textfield/core/dataclasses/language/supported_language.dart';
import 'package:languagetool_textfield/domain/language_fetch_service.dart';

/// A caching decorator for [LanguageFetchService] to not fetch the languages
/// twice.
class CachingLangFetchService implements LanguageFetchService {
static final List<Language> _cachedLanguages = [];
static final List<SupportedLanguage> _cachedLanguages = [];

/// A wrapped [LanguageFetchService]. If there is no cache saved, its
/// [LanguageFetchService.fetchLanguages] method will be used to create it.
Expand All @@ -16,7 +16,7 @@ class CachingLangFetchService implements LanguageFetchService {
const CachingLangFetchService(this.baseService);

@override
Future<List<Language>> fetchLanguages() async {
Future<List<SupportedLanguage>> fetchLanguages() async {
if (_cachedLanguages.isEmpty) {
_cachedLanguages.addAll(await baseService.fetchLanguages());
}
Expand Down
54 changes: 22 additions & 32 deletions lib/implementations/lang_fetch_service/lang_fetch_service.dart
Original file line number Diff line number Diff line change
@@ -1,49 +1,39 @@
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:language_tool/language_tool.dart';
import 'package:languagetool_textfield/domain/api_request_service.dart';
import 'package:languagetool_textfield/domain/language_fetch_service.dart';

import '../../core/dataclasses/language/supported_language.dart';

/// A class that provide the functionality to fetch the supported language list
/// from the langtoolplus API and handles the errors occurred.
class LangFetchService implements LanguageFetchService {
static const String _uri = 'api.languagetoolplus.com';
static const String _path = 'v2/languages';
static final Map<String, String> _headers = {
HttpHeaders.acceptHeader: ContentType.json.value,
};

final ApiRequestService<List<SupportedLanguage>> _fetchService;

/// Creates a new [LangFetchService].
const LangFetchService();
const LangFetchService([
this._fetchService =
const ApiRequestService(_languagesResponseConverter, []),
]);

// todo change to using ErrorWrapper when merged
@override
Future<List<Language>> fetchLanguages() async {
final uri = Uri.https(_uri, _path);
Object? error;

http.Response? response;
try {
response = await http.get(uri, headers: _headers);
} on http.ClientException catch (err) {
error = err;
}

if (error == null && response?.statusCode != HttpStatus.ok) {
error = http.ClientException(
response?.reasonPhrase ?? 'Could not request',
uri,
);
}

if (response == null || response.bodyBytes.isEmpty) {
return [];
}

final decoded =
jsonDecode(utf8.decode(response.bodyBytes)) as List<dynamic>;

return decoded.cast<Map<String, dynamic>>().map(Language.fromJson).toList();
Future<List<SupportedLanguage>> fetchLanguages() async {
final uri = Uri.https(ApiRequestService.apiLink, _path);

return _fetchService.get(uri, headers: _headers);
}
}

List<SupportedLanguage> _languagesResponseConverter(dynamic json) {
final list = json as List<dynamic>;

return list
.cast<Map<String, dynamic>>()
.map(SupportedLanguage.fromJson)
.toList(growable: false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ class DebounceLangToolService extends LanguageCheckService {
) : debouncing = Debouncing(duration: debouncingDuration);

@override
Future<List<Mistake>> findMistakes(String text) async {
Future<List<Mistake>> findMistakes(
String text, {
required String checkLanguage,
required bool isPicky,
}) async {
final value = await debouncing.debounce(() {
return baseService.findMistakes(text);
return baseService.findMistakes(text, checkLanguage: checkLanguage, isPicky: isPicky);
}) as List<Mistake>?;

return value ?? [];
Expand Down
109 changes: 74 additions & 35 deletions lib/implementations/lang_tool_service/lang_tool_service.dart
Original file line number Diff line number Diff line change
@@ -1,50 +1,89 @@
import 'package:language_tool/language_tool.dart';
import 'package:languagetool_textfield/core/enums/mistake_type.dart';
import 'dart:io';

import 'package:languagetool_textfield/core/dataclasses/mistake_match.dart';
import 'package:languagetool_textfield/domain/api_request_service.dart';
import 'package:languagetool_textfield/domain/language_check_service.dart';
import 'package:languagetool_textfield/domain/mistake.dart';

/// An implementation of language check service with language tool service.
/// A basic implementation of a [LanguageCheckService].
class LangToolService extends LanguageCheckService {
/// An instance of this class that is used to interact with LanguageTool API.
final LanguageTool languageTool;
static const String _path = 'v2/check';
static final Map<String, String> _headers = {
HttpHeaders.acceptHeader: ContentType.json.value,
HttpHeaders.contentTypeHeader:
ContentType('application', 'x-www-form-urlencoded', charset: 'utf-8')
.value,
};

final ApiRequestService<List<MistakeMatch>> _fetchService;

/// Creates a new instance of the [LangToolService].
const LangToolService(this.languageTool);
const LangToolService([
this._fetchService =
const ApiRequestService(_mistakesResponseConverter, []),
]);

@override
Future<List<Mistake>> findMistakes(String text) async {
final writingMistakes = await languageTool.check(text);
final mistakes = writingMistakes.map(
(m) => Mistake(
message: m.message,
type: _stringToMistakeType(
m.issueType,
),
offset: m.offset,
length: m.length,
replacements: m.replacements,
),
Future<List<Mistake>> findMistakes(
String text, {
String checkLanguage = 'auto',
bool isPicky = false,
}) async {
final uri = Uri.https(
ApiRequestService.apiLink,
_path,
);

final body = _encodeFormData(
text,
language: checkLanguage,
isPicky: isPicky,
);

final writingMistakes = await _fetchService.post(
uri,
headers: _headers,
body: body,
);

final mistakes = writingMistakes.map(Mistake.fromMatch);

return mistakes.toList();
}

MistakeType _stringToMistakeType(String issueType) {
switch (issueType.toLowerCase()) {
case 'misspelling':
return MistakeType.misspelling;
case 'typographical':
return MistakeType.typographical;
case 'grammar':
return MistakeType.grammar;
case 'uncategorized':
return MistakeType.uncategorized;
case 'non-conformance':
return MistakeType.nonConformance;
case 'style':
return MistakeType.style;
default:
return MistakeType.other;
}
String _encodeFormData(
String text, {
required String language,
required bool isPicky,
}) {
final escapedText = Uri.encodeFull(text);
final formData = '&language=$language'
'&enabledOnly=false'
'&level=${isPicky ? 'picky' : 'default'}'
'&text=$escapedText';

return formData;
}
}

/// expected json structure:
/// ```json
/// {
/// ...
/// matches: [
/// {MistakeMatch},
/// {MistakeMatch},
/// ...
/// ]
/// ...
/// }
/// ```
List<MistakeMatch> _mistakesResponseConverter(dynamic json) {
final checkJson = json as Map<String, dynamic>;
final matchesJson = checkJson['matches'] as List<dynamic>;

return matchesJson
.cast<Map<String, dynamic>>()
.map(MistakeMatch.fromJson)
.toList(growable: false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,18 @@ class ThrottlingLangToolService extends LanguageCheckService {
) : throttling = Throttling(duration: throttlingDuration);

@override
Future<List<Mistake>> findMistakes(String text) =>
throttling.throttle(() => baseService.findMistakes(text))
as Future<List<Mistake>>;
Future<List<Mistake>> findMistakes(
String text, {
required String checkLanguage,
required bool isPicky,
}) =>
throttling.throttle(
() => baseService.findMistakes(
text,
checkLanguage: checkLanguage,
isPicky: isPicky,
),
) as Future<List<Mistake>>;

@override
Future<void> dispose() async {
Expand Down
6 changes: 4 additions & 2 deletions lib/languagetool_textfield.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
library languagetool_textfield;

export 'package:language_tool/language_tool.dart';

export 'core/controllers/colored_text_editing_controller.dart';
export 'core/dataclasses/language/supported_language.dart';
export 'core/dataclasses/mistake_match.dart';
export 'core/dataclasses/mistake_replacement.dart';
export 'domain/api_request_service.dart';
export 'domain/highlight_style.dart';
export 'domain/language_check_service.dart';
export 'domain/mistake.dart';
Expand Down
Loading

0 comments on commit 26d40fc

Please sign in to comment.