initial commit (migrated)
This commit is contained in:
commit
b594facb51
143 changed files with 11057 additions and 0 deletions
22
lib/core/app_updater.dart
Normal file
22
lib/core/app_updater.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:in_app_update/in_app_update.dart';
|
||||
|
||||
/// A wrapper of in-app update plugin
|
||||
abstract class AppUpdater {
|
||||
/// Check for in-app update
|
||||
Future<AppUpdateInfo> checkForUpdate();
|
||||
|
||||
/// Start update process
|
||||
Future<AppUpdateResult> update();
|
||||
}
|
||||
|
||||
class AppUpdaterImpl implements AppUpdater {
|
||||
@override
|
||||
Future<AppUpdateInfo> checkForUpdate() async {
|
||||
return InAppUpdate.checkForUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AppUpdateResult> update() async {
|
||||
return InAppUpdate.performImmediateUpdate();
|
||||
}
|
||||
}
|
||||
11
lib/core/data/local/config.dart
Normal file
11
lib/core/data/local/config.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/// Config base class
|
||||
abstract class Config<T> {
|
||||
/// Get config value
|
||||
Future<T?> get();
|
||||
|
||||
/// Set config value
|
||||
Future<void> set(T value);
|
||||
|
||||
/// Remove config value
|
||||
Future<void> remove();
|
||||
}
|
||||
47
lib/core/data/local/theme_mode_config.dart
Normal file
47
lib/core/data/local/theme_mode_config.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:kuwot/core/data/local/config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Theme mode shared preferences key
|
||||
const themeModeConfigKey = 'themeMode';
|
||||
|
||||
/// Theme mode configuration
|
||||
class ThemeModeConfig extends Config<ThemeMode> {
|
||||
/// Default constructor
|
||||
ThemeModeConfig({required this.sharedPreferences});
|
||||
|
||||
/// Shared preferences instance
|
||||
final SharedPreferences sharedPreferences;
|
||||
|
||||
@override
|
||||
Future<ThemeMode> get() async {
|
||||
final mode = sharedPreferences.getString(themeModeConfigKey);
|
||||
switch (mode) {
|
||||
case 'dark':
|
||||
return ThemeMode.dark;
|
||||
case 'light':
|
||||
return ThemeMode.light;
|
||||
case 'system':
|
||||
return ThemeMode.system;
|
||||
default:
|
||||
return ThemeMode.system;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> set(ThemeMode value) async {
|
||||
switch (value) {
|
||||
case ThemeMode.dark:
|
||||
await sharedPreferences.setString(themeModeConfigKey, 'dark');
|
||||
case ThemeMode.light:
|
||||
await sharedPreferences.setString(themeModeConfigKey, 'light');
|
||||
case ThemeMode.system:
|
||||
await sharedPreferences.setString(themeModeConfigKey, 'system');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove() async {
|
||||
await sharedPreferences.remove(themeModeConfigKey);
|
||||
}
|
||||
}
|
||||
45
lib/core/data/local/translation_target_config.dart
Normal file
45
lib/core/data/local/translation_target_config.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:kuwot/core/data/local/config.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
part 'translation_target_config.freezed.dart';
|
||||
part 'translation_target_config.g.dart';
|
||||
|
||||
const translationTargetConfigKey = 'translationTarget';
|
||||
const defaultTranslationTarget = TranslationTarget(id: 'en', name: 'English');
|
||||
|
||||
@freezed
|
||||
abstract class TranslationTarget with _$TranslationTarget {
|
||||
const factory TranslationTarget({required String id, required String name}) =
|
||||
_TranslationTarget;
|
||||
|
||||
factory TranslationTarget.fromJson(Map<String, dynamic> json) =>
|
||||
_$TranslationTargetFromJson(json);
|
||||
}
|
||||
|
||||
class TranslationTargetConfig extends Config<TranslationTarget> {
|
||||
TranslationTargetConfig({required this.sharedPreferences});
|
||||
|
||||
final SharedPreferences sharedPreferences;
|
||||
|
||||
@override
|
||||
Future<TranslationTarget?> get() async {
|
||||
final data = sharedPreferences.getString(translationTargetConfigKey);
|
||||
return data != null ? TranslationTarget.fromJson(jsonDecode(data)) : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> set(TranslationTarget value) async {
|
||||
await sharedPreferences.setString(
|
||||
translationTargetConfigKey,
|
||||
jsonEncode(value.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove() async {
|
||||
await sharedPreferences.remove(translationTargetConfigKey);
|
||||
}
|
||||
}
|
||||
280
lib/core/data/local/translation_target_config.freezed.dart
Normal file
280
lib/core/data/local/translation_target_config.freezed.dart
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'translation_target_config.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TranslationTarget {
|
||||
|
||||
String get id; String get name;
|
||||
/// Create a copy of TranslationTarget
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$TranslationTargetCopyWith<TranslationTarget> get copyWith => _$TranslationTargetCopyWithImpl<TranslationTarget>(this as TranslationTarget, _$identity);
|
||||
|
||||
/// Serializes this TranslationTarget to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TranslationTarget&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TranslationTarget(id: $id, name: $name)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $TranslationTargetCopyWith<$Res> {
|
||||
factory $TranslationTargetCopyWith(TranslationTarget value, $Res Function(TranslationTarget) _then) = _$TranslationTargetCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String name
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$TranslationTargetCopyWithImpl<$Res>
|
||||
implements $TranslationTargetCopyWith<$Res> {
|
||||
_$TranslationTargetCopyWithImpl(this._self, this._then);
|
||||
|
||||
final TranslationTarget _self;
|
||||
final $Res Function(TranslationTarget) _then;
|
||||
|
||||
/// Create a copy of TranslationTarget
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [TranslationTarget].
|
||||
extension TranslationTargetPatterns on TranslationTarget {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _TranslationTarget value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TranslationTarget() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _TranslationTarget value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TranslationTarget():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _TranslationTarget value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TranslationTarget() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TranslationTarget() when $default != null:
|
||||
return $default(_that.id,_that.name);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TranslationTarget():
|
||||
return $default(_that.id,_that.name);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TranslationTarget() when $default != null:
|
||||
return $default(_that.id,_that.name);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _TranslationTarget implements TranslationTarget {
|
||||
const _TranslationTarget({required this.id, required this.name});
|
||||
factory _TranslationTarget.fromJson(Map<String, dynamic> json) => _$TranslationTargetFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String name;
|
||||
|
||||
/// Create a copy of TranslationTarget
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$TranslationTargetCopyWith<_TranslationTarget> get copyWith => __$TranslationTargetCopyWithImpl<_TranslationTarget>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$TranslationTargetToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TranslationTarget&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TranslationTarget(id: $id, name: $name)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$TranslationTargetCopyWith<$Res> implements $TranslationTargetCopyWith<$Res> {
|
||||
factory _$TranslationTargetCopyWith(_TranslationTarget value, $Res Function(_TranslationTarget) _then) = __$TranslationTargetCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$TranslationTargetCopyWithImpl<$Res>
|
||||
implements _$TranslationTargetCopyWith<$Res> {
|
||||
__$TranslationTargetCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _TranslationTarget _self;
|
||||
final $Res Function(_TranslationTarget) _then;
|
||||
|
||||
/// Create a copy of TranslationTarget
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,}) {
|
||||
return _then(_TranslationTarget(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
13
lib/core/data/local/translation_target_config.g.dart
Normal file
13
lib/core/data/local/translation_target_config.g.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'translation_target_config.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_TranslationTarget _$TranslationTargetFromJson(Map<String, dynamic> json) =>
|
||||
_TranslationTarget(id: json['id'] as String, name: json['name'] as String);
|
||||
|
||||
Map<String, dynamic> _$TranslationTargetToJson(_TranslationTarget instance) =>
|
||||
<String, dynamic>{'id': instance.id, 'name': instance.name};
|
||||
8
lib/core/domain/no_params.dart
Normal file
8
lib/core/domain/no_params.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class NoParams extends Equatable {
|
||||
const NoParams();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
9
lib/core/domain/use_case.dart
Normal file
9
lib/core/domain/use_case.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:kuwot/core/error/failure.dart';
|
||||
|
||||
/// [TReturnType] is the return type of a successful use case call.
|
||||
/// [TParams] are the parameters that are required to call the use case.
|
||||
abstract class UseCase<TReturnType, TParams> {
|
||||
/// Execute the use case
|
||||
Future<Either<Failure, TReturnType>> call(TParams params);
|
||||
}
|
||||
11
lib/core/either_extensions.dart
Normal file
11
lib/core/either_extensions.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
typedef Function1<T, R> = R Function(T t);
|
||||
|
||||
extension EitherFutureX<L, R1> on Future<Either<L, R1>> {
|
||||
Future<Either<L, R2>> chain<R2>(
|
||||
Function1<R1, Future<Either<L, R2>>> f,
|
||||
) async {
|
||||
return (await this).fold(left, f);
|
||||
}
|
||||
}
|
||||
31
lib/core/env.dart
Normal file
31
lib/core/env.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
abstract class Env {
|
||||
String get quoteApiScheme;
|
||||
|
||||
String get quoteApiHost;
|
||||
|
||||
int? get quoteApiPort;
|
||||
|
||||
String get authPublicKey;
|
||||
|
||||
String get sentryDsn;
|
||||
}
|
||||
|
||||
class EnvImpl implements Env {
|
||||
@override
|
||||
String get quoteApiScheme => const String.fromEnvironment('QUOTE_API_SCHEME');
|
||||
|
||||
@override
|
||||
String get quoteApiHost => const String.fromEnvironment('QUOTE_API_HOST');
|
||||
|
||||
@override
|
||||
int? get quoteApiPort =>
|
||||
int.tryParse(const String.fromEnvironment('QUOTE_API_PORT'));
|
||||
|
||||
@override
|
||||
String get authPublicKey => const String.fromEnvironment('AUTH_PUBLIC_KEY');
|
||||
|
||||
@override
|
||||
String get sentryDsn => const String.fromEnvironment('SENTRY_DSN');
|
||||
}
|
||||
16
lib/core/error/exception.dart
Normal file
16
lib/core/error/exception.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/// Exception class for server error
|
||||
/// Generally, this exception is thrown when the server returns an error response
|
||||
class ServerException implements Exception {
|
||||
const ServerException(this.message);
|
||||
|
||||
final String message;
|
||||
}
|
||||
|
||||
/// Exception class for unauthorized client error
|
||||
/// this exception is thrown when the client is not authorized
|
||||
/// to access the resource (server returns 401)
|
||||
class UnauthorizedException implements Exception {
|
||||
const UnauthorizedException(this.message);
|
||||
|
||||
final String message;
|
||||
}
|
||||
28
lib/core/error/failure.dart
Normal file
28
lib/core/error/failure.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Base class for all failures
|
||||
abstract class Failure extends Equatable {
|
||||
/// Default constructor
|
||||
const Failure({required this.message, this.cause});
|
||||
|
||||
/// Message of the failure
|
||||
final String message;
|
||||
|
||||
/// Cause of the failure
|
||||
final Exception? cause;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, cause];
|
||||
}
|
||||
|
||||
class ClientFailure extends Failure {
|
||||
const ClientFailure({required super.message, super.cause});
|
||||
}
|
||||
|
||||
class ServerFailure extends Failure {
|
||||
const ServerFailure({required super.message, super.cause});
|
||||
}
|
||||
|
||||
class UnknownFailure extends Failure {
|
||||
const UnknownFailure({required super.message, super.cause});
|
||||
}
|
||||
8
lib/core/error/reporter.dart
Normal file
8
lib/core/error/reporter.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
/// Report error to Sentry
|
||||
void reportError({required dynamic error, StackTrace? stackTrace}) {
|
||||
if (error == null) return;
|
||||
|
||||
Sentry.captureException(error, stackTrace: stackTrace);
|
||||
}
|
||||
13
lib/core/error/server_error_model.dart
Normal file
13
lib/core/error/server_error_model.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'server_error_model.freezed.dart';
|
||||
part 'server_error_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ServerErrorModel with _$ServerErrorModel {
|
||||
const factory ServerErrorModel({required String message, required int code}) =
|
||||
_ServerErrorModel;
|
||||
|
||||
factory ServerErrorModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ServerErrorModelFromJson(json);
|
||||
}
|
||||
280
lib/core/error/server_error_model.freezed.dart
Normal file
280
lib/core/error/server_error_model.freezed.dart
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'server_error_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ServerErrorModel {
|
||||
|
||||
String get message; int get code;
|
||||
/// Create a copy of ServerErrorModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ServerErrorModelCopyWith<ServerErrorModel> get copyWith => _$ServerErrorModelCopyWithImpl<ServerErrorModel>(this as ServerErrorModel, _$identity);
|
||||
|
||||
/// Serializes this ServerErrorModel to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerErrorModel&&(identical(other.message, message) || other.message == message)&&(identical(other.code, code) || other.code == code));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message,code);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerErrorModel(message: $message, code: $code)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ServerErrorModelCopyWith<$Res> {
|
||||
factory $ServerErrorModelCopyWith(ServerErrorModel value, $Res Function(ServerErrorModel) _then) = _$ServerErrorModelCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String message, int code
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ServerErrorModelCopyWithImpl<$Res>
|
||||
implements $ServerErrorModelCopyWith<$Res> {
|
||||
_$ServerErrorModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ServerErrorModel _self;
|
||||
final $Res Function(ServerErrorModel) _then;
|
||||
|
||||
/// Create a copy of ServerErrorModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? message = null,Object? code = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,code: null == code ? _self.code : code // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ServerErrorModel].
|
||||
extension ServerErrorModelPatterns on ServerErrorModel {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerErrorModel value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerErrorModel() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerErrorModel value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerErrorModel():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerErrorModel value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerErrorModel() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String message, int code)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerErrorModel() when $default != null:
|
||||
return $default(_that.message,_that.code);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String message, int code) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerErrorModel():
|
||||
return $default(_that.message,_that.code);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String message, int code)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ServerErrorModel() when $default != null:
|
||||
return $default(_that.message,_that.code);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _ServerErrorModel implements ServerErrorModel {
|
||||
const _ServerErrorModel({required this.message, required this.code});
|
||||
factory _ServerErrorModel.fromJson(Map<String, dynamic> json) => _$ServerErrorModelFromJson(json);
|
||||
|
||||
@override final String message;
|
||||
@override final int code;
|
||||
|
||||
/// Create a copy of ServerErrorModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ServerErrorModelCopyWith<_ServerErrorModel> get copyWith => __$ServerErrorModelCopyWithImpl<_ServerErrorModel>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ServerErrorModelToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerErrorModel&&(identical(other.message, message) || other.message == message)&&(identical(other.code, code) || other.code == code));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message,code);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerErrorModel(message: $message, code: $code)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ServerErrorModelCopyWith<$Res> implements $ServerErrorModelCopyWith<$Res> {
|
||||
factory _$ServerErrorModelCopyWith(_ServerErrorModel value, $Res Function(_ServerErrorModel) _then) = __$ServerErrorModelCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String message, int code
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ServerErrorModelCopyWithImpl<$Res>
|
||||
implements _$ServerErrorModelCopyWith<$Res> {
|
||||
__$ServerErrorModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ServerErrorModel _self;
|
||||
final $Res Function(_ServerErrorModel) _then;
|
||||
|
||||
/// Create a copy of ServerErrorModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? message = null,Object? code = null,}) {
|
||||
return _then(_ServerErrorModel(
|
||||
message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,code: null == code ? _self.code : code // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
16
lib/core/error/server_error_model.g.dart
Normal file
16
lib/core/error/server_error_model.g.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'server_error_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ServerErrorModel _$ServerErrorModelFromJson(Map<String, dynamic> json) =>
|
||||
_ServerErrorModel(
|
||||
message: json['message'] as String,
|
||||
code: (json['code'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ServerErrorModelToJson(_ServerErrorModel instance) =>
|
||||
<String, dynamic>{'message': instance.message, 'code': instance.code};
|
||||
51
lib/core/network/network.dart
Normal file
51
lib/core/network/network.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:kuwot/core/error/exception.dart';
|
||||
|
||||
/// Network interface
|
||||
abstract class Network {
|
||||
/// Get data from uri
|
||||
Future<String> get(Uri uri, {Map<String, String>? headers});
|
||||
|
||||
/// Post data to uri
|
||||
Future<String> post(Uri uri, {Map<String, String>? headers, Object? body});
|
||||
}
|
||||
|
||||
/// Network implementation
|
||||
class NetworkImpl implements Network {
|
||||
final _client = http.Client();
|
||||
|
||||
@override
|
||||
Future<String> get(Uri uri, {Map<String, String>? headers}) async {
|
||||
if (kDebugMode) {
|
||||
print('GET: $uri, headers: $headers');
|
||||
}
|
||||
final response = await _client.get(uri, headers: headers);
|
||||
final stringResponse = utf8.decode(response.bodyBytes);
|
||||
|
||||
if (response.statusCode == HttpStatus.unauthorized) {
|
||||
throw UnauthorizedException(stringResponse);
|
||||
}
|
||||
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw ServerException(stringResponse);
|
||||
}
|
||||
|
||||
return stringResponse;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> post(
|
||||
Uri uri, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
}) async {
|
||||
if (kDebugMode) {
|
||||
print('POST: $uri, headers: $headers, body: $body');
|
||||
}
|
||||
final response = await _client.post(uri, headers: headers, body: body);
|
||||
return utf8.decode(response.bodyBytes);
|
||||
}
|
||||
}
|
||||
21
lib/core/presentation/bloc/app_bloc_observer.dart
Normal file
21
lib/core/presentation/bloc/app_bloc_observer.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// ignore_for_file: public_member_api_docs, strict_raw_type
|
||||
|
||||
import 'dart:developer' as dev;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class AppBlocObserver extends BlocObserver {
|
||||
@override
|
||||
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
|
||||
dev.log("[bloc_error] $bloc\nerror: $error\nstacktrace: $stackTrace");
|
||||
super.onError(bloc, error, stackTrace);
|
||||
}
|
||||
|
||||
@override
|
||||
void onChange(BlocBase bloc, Change change) {
|
||||
dev.log(
|
||||
"[${bloc.runtimeType}] ${DateTime.now().toIso8601String()}\nFrom: ${change.currentState}\nNext: ${change.nextState}",
|
||||
);
|
||||
super.onChange(bloc, change);
|
||||
}
|
||||
}
|
||||
24
lib/core/presentation/bloc/config/theme_mode_cubit.dart
Normal file
24
lib/core/presentation/bloc/config/theme_mode_cubit.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:kuwot/core/data/local/config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Theme mode cubit for theme mode management
|
||||
class ThemeModeCubit extends Cubit<ThemeMode> {
|
||||
/// Default [ThemeMode] is [ThemeMode.system]
|
||||
ThemeModeCubit({
|
||||
required this.themeModeConfig,
|
||||
required this.initialThemeMode,
|
||||
}) : super(initialThemeMode);
|
||||
|
||||
/// Theme mode config
|
||||
final Config<ThemeMode> themeModeConfig;
|
||||
|
||||
/// Initial theme mode
|
||||
final ThemeMode initialThemeMode;
|
||||
|
||||
/// Set theme mode
|
||||
void setThemeMode(ThemeMode themeMode) {
|
||||
themeModeConfig.set(themeMode);
|
||||
emit(themeMode);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kuwot/core/data/local/config.dart';
|
||||
import 'package:kuwot/core/data/local/translation_target_config.dart';
|
||||
|
||||
class TranslationTargetCubit extends Cubit<TranslationTarget> {
|
||||
TranslationTargetCubit({
|
||||
required this.translationTargetConfig,
|
||||
required this.initialTranslationTarget,
|
||||
}) : super(initialTranslationTarget);
|
||||
|
||||
final Config<TranslationTarget> translationTargetConfig;
|
||||
final TranslationTarget initialTranslationTarget;
|
||||
|
||||
void set(TranslationTarget translationTarget) {
|
||||
translationTargetConfig.set(translationTarget);
|
||||
emit(translationTarget);
|
||||
}
|
||||
}
|
||||
6
lib/core/presentation/bloc/error_state.dart
Normal file
6
lib/core/presentation/bloc/error_state.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
abstract class ErrorState {
|
||||
final String message;
|
||||
final Exception? cause;
|
||||
|
||||
const ErrorState({required this.message, this.cause});
|
||||
}
|
||||
44
lib/core/presentation/error_retry_snackbar.dart
Normal file
44
lib/core/presentation/error_retry_snackbar.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ErrorRetrySnackbar {
|
||||
final BuildContext context;
|
||||
final String errorMessage;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
const ErrorRetrySnackbar(
|
||||
this.context, {
|
||||
required this.errorMessage,
|
||||
required this.onRetry,
|
||||
});
|
||||
|
||||
SnackBar _build() {
|
||||
return SnackBar(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(days: 1),
|
||||
backgroundColor: Colors.red[600],
|
||||
content: Text(errorMessage),
|
||||
action: SnackBarAction(
|
||||
textColor: Colors.white,
|
||||
label: 'RETRY',
|
||||
onPressed: () {
|
||||
onRetry();
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String errorMessage,
|
||||
required VoidCallback onRetry,
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
ErrorRetrySnackbar(
|
||||
context,
|
||||
errorMessage: errorMessage,
|
||||
onRetry: onRetry,
|
||||
)._build(),
|
||||
);
|
||||
}
|
||||
}
|
||||
19
lib/core/presentation/theme/app_theme.dart
Normal file
19
lib/core/presentation/theme/app_theme.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
/// App light theme
|
||||
ThemeData lightTheme = ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF343A40)),
|
||||
useMaterial3: true,
|
||||
fontFamily: GoogleFonts.dmSans().fontFamily,
|
||||
);
|
||||
|
||||
/// App dark theme
|
||||
ThemeData darkTheme = ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF212529),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
fontFamily: GoogleFonts.dmSans().fontFamily,
|
||||
);
|
||||
13
lib/core/router/app_router.dart
Normal file
13
lib/core/router/app_router.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:kuwot/core/router/app_router.gr.dart';
|
||||
|
||||
@AutoRouterConfig()
|
||||
class AppRouter extends RootStackRouter {
|
||||
@override
|
||||
List<AutoRoute> get routes => [
|
||||
AutoRoute(page: AppUpdateRoute.page, initial: true),
|
||||
AutoRoute(page: QuoteRoute.page),
|
||||
AutoRoute(page: AppSettingsRoute.page),
|
||||
AutoRoute(page: DonationRoute.page),
|
||||
];
|
||||
}
|
||||
83
lib/core/router/app_router.gr.dart
Normal file
83
lib/core/router/app_router.gr.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
// **************************************************************************
|
||||
// AutoRouterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// coverage:ignore-file
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:auto_route/auto_route.dart' as _i5;
|
||||
import 'package:kuwot/features/app_settings/presentation/app_settings_page.dart'
|
||||
as _i1;
|
||||
import 'package:kuwot/features/in_app_purchase/presentation/donation_page.dart'
|
||||
as _i3;
|
||||
import 'package:kuwot/features/in_app_update/presentation/app_update_page.dart'
|
||||
as _i2;
|
||||
import 'package:kuwot/features/quote/presentation/quote_page.dart' as _i4;
|
||||
|
||||
/// generated route for
|
||||
/// [_i1.AppSettingsPage]
|
||||
class AppSettingsRoute extends _i5.PageRouteInfo<void> {
|
||||
const AppSettingsRoute({List<_i5.PageRouteInfo>? children})
|
||||
: super(AppSettingsRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'AppSettingsRoute';
|
||||
|
||||
static _i5.PageInfo page = _i5.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i1.AppSettingsPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i2.AppUpdatePage]
|
||||
class AppUpdateRoute extends _i5.PageRouteInfo<void> {
|
||||
const AppUpdateRoute({List<_i5.PageRouteInfo>? children})
|
||||
: super(AppUpdateRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'AppUpdateRoute';
|
||||
|
||||
static _i5.PageInfo page = _i5.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i2.AppUpdatePage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i3.DonationPage]
|
||||
class DonationRoute extends _i5.PageRouteInfo<void> {
|
||||
const DonationRoute({List<_i5.PageRouteInfo>? children})
|
||||
: super(DonationRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'DonationRoute';
|
||||
|
||||
static _i5.PageInfo page = _i5.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i3.DonationPage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i4.QuotePage]
|
||||
class QuoteRoute extends _i5.PageRouteInfo<void> {
|
||||
const QuoteRoute({List<_i5.PageRouteInfo>? children})
|
||||
: super(QuoteRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'QuoteRoute';
|
||||
|
||||
static _i5.PageInfo page = _i5.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i4.QuotePage();
|
||||
},
|
||||
);
|
||||
}
|
||||
13
lib/core/time.dart
Normal file
13
lib/core/time.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/// Humble class for time related operations.
|
||||
abstract class Time {
|
||||
/// Get unix timestamp in seconds.
|
||||
int getUnixTimestamp();
|
||||
}
|
||||
|
||||
/// Implementation of [Time] using [DateTime].
|
||||
class TimeImpl implements Time {
|
||||
@override
|
||||
int getUnixTimestamp() {
|
||||
return DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue