initial commit (migrated)

This commit is contained in:
fiatcode 2025-10-20 16:43:59 +07:00
commit b594facb51
143 changed files with 11057 additions and 0 deletions

22
lib/core/app_updater.dart Normal file
View 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();
}
}

View 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();
}

View 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);
}
}

View 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);
}
}

View 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

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

View file

@ -0,0 +1,8 @@
import 'package:equatable/equatable.dart';
class NoParams extends Equatable {
const NoParams();
@override
List<Object> get props => [];
}

View 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);
}

View 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
View 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');
}

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

View 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});
}

View 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);
}

View 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);
}

View 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

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

View 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);
}
}

View 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);
}
}

View 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);
}
}

View file

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

View file

@ -0,0 +1,6 @@
abstract class ErrorState {
final String message;
final Exception? cause;
const ErrorState({required this.message, this.cause});
}

View 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(),
);
}
}

View 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,
);

View 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),
];
}

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